@blocklet/js-sdk
Version:
sdk for blocklet development, client only
793 lines (779 loc) • 24.1 kB
JavaScript
import { WELLKNOWN_SERVICE_PATH_PREFIX, SESSION_TOKEN_STORAGE_KEY, REFRESH_TOKEN_STORAGE_KEY } from '@abtnode/constant';
import { withQuery, joinURL } from 'ufo';
import Cookie from 'js-cookie';
import QuickLRU from 'quick-lru';
import isEmpty from 'lodash/isEmpty';
import axios from 'axios';
import omit from 'lodash/omit';
import isObject from 'lodash/isObject';
import stableStringify from 'json-stable-stringify';
import { fromPublicKey } from '@ocap/wallet';
import { toTypeInfo } from '@arcblock/did';
import isUrl from 'is-url';
var __defProp$4 = Object.defineProperty;
var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$4 = (obj, key, value) => {
__defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class AuthService {
constructor({ api }) {
__publicField$4(this, "api");
this.api = api;
}
async getUserPublicInfo({ did }) {
const { data } = await this.api.get("/api/user", {
params: { did }
});
return data;
}
async getUserPrivacyConfig({ did }) {
const { data } = await this.api.get("/api/user/privacy/config", {
params: { did }
});
return data;
}
async saveUserPrivacyConfig(config) {
const { data } = await this.api.post("/api/user/privacy/config", config);
return data;
}
async getUserNotificationConfig() {
const { data } = await this.api.get("/api/user/notification/config");
return data;
}
async saveUserNotificationConfig(config) {
const { data } = await this.api.post("/api/user/notification/config", config);
return data;
}
async testNotificationWebhook(webhook) {
const { data } = await this.api.put("/api/user/notification/webhook", webhook);
return data;
}
// eslint-disable-next-line require-await
async getProfileUrl({ did, locale }) {
const url = `${WELLKNOWN_SERVICE_PATH_PREFIX}/user`;
return withQuery(url, {
did,
locale
});
}
async getProfile() {
const { data } = await this.api.get("/api/user/profile");
return data;
}
async refreshProfile() {
await this.api.put("/api/user/refreshProfile");
}
async saveProfile({
locale,
inviter,
metadata,
address
}) {
const { data } = await this.api.put("/api/user/profile", { locale, inviter, metadata, address });
return data;
}
async updateDidSpace({ spaceGateway }) {
await this.api.put("/api/user/updateDidSpace", { spaceGateway });
}
/**
* 指定要退出登录的设备 id
* 指定要退出登录的会话状态
* @param {{ visitorId: string, status: string }} { visitorId, status }
* @return {Promise<void>}
*/
async logout({
visitorId,
status,
includeFederated
}) {
const { data } = await this.api.post("/api/user/logout", { visitorId, status, includeFederated });
return data;
}
/**
* 删除当前登录用户
* @return {Promise<{did: string}>}
*/
async destroyMyself() {
const { data } = await this.api.delete("/api/user");
return data;
}
}
class TokenService {
getSessionToken(config) {
if (Cookie.get(SESSION_TOKEN_STORAGE_KEY)) {
return Cookie.get(SESSION_TOKEN_STORAGE_KEY);
}
if (config.sessionTokenKey) {
return window.localStorage.getItem(config.sessionTokenKey);
}
return "";
}
setSessionToken(value) {
Cookie.set(SESSION_TOKEN_STORAGE_KEY, value);
}
removeSessionToken() {
Cookie.remove(SESSION_TOKEN_STORAGE_KEY);
}
getRefreshToken() {
return localStorage.getItem(REFRESH_TOKEN_STORAGE_KEY);
}
setRefreshToken(value) {
localStorage.setItem(REFRESH_TOKEN_STORAGE_KEY, value);
}
removeRefreshToken() {
localStorage.removeItem(REFRESH_TOKEN_STORAGE_KEY);
}
}
const blockletCache = new QuickLRU({ maxSize: 30, maxAge: 60 * 1e3 });
class BlockletService {
getBlocklet(baseUrl, force = false) {
if (!baseUrl) {
if (typeof window === "undefined" || typeof document === "undefined") {
throw new Error("Cannot get blocklet in server side without baseUrl");
}
return window.blocklet;
}
if (!force && blockletCache.has(baseUrl)) {
return blockletCache.get(baseUrl);
}
const url = withQuery(joinURL(baseUrl, "__blocklet__.js"), {
type: "json",
t: Date.now()
});
return new Promise(async (resolve) => {
const res = await fetch(url);
const data = await res.json();
blockletCache.set(baseUrl, data);
resolve(data);
});
}
loadBlocklet() {
return new Promise((resolve, reject) => {
if (typeof window === "undefined" || typeof document === "undefined") {
reject();
return;
}
const blockletScript = document.createElement("script");
let basename = "/";
if (window.blocklet && window.blocklet.prefix) {
basename = window.blocklet.prefix;
}
blockletScript.src = withQuery(joinURL(basename, "__blocklet__.js"), {
t: Date.now()
});
blockletScript.onload = () => {
resolve();
};
blockletScript.onerror = () => {
reject();
};
document.head.append(blockletScript);
});
}
getPrefix(blocklet) {
if (blocklet) {
return blocklet?.prefix || "/";
}
if (typeof window === "undefined" || typeof document === "undefined") {
return null;
}
return window.blocklet?.prefix || "/";
}
}
var __defProp$3 = Object.defineProperty;
var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$3 = (obj, key, value) => {
__defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class UserSessionService {
constructor({ api, blocklet }) {
__publicField$3(this, "api");
__publicField$3(this, "blocklet");
this.api = api;
this.blocklet = blocklet || new BlockletService();
}
getBaseUrl(appUrl) {
return appUrl ? joinURL(appUrl, WELLKNOWN_SERVICE_PATH_PREFIX) : void 0;
}
async getUserSessions({ did, appUrl }) {
const baseURL = this.getBaseUrl(appUrl);
const blocklet = await this.blocklet.getBlocklet();
const { data } = await this.api.get("/api/user-session", {
baseURL,
params: {
userDid: did,
appPid: blocklet.appPid
}
});
return data;
}
/**
* 获取个人的所有登录会话
*/
async getMyLoginSessions({ appUrl } = {}, params = { page: 1, pageSize: 10 }) {
const baseURL = this.getBaseUrl(appUrl);
const { data } = await this.api.get("/api/user-session/myself", { baseURL, params });
return data;
}
async loginByUserSession({
id,
appPid,
userDid,
passportId,
appUrl
}) {
const baseURL = this.getBaseUrl(appUrl);
const { data } = await this.api.post(
"/api/user-session/login",
{
id,
appPid,
userDid,
passportId
},
{ baseURL }
);
return data;
}
}
var __defProp$2 = Object.defineProperty;
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$2 = (obj, key, value) => {
__defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class ComponentService {
constructor({ blocklet = window.blocklet } = {}) {
__publicField$2(this, "blocklet");
this.blocklet = blocklet;
}
getComponent(name) {
const componentMountPoints = this.blocklet?.componentMountPoints || [];
const item = componentMountPoints.find((x) => [x.title, x.name, x.did].includes(name));
return item;
}
getComponentMountPoint(name) {
const component = this.getComponent(name);
return component?.mountPoint || "";
}
getUrl(name, ...parts) {
const mountPoint = this.getComponentMountPoint(name);
const appUrl = this.blocklet?.appUrl || "";
return joinURL(appUrl, mountPoint, ...parts);
}
}
var __defProp$1 = Object.defineProperty;
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$1 = (obj, key, value) => {
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class FederatedService {
constructor({ api, blocklet }) {
__publicField$1(this, "api");
__publicField$1(this, "blocklet");
__publicField$1(this, "blockletDataCache", {});
this.api = api;
this.blocklet = blocklet || new BlockletService();
}
async getTrustedDomains() {
const { data } = await this.api.get("/api/federated/getTrustedDomains");
return data;
}
getMaster(blocklet = this.blocklet.getBlocklet()) {
const federated = blocklet?.settings?.federated;
return federated?.master;
}
getConfig(blocklet = this.blocklet.getBlocklet()) {
const federated = blocklet?.settings?.federated;
return federated?.config;
}
getFederatedEnabled(blocklet = this.blocklet.getBlocklet()) {
const config = this.getConfig(blocklet);
return config?.status === "approved";
}
getSourceAppPid(blocklet = this.blocklet.getBlocklet()) {
const master = this.getMaster(blocklet);
return master?.appPid;
}
getFederatedApp(blocklet = this.blocklet.getBlocklet()) {
const master = this.getMaster(blocklet);
const isFederatedMode = !isEmpty(master);
if (!isFederatedMode) {
return null;
}
return {
appId: master.appId,
appName: master.appName,
appDescription: master.appDescription,
appLogo: master.appLogo,
appPid: master.appPid,
appUrl: master.appUrl,
version: master.version,
sourceAppPid: master.appPid,
provider: "wallet"
};
}
getCurrentApp(blocklet = this.blocklet.getBlocklet()) {
if (blocklet) {
return {
appId: blocklet.appId,
appName: blocklet.appName,
appDescription: blocklet.appDescription,
appLogo: blocklet.appLogo,
appPid: blocklet.appPid,
appUrl: blocklet.appUrl,
version: blocklet.version,
// NOTICE: null 代表该值置空
sourceAppPid: null,
provider: "wallet"
};
}
if (window.env) {
const server = window.env;
return {
appId: server.appId,
appName: server.appName,
appDescription: server.appDescription,
appUrl: server.baseUrl,
// NOTICE: null 代表该值置空
sourceAppPid: null,
provider: "wallet",
type: "server"
};
}
return null;
}
getApps(blocklet = this.blocklet.getBlocklet()) {
const appList = [];
const masterApp = this.getFederatedApp(blocklet);
const currentApp = this.getCurrentApp(blocklet);
const federatedEnabled = this.getFederatedEnabled(blocklet);
if (currentApp) {
appList.push(currentApp);
}
if (masterApp && masterApp?.appId !== currentApp?.appId && federatedEnabled) {
appList.push(masterApp);
}
return appList.reverse();
}
async getBlockletData(appUrl, force = false) {
if (!force && this.blockletDataCache[appUrl]) {
return this.blockletDataCache[appUrl];
}
try {
const url = new URL("__blocklet__.js", appUrl);
url.searchParams.set("type", "json");
const res = await fetch(url.href);
const jsonData = await res.json();
this.blockletDataCache[appUrl] = jsonData;
return jsonData;
} catch (err) {
console.error(`Failed to get blocklet data: ${appUrl}`, err);
return null;
}
}
}
const version = "1.16.45";
const sleep = (time = 0) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, time);
});
};
const getBearerToken = (token) => {
return `Bearer ${encodeURIComponent(token)}`;
};
const visitorIdKey = "__visitor_id";
const getVisitorId = () => {
return localStorage.getItem(visitorIdKey);
};
const verifyResponse = (response, onInvalid) => {
if (isObject(response.data) && response.status >= 200 && response.status < 300 && window.blocklet && window.blocklet.appId && window.blocklet.appPk) {
if (!response.data.$signature) {
onInvalid();
throw new Error("Invalid response");
}
const { appId, appPk } = window.blocklet;
const wallet = fromPublicKey(appPk, toTypeInfo(appId));
if (wallet.verify(stableStringify(omit(response.data, ["$signature"])), response.data.$signature) === false) {
onInvalid();
throw new Error("Invalid response");
}
}
return response;
};
function getCSRFToken() {
return Cookie.get("x-csrf-token");
}
async function sleepForLoading(config, lazyTime = 300) {
config.metaData.endTime = +/* @__PURE__ */ new Date();
const { startTime, endTime } = config.metaData;
const timeDiff = endTime - startTime;
if (timeDiff < lazyTime)
await sleep(lazyTime - timeDiff);
delete config.metaData;
}
const createAxios$1 = (options, requestParams) => {
const headers = {
...options?.headers,
"x-blocklet-js-sdk-version": version
};
const componentService = new ComponentService();
const instance = axios.create({
...options,
headers
});
if (requestParams?.lazy) {
instance.interceptors.request.use(
(config) => {
config.metaData = { startTime: +/* @__PURE__ */ new Date() };
return config;
},
(err) => Promise.reject(err)
);
instance.interceptors.response.use(
async (res) => {
if (res.config) {
await sleepForLoading(res.config, requestParams?.lazyTime);
}
return res;
},
async (err) => {
if (err.response) {
await sleepForLoading(err.response.config, requestParams?.lazyTime);
}
return Promise.reject(err);
}
);
}
instance.interceptors.request.use(
(config) => {
const componentDid = requestParams?.componentDid ?? window.blocklet?.componentId?.split("/").pop();
config.baseURL = config.baseURL || componentService.getComponentMountPoint(componentDid);
config.timeout = config.timeout || 20 * 1e3;
config.headers["x-csrf-token"] = getCSRFToken();
const visitorId = getVisitorId();
if (![void 0, null].includes(visitorId)) {
config.headers["x-blocklet-visitor-id"] = visitorId;
}
return config;
},
(error) => Promise.reject(error)
);
return instance;
};
async function renewRefreshToken$1(refreshToken) {
if (!refreshToken) {
throw new Error("Refresh token not found");
}
const refreshApi = createAxios$1({
baseURL: WELLKNOWN_SERVICE_PATH_PREFIX,
timeout: 10 * 1e3,
secure: true,
headers: {
authorization: getBearerToken(refreshToken)
}
});
const { data } = await refreshApi.post("/api/did/refreshSession");
return data;
}
function createRequest$1({
getSessionToken,
setSessionToken,
removeSessionToken,
getRefreshToken,
setRefreshToken,
removeRefreshToken,
onRefreshTokenError,
onRefreshTokenSuccess
}, requestOptions, requestParams) {
let refreshingTokenRequest = null;
const service = createAxios$1(
{
timeout: 30 * 1e3,
...requestOptions
},
requestParams
);
service.interceptors.request.use(
async (config) => {
if (!Cookie.get(SESSION_TOKEN_STORAGE_KEY)) {
const token = getSessionToken(config);
if (token) {
config.headers.authorization = getBearerToken(token);
}
}
if (refreshingTokenRequest) {
await refreshingTokenRequest;
}
return config;
},
(error) => Promise.reject(error)
);
service.interceptors.response.use(
(response) => {
if (response.config?.secure) {
return verifyResponse(response, () => {
removeSessionToken();
removeRefreshToken();
});
}
return response;
},
async (error) => {
const originalRequest = error.config;
if (originalRequest) {
originalRequest.headers = originalRequest?.headers ? { ...originalRequest.headers } : {};
if (error?.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
if (!refreshingTokenRequest) {
refreshingTokenRequest = renewRefreshToken$1(getRefreshToken());
}
const tokenData = await refreshingTokenRequest;
setSessionToken(tokenData.nextToken);
setRefreshToken(tokenData.nextRefreshToken);
if (typeof onRefreshTokenSuccess === "function") {
onRefreshTokenSuccess(tokenData);
}
return service(originalRequest);
} catch (refreshTokenError) {
removeSessionToken();
removeRefreshToken();
if (typeof onRefreshTokenError === "function") {
onRefreshTokenError(refreshTokenError);
}
return Promise.reject(error);
} finally {
refreshingTokenRequest = null;
}
}
}
return Promise.reject(error);
}
);
return service;
}
function createFetch$1(globalOptions = {}, requestParams) {
return async (input, options) => {
const startAt = Date.now();
const headers = {
...globalOptions?.headers,
...options?.headers,
"x-csrf-token": getCSRFToken(),
"x-blocklet-js-sdk-version": version
};
const visitorId = getVisitorId();
if (![void 0, null].includes(visitorId)) {
headers["x-blocklet-visitor-id"] = visitorId;
}
const request = fetch(input, {
...globalOptions,
...options,
headers
});
try {
return request;
} catch (error) {
throw error;
} finally {
const endAt = Date.now();
if (requestParams?.lazy) {
const lazyTime = requestParams?.lazyTime ?? 300;
const timeDiff = endAt - startAt;
if (timeDiff < lazyTime)
await sleep(lazyTime - timeDiff);
}
}
};
}
async function renewRefreshToken(refreshToken) {
if (!refreshToken) {
throw new Error("Refresh token not found");
}
const refreshApi = createFetch$1();
const res = await refreshApi(joinURL(WELLKNOWN_SERVICE_PATH_PREFIX, "/api/did/refreshSession"), {
method: "POST",
headers: {
authorization: getBearerToken(refreshToken)
}
});
const data = await res.json();
return data;
}
function createRequest({
baseURL,
getSessionToken,
setSessionToken,
removeSessionToken,
getRefreshToken,
setRefreshToken,
removeRefreshToken,
onRefreshTokenError,
onRefreshTokenSuccess
}, requestOptions, requestParams) {
let refreshingTokenRequest = null;
const service = createFetch$1(requestOptions, requestParams);
const componentService = new ComponentService();
return async (input, options) => {
let authorization;
let finalUrl = input;
if (typeof input === "string") {
if (!isUrl(input)) {
if (baseURL) {
finalUrl = joinURL(baseURL, input);
} else {
const componentDid = requestParams?.componentDid ?? window.blocklet?.componentId?.split("/").pop();
const mountPoint = componentService.getComponentMountPoint(componentDid);
finalUrl = joinURL(mountPoint, input);
}
}
}
if (!Cookie.get(SESSION_TOKEN_STORAGE_KEY)) {
const token = getSessionToken(requestOptions);
if (token) {
authorization = getBearerToken(token);
}
}
if (refreshingTokenRequest) {
await refreshingTokenRequest;
}
const res = await service(finalUrl, {
...options,
headers: {
...options?.headers,
authorization
}
});
if (!res.ok && res.status === 401) {
refreshingTokenRequest = renewRefreshToken(getRefreshToken());
try {
const tokenData = await refreshingTokenRequest;
setSessionToken(tokenData.nextToken);
setRefreshToken(tokenData.nextRefreshToken);
if (typeof onRefreshTokenSuccess === "function") {
onRefreshTokenSuccess(tokenData);
}
return service(finalUrl, {
...options,
headers: {
...options?.headers,
authorization
}
});
} catch (error) {
removeSessionToken();
removeRefreshToken();
if (typeof onRefreshTokenError === "function") {
onRefreshTokenError(error);
}
return res;
} finally {
refreshingTokenRequest = null;
}
}
if (res.ok && options?.secure) {
verifyResponse({ status: res.status, data: await res.json() }, () => {
removeSessionToken();
removeRefreshToken();
});
}
return res;
};
}
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class BlockletSDK {
constructor() {
__publicField(this, "api");
__publicField(this, "user");
__publicField(this, "userSession");
__publicField(this, "token");
__publicField(this, "blocklet");
__publicField(this, "federated");
const tokenService = new TokenService();
const internalApi = createRequest$1(
{
getSessionToken: tokenService.getSessionToken,
setSessionToken: tokenService.setSessionToken,
removeSessionToken: tokenService.removeSessionToken,
getRefreshToken: tokenService.getRefreshToken,
setRefreshToken: tokenService.setRefreshToken,
removeRefreshToken: tokenService.removeRefreshToken,
onRefreshTokenError: () => {
console.error("Failed to refresh token");
},
onRefreshTokenSuccess: () => {
}
},
{
baseURL: WELLKNOWN_SERVICE_PATH_PREFIX
}
);
const blocklet = new BlockletService();
this.user = new AuthService({ api: internalApi });
this.federated = new FederatedService({ api: internalApi, blocklet });
this.userSession = new UserSessionService({ api: internalApi, blocklet });
this.token = tokenService;
this.blocklet = blocklet;
this.api = internalApi;
}
}
function createAxios(config = {}, requestParams = {}) {
const tokenService = new TokenService();
return createRequest$1(
{
getSessionToken: tokenService.getSessionToken,
setSessionToken: tokenService.setSessionToken,
removeSessionToken: tokenService.removeSessionToken,
getRefreshToken: tokenService.getRefreshToken,
setRefreshToken: tokenService.setRefreshToken,
removeRefreshToken: tokenService.removeRefreshToken,
onRefreshTokenError: () => {
console.error("Failed to refresh token");
},
onRefreshTokenSuccess: () => {
}
},
config,
requestParams
);
}
function createFetch(options, requestParams) {
const tokenService = new TokenService();
return createRequest(
{
getSessionToken: tokenService.getSessionToken,
setSessionToken: tokenService.setSessionToken,
removeSessionToken: tokenService.removeSessionToken,
getRefreshToken: tokenService.getRefreshToken,
setRefreshToken: tokenService.setRefreshToken,
removeRefreshToken: tokenService.removeRefreshToken,
onRefreshTokenError: () => {
console.error("Failed to refresh token");
},
onRefreshTokenSuccess: () => {
}
},
options,
requestParams
);
}
const getBlockletSDK = /* @__PURE__ */ (() => {
let instance;
return () => {
if (!instance) {
instance = new BlockletSDK();
}
return instance;
};
})();
export { BlockletSDK, createAxios, createFetch, getBlockletSDK, getCSRFToken };