@toruslabs/session-manager
Advanced tools
Sorry, the diff of this file is not supported yet
| 'use strict'; | ||
| var _defineProperty = require('@babel/runtime/helpers/defineProperty'); | ||
| class LocalStorageHandler { | ||
| constructor(storage, baseStorageKey) { | ||
| _defineProperty(this, "storage", void 0); | ||
| _defineProperty(this, "baseStorageKey", void 0); | ||
| this.storage = storage; | ||
| this.baseStorageKey = baseStorageKey; | ||
| } | ||
| async storeData(key, data) { | ||
| const storageKey = this.getStorageKey(key); | ||
| this.storage.setItem(storageKey, JSON.stringify(data)); | ||
| } | ||
| async retrieveData(key) { | ||
| const storageKey = this.getStorageKey(key); | ||
| const localData = this.storage.getItem(storageKey); | ||
| if (!localData) return undefined; | ||
| try { | ||
| return JSON.parse(localData); | ||
| } catch { | ||
| // Corrupted cache should not block retrieval. | ||
| this.storage.removeItem(storageKey); | ||
| return undefined; | ||
| } | ||
| } | ||
| clearStorage(key) { | ||
| this.storage.removeItem(this.getStorageKey(key)); | ||
| } | ||
| clearOrphanedData(baseKey = "") { | ||
| const keyPrefix = `${this.baseStorageKey}${baseKey}`; | ||
| const keysToRemove = []; | ||
| for (let index = 0; index < this.storage.length; index += 1) { | ||
| const storageKey = this.storage.key(index); | ||
| if (storageKey && storageKey.startsWith(keyPrefix)) { | ||
| keysToRemove.push(storageKey); | ||
| } | ||
| } | ||
| keysToRemove.forEach(key => { | ||
| this.storage.removeItem(key); | ||
| }); | ||
| } | ||
| getStorageKey(key) { | ||
| return `${this.baseStorageKey}${key}`; | ||
| } | ||
| } | ||
| exports.LocalStorageHandler = LocalStorageHandler; |
| 'use strict'; | ||
| var _defineProperty = require('@babel/runtime/helpers/defineProperty'); | ||
| var eccrypto = require('@toruslabs/eccrypto'); | ||
| var metadataHelpers = require('@toruslabs/metadata-helpers'); | ||
| class ServerHandler { | ||
| constructor(sessionServerBaseUrl, request) { | ||
| _defineProperty(this, "sessionServerBaseUrl", void 0); | ||
| _defineProperty(this, "request", void 0); | ||
| this.sessionServerBaseUrl = sessionServerBaseUrl; | ||
| this.request = request; | ||
| } | ||
| async storeData(key, data, options = {}) { | ||
| var _options$operation; | ||
| const privKey = metadataHelpers.hexToBytes(key); | ||
| const pubKey = metadataHelpers.bytesToHex(eccrypto.getPublic(privKey)); | ||
| const encData = await metadataHelpers.encryptData(key, data); | ||
| const signature = metadataHelpers.bytesToHex(await eccrypto.sign(privKey, metadataHelpers.keccak256(metadataHelpers.utf8ToBytes(encData)))); | ||
| const body = { | ||
| key: pubKey, | ||
| data: encData, | ||
| signature, | ||
| namespace: options.namespace, | ||
| allowedOrigin: options.allowedOrigin | ||
| }; | ||
| const operation = (_options$operation = options.operation) !== null && _options$operation !== void 0 ? _options$operation : "create"; | ||
| if (operation === "update") { | ||
| await this.request({ | ||
| method: "PUT", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/update`, | ||
| data: body, | ||
| headers: options.headers | ||
| }); | ||
| return; | ||
| } | ||
| if (operation === "create") { | ||
| body.timeout = options.timeout; | ||
| } else if (operation === "invalidate") { | ||
| body.timeout = 1; | ||
| } | ||
| await this.request({ | ||
| method: "POST", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/set`, | ||
| data: body, | ||
| headers: options.headers | ||
| }); | ||
| } | ||
| async retrieveData(key, options = {}) { | ||
| const pubKey = metadataHelpers.bytesToHex(eccrypto.getPublic(metadataHelpers.hexToBytes(key))); | ||
| const body = { | ||
| key: pubKey, | ||
| namespace: options.namespace | ||
| }; | ||
| const result = await this.request({ | ||
| method: "POST", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/get`, | ||
| data: body, | ||
| headers: options.headers | ||
| }); | ||
| if (!result.message) { | ||
| throw new Error("Session Expired or Invalid public key"); | ||
| } | ||
| const response = await metadataHelpers.decryptData(key, result.message); | ||
| if (response.error) { | ||
| throw new Error("There was an error decrypting data."); | ||
| } | ||
| return response; | ||
| } | ||
| clearStorage(_key) { | ||
| // No-op. Server entries expire based on timeout. | ||
| } | ||
| clearOrphanedData(_baseKey = "") { | ||
| // No-op. Server entries expire based on timeout. | ||
| } | ||
| } | ||
| exports.ServerHandler = ServerHandler; |
| export * from "./localStorageHandler"; | ||
| export * from "./serverHandler"; |
| import type { StorageHandler } from "../interfaces"; | ||
| export declare class LocalStorageHandler<T> implements StorageHandler<T> { | ||
| private readonly storage; | ||
| private readonly baseStorageKey; | ||
| constructor(storage: Storage, baseStorageKey: string); | ||
| storeData(key: string, data: Partial<T> | T | Record<string, never>): Promise<void>; | ||
| retrieveData(key: string): Promise<T | undefined>; | ||
| clearStorage(key: string): void; | ||
| clearOrphanedData(baseKey?: string): void; | ||
| private getStorageKey; | ||
| } |
| import type { ApiRequestParams, StorageHandler, StorageHandlerRetrieveOptions, StorageHandlerStoreOptions } from "../interfaces"; | ||
| type RequestFn = <T>(params: ApiRequestParams) => Promise<T>; | ||
| export declare class ServerHandler<T> implements StorageHandler<T> { | ||
| private readonly sessionServerBaseUrl; | ||
| private readonly request; | ||
| constructor(sessionServerBaseUrl: string, request: RequestFn); | ||
| storeData(key: string, data: Partial<T> | T | Record<string, never>, options?: StorageHandlerStoreOptions): Promise<void>; | ||
| retrieveData(key: string, options?: StorageHandlerRetrieveOptions): Promise<T | undefined>; | ||
| clearStorage(_key: string): void; | ||
| clearOrphanedData(_baseKey?: string): void; | ||
| } | ||
| export {}; |
| import _defineProperty from '@babel/runtime/helpers/defineProperty'; | ||
| class LocalStorageHandler { | ||
| constructor(storage, baseStorageKey) { | ||
| _defineProperty(this, "storage", void 0); | ||
| _defineProperty(this, "baseStorageKey", void 0); | ||
| this.storage = storage; | ||
| this.baseStorageKey = baseStorageKey; | ||
| } | ||
| async storeData(key, data) { | ||
| const storageKey = this.getStorageKey(key); | ||
| this.storage.setItem(storageKey, JSON.stringify(data)); | ||
| } | ||
| async retrieveData(key) { | ||
| const storageKey = this.getStorageKey(key); | ||
| const localData = this.storage.getItem(storageKey); | ||
| if (!localData) return undefined; | ||
| try { | ||
| return JSON.parse(localData); | ||
| } catch { | ||
| // Corrupted cache should not block retrieval. | ||
| this.storage.removeItem(storageKey); | ||
| return undefined; | ||
| } | ||
| } | ||
| clearStorage(key) { | ||
| this.storage.removeItem(this.getStorageKey(key)); | ||
| } | ||
| clearOrphanedData(baseKey = "") { | ||
| const keyPrefix = `${this.baseStorageKey}${baseKey}`; | ||
| const keysToRemove = []; | ||
| for (let index = 0; index < this.storage.length; index += 1) { | ||
| const storageKey = this.storage.key(index); | ||
| if (storageKey && storageKey.startsWith(keyPrefix)) { | ||
| keysToRemove.push(storageKey); | ||
| } | ||
| } | ||
| keysToRemove.forEach(key => { | ||
| this.storage.removeItem(key); | ||
| }); | ||
| } | ||
| getStorageKey(key) { | ||
| return `${this.baseStorageKey}${key}`; | ||
| } | ||
| } | ||
| export { LocalStorageHandler }; |
| import _defineProperty from '@babel/runtime/helpers/defineProperty'; | ||
| import { getPublic, sign } from '@toruslabs/eccrypto'; | ||
| import { hexToBytes, bytesToHex, encryptData, keccak256, utf8ToBytes, decryptData } from '@toruslabs/metadata-helpers'; | ||
| class ServerHandler { | ||
| constructor(sessionServerBaseUrl, request) { | ||
| _defineProperty(this, "sessionServerBaseUrl", void 0); | ||
| _defineProperty(this, "request", void 0); | ||
| this.sessionServerBaseUrl = sessionServerBaseUrl; | ||
| this.request = request; | ||
| } | ||
| async storeData(key, data, options = {}) { | ||
| var _options$operation; | ||
| const privKey = hexToBytes(key); | ||
| const pubKey = bytesToHex(getPublic(privKey)); | ||
| const encData = await encryptData(key, data); | ||
| const signature = bytesToHex(await sign(privKey, keccak256(utf8ToBytes(encData)))); | ||
| const body = { | ||
| key: pubKey, | ||
| data: encData, | ||
| signature, | ||
| namespace: options.namespace, | ||
| allowedOrigin: options.allowedOrigin | ||
| }; | ||
| const operation = (_options$operation = options.operation) !== null && _options$operation !== void 0 ? _options$operation : "create"; | ||
| if (operation === "update") { | ||
| await this.request({ | ||
| method: "PUT", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/update`, | ||
| data: body, | ||
| headers: options.headers | ||
| }); | ||
| return; | ||
| } | ||
| if (operation === "create") { | ||
| body.timeout = options.timeout; | ||
| } else if (operation === "invalidate") { | ||
| body.timeout = 1; | ||
| } | ||
| await this.request({ | ||
| method: "POST", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/set`, | ||
| data: body, | ||
| headers: options.headers | ||
| }); | ||
| } | ||
| async retrieveData(key, options = {}) { | ||
| const pubKey = bytesToHex(getPublic(hexToBytes(key))); | ||
| const body = { | ||
| key: pubKey, | ||
| namespace: options.namespace | ||
| }; | ||
| const result = await this.request({ | ||
| method: "POST", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/get`, | ||
| data: body, | ||
| headers: options.headers | ||
| }); | ||
| if (!result.message) { | ||
| throw new Error("Session Expired or Invalid public key"); | ||
| } | ||
| const response = await decryptData(key, result.message); | ||
| if (response.error) { | ||
| throw new Error("There was an error decrypting data."); | ||
| } | ||
| return response; | ||
| } | ||
| clearStorage(_key) { | ||
| // No-op. Server entries expire based on timeout. | ||
| } | ||
| clearOrphanedData(_baseKey = "") { | ||
| // No-op. Server entries expire based on timeout. | ||
| } | ||
| } | ||
| export { ServerHandler }; |
| 'use strict'; | ||
| var base = require('./base.js'); | ||
| var localStorageHandler = require('./handlers/localStorageHandler.js'); | ||
| var serverHandler = require('./handlers/serverHandler.js'); | ||
| var sessionManager = require('./sessionManager.js'); | ||
| var util = require('./util.js'); | ||
@@ -9,2 +12,7 @@ | ||
| exports.BaseSessionManager = base.BaseSessionManager; | ||
| exports.LocalStorageHandler = localStorageHandler.LocalStorageHandler; | ||
| exports.ServerHandler = serverHandler.ServerHandler; | ||
| exports.SessionManager = sessionManager.SessionManager; | ||
| exports.STORAGE_METHOD = util.STORAGE_METHOD; | ||
| exports.padHexString = util.padHexString; | ||
| exports.storageAvailable = util.storageAvailable; |
@@ -8,2 +8,4 @@ 'use strict'; | ||
| var base = require('./base.js'); | ||
| var localStorageHandler = require('./handlers/localStorageHandler.js'); | ||
| var serverHandler = require('./handlers/serverHandler.js'); | ||
| var util = require('./util.js'); | ||
@@ -18,3 +20,4 @@ | ||
| sessionId, | ||
| allowedOrigin | ||
| allowedOrigin, | ||
| useLocalStorage | ||
| } = {}) { | ||
@@ -27,2 +30,5 @@ super(); | ||
| _defineProperty(this, "sessionId", ""); | ||
| _defineProperty(this, "serverHandler", void 0); | ||
| _defineProperty(this, "useLocalStorage", void 0); | ||
| _defineProperty(this, "localStorageHandler", void 0); | ||
| if (sessionServerBaseUrl) { | ||
@@ -39,26 +45,25 @@ this.sessionServerBaseUrl = sessionServerBaseUrl; | ||
| } | ||
| this.useLocalStorage = Boolean(useLocalStorage); | ||
| this.serverHandler = new serverHandler.ServerHandler(this.sessionServerBaseUrl, params => super.request(params)); | ||
| } | ||
| get baseLocalStorageKey() { | ||
| var _this$sessionNamespac; | ||
| return `${(_this$sessionNamespac = this.sessionNamespace) !== null && _this$sessionNamespac !== void 0 ? _this$sessionNamespac : "w3a_session_manager_default"}:`; | ||
| } | ||
| static generateRandomSessionKey() { | ||
| return util.padHexString(metadataHelpers.bytesToHex(eccrypto.generatePrivate())); | ||
| } | ||
| setSessionId(sessionId) { | ||
| this.sessionId = util.padHexString(sessionId); | ||
| } | ||
| async createSession(data, headers = {}) { | ||
| super.checkSessionParams(); | ||
| const privKey = metadataHelpers.hexToBytes(this.sessionId); | ||
| const pubKey = metadataHelpers.bytesToHex(eccrypto.getPublic(privKey)); | ||
| const encData = await metadataHelpers.encryptData(this.sessionId, data); | ||
| const signature = metadataHelpers.bytesToHex(await eccrypto.sign(privKey, metadataHelpers.keccak256(metadataHelpers.utf8ToBytes(encData)))); | ||
| const body = { | ||
| key: pubKey, | ||
| data: encData, | ||
| signature, | ||
| await this.serverHandler.storeData(this.sessionId, data, { | ||
| operation: "create", | ||
| namespace: this.sessionNamespace, | ||
| timeout: this.sessionTime, | ||
| allowedOrigin: this.allowedOrigin | ||
| }; | ||
| await super.request({ | ||
| method: "POST", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/set`, | ||
| data: body, | ||
| allowedOrigin: this.allowedOrigin, | ||
| headers | ||
| }); | ||
| await this.safeLocalStorageOp(h => h.storeData(this.sessionId, data)); | ||
| return this.sessionId; | ||
@@ -72,20 +77,14 @@ } | ||
| super.checkSessionParams(); | ||
| const pubkey = metadataHelpers.bytesToHex(eccrypto.getPublic(metadataHelpers.hexToBytes(this.sessionId))); | ||
| const body = { | ||
| key: pubkey, | ||
| namespace: this.sessionNamespace | ||
| }; | ||
| const result = await super.request({ | ||
| method: "POST", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/get`, | ||
| data: body, | ||
| const localData = await this.safeLocalStorageOp(h => h.retrieveData(this.sessionId)); | ||
| if (localData !== undefined) { | ||
| return localData; | ||
| } | ||
| const response = await this.serverHandler.retrieveData(this.sessionId, { | ||
| namespace: this.sessionNamespace, | ||
| headers | ||
| }); | ||
| if (!result.message) { | ||
| if (response === undefined) { | ||
| throw new Error("Session Expired or Invalid public key"); | ||
| } | ||
| const response = await metadataHelpers.decryptData(this.sessionId, result.message); | ||
| if (response.error) { | ||
| throw new Error("There was an error decrypting data."); | ||
| } | ||
| await this.safeLocalStorageOp(h => h.storeData(this.sessionId, response)); | ||
| return response; | ||
@@ -95,44 +94,55 @@ } | ||
| super.checkSessionParams(); | ||
| const privKey = metadataHelpers.hexToBytes(this.sessionId); | ||
| const pubKey = metadataHelpers.bytesToHex(eccrypto.getPublic(privKey)); | ||
| const encData = await metadataHelpers.encryptData(this.sessionId, data); | ||
| const signature = metadataHelpers.bytesToHex(await eccrypto.sign(privKey, metadataHelpers.keccak256(metadataHelpers.utf8ToBytes(encData)))); | ||
| const body = { | ||
| key: pubKey, | ||
| data: encData, | ||
| signature, | ||
| await this.serverHandler.storeData(this.sessionId, data, { | ||
| operation: "update", | ||
| namespace: this.sessionNamespace, | ||
| allowedOrigin: this.allowedOrigin | ||
| }; | ||
| await super.request({ | ||
| method: "PUT", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/update`, | ||
| data: body, | ||
| allowedOrigin: this.allowedOrigin, | ||
| headers | ||
| }); | ||
| await this.safeLocalStorageOp(h => h.storeData(this.sessionId, data)); | ||
| } | ||
| async invalidateSession(headers = {}) { | ||
| super.checkSessionParams(); | ||
| const privKey = metadataHelpers.hexToBytes(this.sessionId); | ||
| const pubKey = metadataHelpers.bytesToHex(eccrypto.getPublic(privKey)); | ||
| const encData = await metadataHelpers.encryptData(this.sessionId, {}); | ||
| const signature = metadataHelpers.bytesToHex(await eccrypto.sign(privKey, metadataHelpers.keccak256(metadataHelpers.utf8ToBytes(encData)))); | ||
| const data = { | ||
| key: pubKey, | ||
| data: encData, | ||
| signature, | ||
| await this.serverHandler.storeData(this.sessionId, {}, { | ||
| operation: "invalidate", | ||
| namespace: this.sessionNamespace, | ||
| timeout: 1 | ||
| }; | ||
| await super.request({ | ||
| method: "POST", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/set`, | ||
| data, | ||
| headers | ||
| }); | ||
| this.clearStorage(); | ||
| this.sessionId = ""; | ||
| return true; | ||
| } | ||
| clearStorage() { | ||
| super.checkSessionParams(); | ||
| const localStorageHandler = this.getLocalStorageHandler(); | ||
| if (!localStorageHandler) return; | ||
| localStorageHandler.clearStorage(this.sessionId); | ||
| } | ||
| clearOrphanedData(baseKey) { | ||
| const localStorageHandler = this.getLocalStorageHandler(); | ||
| if (!localStorageHandler) return; | ||
| localStorageHandler.clearOrphanedData(baseKey); | ||
| } | ||
| /** | ||
| * Runs a local-storage operation, swallowing any errors so that failures | ||
| * like QuotaExceededError never break the primary server-backed flow. | ||
| */ | ||
| async safeLocalStorageOp(fn) { | ||
| try { | ||
| const handler = this.getLocalStorageHandler(); | ||
| if (!handler) return undefined; | ||
| return await fn(handler); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
| getLocalStorageHandler() { | ||
| if (!this.useLocalStorage) return undefined; | ||
| if (typeof window === "undefined") return undefined; | ||
| if (this.localStorageHandler) return this.localStorageHandler; | ||
| if (!util.storageAvailable(util.STORAGE_METHOD.LOCAL_STORAGE)) return undefined; | ||
| this.localStorageHandler = new localStorageHandler.LocalStorageHandler(window[util.STORAGE_METHOD.LOCAL_STORAGE], this.baseLocalStorageKey); | ||
| return this.localStorageHandler; | ||
| } | ||
| } | ||
| exports.SessionManager = SessionManager; |
| export * from "./base"; | ||
| export * from "./handlers"; | ||
| export * from "./interfaces"; | ||
| export * from "./sessionManager"; | ||
| export * from "./util"; |
@@ -18,2 +18,20 @@ export type IRequestBody = Record<string, unknown>; | ||
| }; | ||
| export type StorageOperation = "create" | "update" | "invalidate"; | ||
| export interface StorageHandlerStoreOptions { | ||
| headers?: RequestInit["headers"]; | ||
| namespace?: string; | ||
| timeout?: number; | ||
| allowedOrigin?: string | boolean; | ||
| operation?: StorageOperation; | ||
| } | ||
| export interface StorageHandlerRetrieveOptions { | ||
| headers?: RequestInit["headers"]; | ||
| namespace?: string; | ||
| } | ||
| export interface StorageHandler<T> { | ||
| storeData(key: string, data: Partial<T> | T | Record<string, never>, options?: StorageHandlerStoreOptions): Promise<void>; | ||
| retrieveData(key: string, options?: StorageHandlerRetrieveOptions): Promise<T | undefined>; | ||
| clearStorage(key: string): void | Promise<void>; | ||
| clearOrphanedData(baseKey?: string): void | Promise<void>; | ||
| } | ||
| export interface SessionManagerOptions { | ||
@@ -25,2 +43,3 @@ sessionServerBaseUrl?: string; | ||
| allowedOrigin?: string | boolean; | ||
| useLocalStorage?: boolean; | ||
| } | ||
@@ -27,0 +46,0 @@ export interface SessionRequestBody extends IRequestBody { |
@@ -9,4 +9,9 @@ import { BaseSessionManager } from "./base"; | ||
| sessionId: string; | ||
| constructor({ sessionServerBaseUrl, sessionNamespace, sessionTime, sessionId, allowedOrigin }?: SessionManagerOptions); | ||
| private serverHandler; | ||
| private readonly useLocalStorage; | ||
| private localStorageHandler?; | ||
| constructor({ sessionServerBaseUrl, sessionNamespace, sessionTime, sessionId, allowedOrigin, useLocalStorage }?: SessionManagerOptions); | ||
| private get baseLocalStorageKey(); | ||
| static generateRandomSessionKey(): string; | ||
| setSessionId(sessionId: string): void; | ||
| createSession(data: T, headers?: RequestInit["headers"]): Promise<string>; | ||
@@ -18,2 +23,10 @@ authorizeSession({ headers }?: { | ||
| invalidateSession(headers?: RequestInit["headers"]): Promise<boolean>; | ||
| clearStorage(): void; | ||
| clearOrphanedData(baseKey?: string): void; | ||
| /** | ||
| * Runs a local-storage operation, swallowing any errors so that failures | ||
| * like QuotaExceededError never break the primary server-backed flow. | ||
| */ | ||
| private safeLocalStorageOp; | ||
| private getLocalStorageHandler; | ||
| } |
| export declare const padHexString: (hexString: string) => string; | ||
| export declare const STORAGE_METHOD: { | ||
| readonly LOCAL_STORAGE: "localStorage"; | ||
| }; | ||
| export type StorageMethod = (typeof STORAGE_METHOD)[keyof typeof STORAGE_METHOD]; | ||
| export declare function storageAvailable(type: StorageMethod): boolean; |
+29
-0
@@ -6,3 +6,32 @@ 'use strict'; | ||
| }; | ||
| const STORAGE_METHOD = { | ||
| LOCAL_STORAGE: "localStorage" | ||
| }; | ||
| function storageAvailable(type) { | ||
| let storage; | ||
| try { | ||
| storage = window[type]; | ||
| const x = "__storage_test__"; | ||
| storage.setItem(x, x); | ||
| storage.removeItem(x); | ||
| return true; | ||
| } catch (error) { | ||
| const e = error; | ||
| return e && ( | ||
| // everything except Firefox | ||
| e.code === 22 || | ||
| // Firefox | ||
| e.code === 1014 || | ||
| // test name field too, because code might not be present | ||
| // everything except Firefox | ||
| e.name === "QuotaExceededError" || | ||
| // Firefox | ||
| e.name === "NS_ERROR_DOM_QUOTA_REACHED") && | ||
| // acknowledge QuotaExceededError only if there's something already stored | ||
| storage && storage.length !== 0; | ||
| } | ||
| } | ||
| exports.STORAGE_METHOD = STORAGE_METHOD; | ||
| exports.padHexString = padHexString; | ||
| exports.storageAvailable = storageAvailable; |
| export { BaseSessionManager } from './base.js'; | ||
| export { SessionManager } from './sessionManager.js'; | ||
| export { STORAGE_METHOD, padHexString, storageAvailable } from './util.js'; | ||
| export { LocalStorageHandler } from './handlers/localStorageHandler.js'; | ||
| export { ServerHandler } from './handlers/serverHandler.js'; |
| import _defineProperty from '@babel/runtime/helpers/defineProperty'; | ||
| import { SESSION_SERVER_API_URL } from '@toruslabs/constants'; | ||
| import { generatePrivate, getPublic, sign } from '@toruslabs/eccrypto'; | ||
| import { bytesToHex, hexToBytes, encryptData, keccak256, utf8ToBytes, decryptData } from '@toruslabs/metadata-helpers'; | ||
| import { generatePrivate } from '@toruslabs/eccrypto'; | ||
| import { bytesToHex } from '@toruslabs/metadata-helpers'; | ||
| import { BaseSessionManager } from './base.js'; | ||
| import { padHexString } from './util.js'; | ||
| import { LocalStorageHandler } from './handlers/localStorageHandler.js'; | ||
| import { ServerHandler } from './handlers/serverHandler.js'; | ||
| import { padHexString, storageAvailable, STORAGE_METHOD } from './util.js'; | ||
@@ -15,3 +17,4 @@ const DEFAULT_SESSION_TIMEOUT = 86400; | ||
| sessionId, | ||
| allowedOrigin | ||
| allowedOrigin, | ||
| useLocalStorage | ||
| } = {}) { | ||
@@ -24,2 +27,5 @@ super(); | ||
| _defineProperty(this, "sessionId", ""); | ||
| _defineProperty(this, "serverHandler", void 0); | ||
| _defineProperty(this, "useLocalStorage", void 0); | ||
| _defineProperty(this, "localStorageHandler", void 0); | ||
| if (sessionServerBaseUrl) { | ||
@@ -36,26 +42,25 @@ this.sessionServerBaseUrl = sessionServerBaseUrl; | ||
| } | ||
| this.useLocalStorage = Boolean(useLocalStorage); | ||
| this.serverHandler = new ServerHandler(this.sessionServerBaseUrl, params => super.request(params)); | ||
| } | ||
| get baseLocalStorageKey() { | ||
| var _this$sessionNamespac; | ||
| return `${(_this$sessionNamespac = this.sessionNamespace) !== null && _this$sessionNamespac !== void 0 ? _this$sessionNamespac : "w3a_session_manager_default"}:`; | ||
| } | ||
| static generateRandomSessionKey() { | ||
| return padHexString(bytesToHex(generatePrivate())); | ||
| } | ||
| setSessionId(sessionId) { | ||
| this.sessionId = padHexString(sessionId); | ||
| } | ||
| async createSession(data, headers = {}) { | ||
| super.checkSessionParams(); | ||
| const privKey = hexToBytes(this.sessionId); | ||
| const pubKey = bytesToHex(getPublic(privKey)); | ||
| const encData = await encryptData(this.sessionId, data); | ||
| const signature = bytesToHex(await sign(privKey, keccak256(utf8ToBytes(encData)))); | ||
| const body = { | ||
| key: pubKey, | ||
| data: encData, | ||
| signature, | ||
| await this.serverHandler.storeData(this.sessionId, data, { | ||
| operation: "create", | ||
| namespace: this.sessionNamespace, | ||
| timeout: this.sessionTime, | ||
| allowedOrigin: this.allowedOrigin | ||
| }; | ||
| await super.request({ | ||
| method: "POST", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/set`, | ||
| data: body, | ||
| allowedOrigin: this.allowedOrigin, | ||
| headers | ||
| }); | ||
| await this.safeLocalStorageOp(h => h.storeData(this.sessionId, data)); | ||
| return this.sessionId; | ||
@@ -69,20 +74,14 @@ } | ||
| super.checkSessionParams(); | ||
| const pubkey = bytesToHex(getPublic(hexToBytes(this.sessionId))); | ||
| const body = { | ||
| key: pubkey, | ||
| namespace: this.sessionNamespace | ||
| }; | ||
| const result = await super.request({ | ||
| method: "POST", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/get`, | ||
| data: body, | ||
| const localData = await this.safeLocalStorageOp(h => h.retrieveData(this.sessionId)); | ||
| if (localData !== undefined) { | ||
| return localData; | ||
| } | ||
| const response = await this.serverHandler.retrieveData(this.sessionId, { | ||
| namespace: this.sessionNamespace, | ||
| headers | ||
| }); | ||
| if (!result.message) { | ||
| if (response === undefined) { | ||
| throw new Error("Session Expired or Invalid public key"); | ||
| } | ||
| const response = await decryptData(this.sessionId, result.message); | ||
| if (response.error) { | ||
| throw new Error("There was an error decrypting data."); | ||
| } | ||
| await this.safeLocalStorageOp(h => h.storeData(this.sessionId, response)); | ||
| return response; | ||
@@ -92,44 +91,56 @@ } | ||
| super.checkSessionParams(); | ||
| const privKey = hexToBytes(this.sessionId); | ||
| const pubKey = bytesToHex(getPublic(privKey)); | ||
| const encData = await encryptData(this.sessionId, data); | ||
| const signature = bytesToHex(await sign(privKey, keccak256(utf8ToBytes(encData)))); | ||
| const body = { | ||
| key: pubKey, | ||
| data: encData, | ||
| signature, | ||
| await this.serverHandler.storeData(this.sessionId, data, { | ||
| operation: "update", | ||
| namespace: this.sessionNamespace, | ||
| allowedOrigin: this.allowedOrigin | ||
| }; | ||
| await super.request({ | ||
| method: "PUT", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/update`, | ||
| data: body, | ||
| allowedOrigin: this.allowedOrigin, | ||
| headers | ||
| }); | ||
| await this.safeLocalStorageOp(h => h.storeData(this.sessionId, data)); | ||
| } | ||
| async invalidateSession(headers = {}) { | ||
| super.checkSessionParams(); | ||
| const privKey = hexToBytes(this.sessionId); | ||
| const pubKey = bytesToHex(getPublic(privKey)); | ||
| const encData = await encryptData(this.sessionId, {}); | ||
| const signature = bytesToHex(await sign(privKey, keccak256(utf8ToBytes(encData)))); | ||
| const data = { | ||
| key: pubKey, | ||
| data: encData, | ||
| signature, | ||
| await this.serverHandler.storeData(this.sessionId, {}, { | ||
| operation: "invalidate", | ||
| namespace: this.sessionNamespace, | ||
| timeout: 1 | ||
| }; | ||
| await super.request({ | ||
| method: "POST", | ||
| url: `${this.sessionServerBaseUrl}/v2/store/set`, | ||
| data, | ||
| headers | ||
| }); | ||
| this.clearStorage(); | ||
| this.sessionId = ""; | ||
| return true; | ||
| } | ||
| clearStorage() { | ||
| super.checkSessionParams(); | ||
| const localStorageHandler = this.getLocalStorageHandler(); | ||
| if (!localStorageHandler) return; | ||
| localStorageHandler.clearStorage(this.sessionId); | ||
| } | ||
| clearOrphanedData(baseKey) { | ||
| const localStorageHandler = this.getLocalStorageHandler(); | ||
| if (!localStorageHandler) return; | ||
| localStorageHandler.clearOrphanedData(baseKey); | ||
| } | ||
| /** | ||
| * Runs a local-storage operation, swallowing any errors so that failures | ||
| * like QuotaExceededError never break the primary server-backed flow. | ||
| */ | ||
| async safeLocalStorageOp(fn) { | ||
| try { | ||
| const handler = this.getLocalStorageHandler(); | ||
| if (!handler) return undefined; | ||
| return await fn(handler); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
| getLocalStorageHandler() { | ||
| if (!this.useLocalStorage) return undefined; | ||
| if (typeof window === "undefined") return undefined; | ||
| if (this.localStorageHandler) return this.localStorageHandler; | ||
| if (!storageAvailable(STORAGE_METHOD.LOCAL_STORAGE)) return undefined; | ||
| this.localStorageHandler = new LocalStorageHandler(window[STORAGE_METHOD.LOCAL_STORAGE], this.baseLocalStorageKey); | ||
| return this.localStorageHandler; | ||
| } | ||
| } | ||
| export { SessionManager }; |
+28
-1
| const padHexString = hexString => { | ||
| return hexString.padStart(64, "0").slice(0, 64); | ||
| }; | ||
| const STORAGE_METHOD = { | ||
| LOCAL_STORAGE: "localStorage" | ||
| }; | ||
| function storageAvailable(type) { | ||
| let storage; | ||
| try { | ||
| storage = window[type]; | ||
| const x = "__storage_test__"; | ||
| storage.setItem(x, x); | ||
| storage.removeItem(x); | ||
| return true; | ||
| } catch (error) { | ||
| const e = error; | ||
| return e && ( | ||
| // everything except Firefox | ||
| e.code === 22 || | ||
| // Firefox | ||
| e.code === 1014 || | ||
| // test name field too, because code might not be present | ||
| // everything except Firefox | ||
| e.name === "QuotaExceededError" || | ||
| // Firefox | ||
| e.name === "NS_ERROR_DOM_QUOTA_REACHED") && | ||
| // acknowledge QuotaExceededError only if there's something already stored | ||
| storage && storage.length !== 0; | ||
| } | ||
| } | ||
| export { padHexString }; | ||
| export { STORAGE_METHOD, padHexString, storageAvailable }; |
+1
-1
| { | ||
| "name": "@toruslabs/session-manager", | ||
| "version": "5.0.0", | ||
| "version": "5.1.0", | ||
| "description": "session manager web", | ||
@@ -5,0 +5,0 @@ "sideEffects": false, |
| import { SESSION_SERVER_API_URL } from "@toruslabs/constants"; | ||
| import { beforeEach, describe, expect, test } from "vitest"; | ||
| import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; | ||
| import { BaseSessionManager } from "../src/base"; | ||
| import { SessionManager } from "../src/sessionManager"; | ||
@@ -10,5 +11,3 @@ | ||
| const sessionManager = new SessionManager({}); | ||
| await expect(async () => { | ||
| await sessionManager.createSession({}); | ||
| }).rejects.toThrow("Session id is required"); | ||
| await expect(sessionManager.createSession({})).rejects.toThrow("Session id is required"); | ||
| }); | ||
@@ -237,5 +236,3 @@ test("should use default base url if not set", () => { | ||
| await sessionManagerWithTrueOrigin.createSession({ testData: "test" }, { origin: "https://example.com" }); | ||
| await expect(async () => { | ||
| await sessionManagerWithTrueOrigin.authorizeSession(); | ||
| }).rejects.toThrow(); | ||
| await expect(sessionManagerWithTrueOrigin.authorizeSession()).rejects.toThrow(); | ||
| }); | ||
@@ -274,2 +271,159 @@ test("should return session data if allowedOrigin: FALSE, origin: empty", async () => { | ||
| }); | ||
| describe("Local Storage Behavior", () => { | ||
| const originalWindow = globalThis.window; | ||
| class MemoryStorage implements Storage { | ||
| private store = new Map<string, string>(); | ||
| get length() { | ||
| return this.store.size; | ||
| } | ||
| clear(): void { | ||
| this.store.forEach((_value, key) => { | ||
| delete (this as Record<string, unknown>)[key]; | ||
| }); | ||
| this.store.clear(); | ||
| } | ||
| getItem(key: string): string | null { | ||
| return this.store.has(key) ? this.store.get(key)! : null; | ||
| } | ||
| key(index: number): string | null { | ||
| return Array.from(this.store.keys())[index] ?? null; | ||
| } | ||
| removeItem(key: string): void { | ||
| this.store.delete(key); | ||
| delete (this as Record<string, unknown>)[key]; | ||
| } | ||
| setItem(key: string, value: string): void { | ||
| this.store.set(key, value); | ||
| Object.defineProperty(this, key, { | ||
| value, | ||
| configurable: true, | ||
| enumerable: true, | ||
| writable: true, | ||
| }); | ||
| } | ||
| } | ||
| let storage: Storage; | ||
| let sessionId: string; | ||
| const sessionNamespace = "test-local"; | ||
| beforeEach(() => { | ||
| storage = new MemoryStorage(); | ||
| // Node test environment does not provide localStorage by default. | ||
| Object.defineProperty(globalThis, "window", { | ||
| value: { localStorage: storage }, | ||
| configurable: true, | ||
| }); | ||
| sessionId = SessionManager.generateRandomSessionKey(); | ||
| }); | ||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| Object.defineProperty(globalThis, "window", { | ||
| value: originalWindow, | ||
| configurable: true, | ||
| }); | ||
| }); | ||
| test("should write to local storage while creating session", async () => { | ||
| const sessionManager = new SessionManager<{ testData: string }>({ | ||
| sessionId, | ||
| sessionNamespace, | ||
| useLocalStorage: true, | ||
| }); | ||
| vi.spyOn(BaseSessionManager.prototype as unknown as { request: (...args: unknown[]) => Promise<unknown> }, "request").mockResolvedValue({}); | ||
| await sessionManager.createSession({ testData: "test" }); | ||
| expect(storage.getItem(`${sessionNamespace}:${sessionId}`)).toBe(JSON.stringify({ testData: "test" })); | ||
| }); | ||
| test("should read from local storage before calling server", async () => { | ||
| const sessionManager = new SessionManager<{ testData: string }>({ | ||
| sessionId, | ||
| sessionNamespace, | ||
| useLocalStorage: true, | ||
| }); | ||
| const requestSpy = vi.spyOn(BaseSessionManager.prototype as unknown as { request: (...args: unknown[]) => Promise<unknown> }, "request"); | ||
| storage.setItem(`${sessionNamespace}:${sessionId}`, JSON.stringify({ testData: "test" })); | ||
| const result = await sessionManager.authorizeSession(); | ||
| expect(result).toEqual({ testData: "test" }); | ||
| expect(requestSpy).not.toHaveBeenCalled(); | ||
| }); | ||
| test("should ignore corrupted local cache while updating session", async () => { | ||
| const sessionManager = new SessionManager<{ testData: string; count: number }>({ | ||
| sessionId, | ||
| sessionNamespace, | ||
| useLocalStorage: true, | ||
| }); | ||
| const requestSpy = vi | ||
| .spyOn(BaseSessionManager.prototype as unknown as { request: (...args: unknown[]) => Promise<unknown> }, "request") | ||
| .mockResolvedValueOnce({}); | ||
| storage.setItem(`${sessionNamespace}:${sessionId}`, "not-json"); | ||
| await expect(sessionManager.updateSession({ testData: "new" })).resolves.toBeUndefined(); | ||
| expect(requestSpy).toHaveBeenCalledTimes(1); | ||
| expect(storage.getItem(`${sessionNamespace}:${sessionId}`)).toBe(JSON.stringify({ testData: "new" })); | ||
| }); | ||
| test("should not write local storage when createSession request fails", async () => { | ||
| const sessionManager = new SessionManager<{ testData: string }>({ | ||
| sessionId, | ||
| sessionNamespace, | ||
| useLocalStorage: true, | ||
| }); | ||
| vi.spyOn(BaseSessionManager.prototype as unknown as { request: (...args: unknown[]) => Promise<unknown> }, "request").mockRejectedValueOnce( | ||
| new Error("request failed") | ||
| ); | ||
| await expect(sessionManager.createSession({ testData: "test" })).rejects.toThrow("request failed"); | ||
| expect(storage.getItem(`${sessionNamespace}:${sessionId}`)).toBeNull(); | ||
| }); | ||
| test("should not update local storage when updateSession request fails", async () => { | ||
| const sessionManager = new SessionManager<{ testData: string; count: number }>({ | ||
| sessionId, | ||
| sessionNamespace, | ||
| useLocalStorage: true, | ||
| }); | ||
| storage.setItem(`${sessionNamespace}:${sessionId}`, JSON.stringify({ testData: "existing", count: 1 })); | ||
| vi.spyOn(BaseSessionManager.prototype as unknown as { request: (...args: unknown[]) => Promise<unknown> }, "request").mockRejectedValueOnce( | ||
| new Error("request failed") | ||
| ); | ||
| await expect(sessionManager.updateSession({ testData: "new" })).rejects.toThrow("request failed"); | ||
| expect(storage.getItem(`${sessionNamespace}:${sessionId}`)).toBe(JSON.stringify({ testData: "existing", count: 1 })); | ||
| }); | ||
| test("should clear current and orphaned local storage entries", () => { | ||
| const sessionManager = new SessionManager<{ testData: string }>({ | ||
| sessionId, | ||
| sessionNamespace, | ||
| useLocalStorage: true, | ||
| }); | ||
| storage.setItem(`${sessionNamespace}:${sessionId}`, JSON.stringify({ testData: "test" })); | ||
| storage.setItem(`${sessionNamespace}:orphaned`, JSON.stringify({ testData: "test2" })); | ||
| storage.setItem("another-namespace:keep", JSON.stringify({ testData: "keep" })); | ||
| sessionManager.clearStorage(); | ||
| sessionManager.clearOrphanedData(); | ||
| expect(storage.getItem(`${sessionNamespace}:${sessionId}`)).toBeNull(); | ||
| expect(storage.getItem(`${sessionNamespace}:orphaned`)).toBeNull(); | ||
| expect(storage.getItem("another-namespace:keep")).toBe(JSON.stringify({ testData: "keep" })); | ||
| }); | ||
| }); | ||
| }); |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
55967
63.93%36
28.57%1271
68.57%3
50%