UNPKG

@blocklet/js-sdk

Version:

sdk for blocklet development, client only

793 lines (779 loc) 24.1 kB
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 };