UNPKG

@bsv/wallet-toolbox-client

Version:
660 lines 27.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.WalletStorageManager = void 0; const entities_1 = require("../storage/schema/entities"); const sdk = __importStar(require("../sdk")); class ManagedStorage { constructor(storage) { this.storage = storage; this.isStorageProvider = storage.isStorageProvider(); this.isAvailable = false; } } /** * The `WalletStorageManager` class delivers authentication checking storage access to the wallet. * * If manages multiple `StorageBase` derived storage services: one actice, the rest as backups. * * Of the storage services, one is 'active' at any one time. * On startup, and whenever triggered by the wallet, `WalletStorageManager` runs a syncrhonization sequence: * * 1. While synchronizing, all other access to storage is blocked waiting. * 2. The active service is confirmed, potentially triggering a resolution process if there is disagreement. * 3. Changes are pushed from the active storage service to each inactive, backup service. * * Some storage services do not support multiple writers. `WalletStorageManager` manages wait-blocking write requests * for these services. */ class WalletStorageManager { /** * Creates a new WalletStorageManager with the given identityKey and optional active and backup storage providers. * * @param identityKey The identity key of the user for whom this wallet is being managed. * @param active An optional active storage provider. If not provided, no active storage will be set. * @param backups An optional array of backup storage providers. If not provided, no backups will be set. */ constructor(identityKey, active, backups) { /** * All configured stores including current active, backups, and conflicting actives. */ this._stores = []; /** * True if makeAvailable has been run and access to managed stores (active) is allowed */ this._isAvailable = false; this.readerLocks = []; this.writerLocks = []; this.syncLocks = []; this.spLocks = []; const stores = [...(backups || [])]; if (active) stores.unshift(active); this._stores = stores.map(s => new ManagedStorage(s)); this._authId = { identityKey }; } isStorageProvider() { return false; } isAvailable() { return this._isAvailable; } /** * The active storage is "enabled" only if its `storageIdentityKey` matches the user's currently selected `activeStorage`, * and only if there are no stores with conflicting `activeStorage` selections. * * A wallet may be created without including the user's currently selected active storage. This allows readonly access to their wallet data. * * In addition, if there are conflicting `activeStorage` selections among backup storage providers then the active remains disabled. */ get isActiveEnabled() { return (this._active !== undefined && this._active.settings.storageIdentityKey === this._active.user.activeStorage && this._conflictingActives !== undefined && this._conflictingActives.length === 0); } /** * @returns true if at least one WalletStorageProvider has been added. */ canMakeAvailable() { return this._stores.length > 0; } /** * This async function must be called after construction and before * any other async function can proceed. * * Runs through `_stores` validating all properties and partitioning across `_active`, `_backups`, `_conflictingActives`. * * @throws WERR_INVALID_PARAMETER if canMakeAvailable returns false. * * @returns {TableSettings} from the active storage. */ async makeAvailable() { var _a; if (this._isAvailable) return this._active.settings; this._active = undefined; this._backups = []; this._conflictingActives = []; if (this._stores.length < 1) throw new sdk.WERR_INVALID_PARAMETER('active', 'valid. Must add active storage provider to wallet.'); // Initial backups. conflictingActives will be removed. const backups = []; let i = -1; for (const store of this._stores) { i++; if (!store.isAvailable || !store.settings || !store.user) { // Validate all ManagedStorage properties. store.settings = await store.storage.makeAvailable(); const r = await store.storage.findOrInsertUser(this._authId.identityKey); store.user = r.user; store.isAvailable = true; } if (!this._active) // _stores[0] becomes the default active store. It may be replaced if it is not the user's "enabled" activeStorage and that store is found among the remainder (backups). this._active = store; else { const ua = store.user.activeStorage; const si = store.settings.storageIdentityKey; if (ua === si && !this.isActiveEnabled) { // This store's user record selects it as an enabled active storage... // swap the current not-enabled active for this storeage. backups.push(this._active); this._active = store; } else { // This store is a backup: Its user record selects some other storage as active. backups.push(store); } } } // Review backups, partition out conflicting actives. const si = (_a = this._active.settings) === null || _a === void 0 ? void 0 : _a.storageIdentityKey; for (const store of backups) { if (store.user.activeStorage !== si) this._conflictingActives.push(store); else this._backups.push(store); } this._isAvailable = true; this._authId.userId = this._active.user.userId; this._authId.isActive = this.isActiveEnabled; return this._active.settings; } verifyActive() { if (!this._active || !this._isAvailable) throw new sdk.WERR_INVALID_OPERATION('An active WalletStorageProvider must be added to this WalletStorageManager and makeAvailable must be called.'); return this._active; } async getAuth(mustBeActive) { if (!this.isAvailable()) await this.makeAvailable(); if (mustBeActive && !this._authId.isActive) throw new sdk.WERR_NOT_ACTIVE(); return this._authId; } async getUserId() { return (await this.getAuth()).userId; } getActive() { return this.verifyActive().storage; } getActiveSettings() { return this.verifyActive().settings; } getActiveUser() { return this.verifyActive().user; } getActiveStore() { return this.verifyActive().settings.storageIdentityKey; } getActiveStoreName() { return this.verifyActive().settings.storageName; } getBackupStores() { this.verifyActive(); return this._backups.map(b => b.settings.storageIdentityKey); } getConflictingStores() { this.verifyActive(); return this._conflictingActives.map(b => b.settings.storageIdentityKey); } getAllStores() { this.verifyActive(); return this._stores.map(b => b.settings.storageIdentityKey); } async getActiveLock(lockQueue) { if (!this.isAvailable()) await this.makeAvailable(); let resolveNewLock = () => { }; const newLock = new Promise(resolve => { resolveNewLock = resolve; lockQueue.push(resolve); }); if (lockQueue.length === 1) { resolveNewLock(); } await newLock; } releaseActiveLock(queue) { queue.shift(); // Remove the current lock from the queue if (queue.length > 0) { queue[0](); } } async getActiveForReader() { await this.getActiveLock(this.readerLocks); return this.getActive(); } releaseActiveForReader() { this.releaseActiveLock(this.readerLocks); } async getActiveForWriter() { await this.getActiveLock(this.readerLocks); await this.getActiveLock(this.writerLocks); return this.getActive(); } releaseActiveForWriter() { this.releaseActiveLock(this.writerLocks); this.releaseActiveLock(this.readerLocks); } async getActiveForSync() { await this.getActiveLock(this.readerLocks); await this.getActiveLock(this.writerLocks); await this.getActiveLock(this.syncLocks); return this.getActive(); } releaseActiveForSync() { this.releaseActiveLock(this.syncLocks); this.releaseActiveLock(this.writerLocks); this.releaseActiveLock(this.readerLocks); } async getActiveForStorageProvider() { await this.getActiveLock(this.readerLocks); await this.getActiveLock(this.writerLocks); await this.getActiveLock(this.syncLocks); await this.getActiveLock(this.spLocks); const active = this.getActive(); // We can finally confirm that active storage is still able to support `StorageProvider` if (!active.isStorageProvider()) throw new sdk.WERR_INVALID_OPERATION('Active "WalletStorageProvider" does not support "StorageProvider" interface.'); // Allow the sync to proceed on the active store. return active; } releaseActiveForStorageProvider() { this.releaseActiveLock(this.spLocks); this.releaseActiveLock(this.syncLocks); this.releaseActiveLock(this.writerLocks); this.releaseActiveLock(this.readerLocks); } async runAsWriter(writer) { try { const active = await this.getActiveForWriter(); const r = await writer(active); return r; } finally { this.releaseActiveForWriter(); } } async runAsReader(reader) { try { const active = await this.getActiveForReader(); const r = await reader(active); return r; } finally { this.releaseActiveForReader(); } } /** * * @param sync the function to run with sync access lock * @param activeSync from chained sync functions, active storage already held under sync access lock. * @returns */ async runAsSync(sync, activeSync) { try { const active = activeSync || (await this.getActiveForSync()); const r = await sync(active); return r; } finally { if (!activeSync) this.releaseActiveForSync(); } } async runAsStorageProvider(sync) { try { const active = await this.getActiveForStorageProvider(); const r = await sync(active); return r; } finally { this.releaseActiveForStorageProvider(); } } /** * * @returns true if the active `WalletStorageProvider` also implements `StorageProvider` */ isActiveStorageProvider() { return this.getActive().isStorageProvider(); } async addWalletStorageProvider(provider) { await provider.makeAvailable(); if (this._services) provider.setServices(this._services); this._stores.push(new ManagedStorage(provider)); this._isAvailable = false; await this.makeAvailable(); } setServices(v) { this._services = v; for (const store of this._stores) store.storage.setServices(v); } getServices() { if (!this._services) throw new sdk.WERR_INVALID_OPERATION('Must setServices first.'); return this._services; } getSettings() { return this.getActive().getSettings(); } async migrate(storageName, storageIdentityKey) { return await this.runAsWriter(async (writer) => { return writer.migrate(storageName, storageIdentityKey); }); } async destroy() { if (this._stores.length < 1) return; return await this.runAsWriter(async (writer) => { for (const store of this._stores) await store.storage.destroy(); }); } async findOrInsertUser(identityKey) { const auth = await this.getAuth(); if (identityKey != auth.identityKey) throw new sdk.WERR_UNAUTHORIZED(); return await this.runAsWriter(async (writer) => { const r = await writer.findOrInsertUser(identityKey); if (auth.userId && auth.userId !== r.user.userId) throw new sdk.WERR_INTERNAL('userId may not change for given identityKey'); this._authId.userId = r.user.userId; return r; }); } async abortAction(args) { sdk.validateAbortActionArgs(args); return await this.runAsWriter(async (writer) => { const auth = await this.getAuth(true); return await writer.abortAction(auth, args); }); } async createAction(vargs) { return await this.runAsWriter(async (writer) => { const auth = await this.getAuth(true); return await writer.createAction(auth, vargs); }); } async internalizeAction(args) { sdk.validateInternalizeActionArgs(args); return await this.runAsWriter(async (writer) => { const auth = await this.getAuth(true); return await writer.internalizeAction(auth, args); }); } async relinquishCertificate(args) { sdk.validateRelinquishCertificateArgs(args); return await this.runAsWriter(async (writer) => { const auth = await this.getAuth(true); return await writer.relinquishCertificate(auth, args); }); } async relinquishOutput(args) { sdk.validateRelinquishOutputArgs(args); return await this.runAsWriter(async (writer) => { const auth = await this.getAuth(true); return await writer.relinquishOutput(auth, args); }); } async processAction(args) { return await this.runAsWriter(async (writer) => { const auth = await this.getAuth(true); return await writer.processAction(auth, args); }); } async insertCertificate(certificate) { return await this.runAsWriter(async (writer) => { const auth = await this.getAuth(true); return await writer.insertCertificateAuth(auth, certificate); }); } async listActions(vargs) { const auth = await this.getAuth(); return await this.runAsReader(async (reader) => { return await reader.listActions(auth, vargs); }); } async listCertificates(args) { const auth = await this.getAuth(); return await this.runAsReader(async (reader) => { return await reader.listCertificates(auth, args); }); } async listOutputs(vargs) { const auth = await this.getAuth(); return await this.runAsReader(async (reader) => { return await reader.listOutputs(auth, vargs); }); } async findCertificates(args) { const auth = await this.getAuth(); return await this.runAsReader(async (reader) => { return await reader.findCertificatesAuth(auth, args); }); } async findOutputBaskets(args) { const auth = await this.getAuth(); return await this.runAsReader(async (reader) => { return await reader.findOutputBasketsAuth(auth, args); }); } async findOutputs(args) { const auth = await this.getAuth(); return await this.runAsReader(async (reader) => { return await reader.findOutputsAuth(auth, args); }); } async findProvenTxReqs(args) { return await this.runAsReader(async (reader) => { return await reader.findProvenTxReqs(args); }); } async syncFromReader(identityKey, reader, activeSync, log = '') { const auth = await this.getAuth(); if (identityKey !== auth.identityKey) throw new sdk.WERR_UNAUTHORIZED(); const readerSettings = await reader.makeAvailable(); let inserts = 0, updates = 0; log = await this.runAsSync(async (sync) => { const writer = sync; const writerSettings = this.getSettings(); log += `syncFromReader from ${readerSettings.storageName} to ${writerSettings.storageName}\n`; let i = -1; for (;;) { i++; const ss = await entities_1.EntitySyncState.fromStorage(writer, identityKey, readerSettings); const args = ss.makeRequestSyncChunkArgs(identityKey, writerSettings.storageIdentityKey); const chunk = await reader.getSyncChunk(args); if (chunk.user) { // Merging state from a reader cannot update activeStorage chunk.user.activeStorage = this._active.user.activeStorage; } const r = await writer.processSyncChunk(args, chunk); inserts += r.inserts; updates += r.updates; log += `chunk ${i} inserted ${r.inserts} updated ${r.updates} ${r.maxUpdated_at}\n`; if (r.done) break; } log += `syncFromReader complete: ${inserts} inserts, ${updates} updates\n`; return log; }, activeSync); return { inserts, updates, log }; } async syncToWriter(auth, writer, activeSync, log = '', progLog) { progLog || (progLog = s => s); const identityKey = auth.identityKey; const writerSettings = await writer.makeAvailable(); let inserts = 0, updates = 0; log = await this.runAsSync(async (sync) => { const reader = sync; const readerSettings = reader.getSettings(); log += progLog(`syncToWriter from ${readerSettings.storageName} to ${writerSettings.storageName}\n`); let i = -1; for (;;) { i++; const ss = await entities_1.EntitySyncState.fromStorage(writer, identityKey, readerSettings); const args = ss.makeRequestSyncChunkArgs(identityKey, writerSettings.storageIdentityKey); const chunk = await reader.getSyncChunk(args); log += entities_1.EntitySyncState.syncChunkSummary(chunk); const r = await writer.processSyncChunk(args, chunk); inserts += r.inserts; updates += r.updates; log += progLog(`chunk ${i} inserted ${r.inserts} updated ${r.updates} ${r.maxUpdated_at}\n`); if (r.done) break; } log += progLog(`syncToWriter complete: ${inserts} inserts, ${updates} updates\n`); return log; }, activeSync); return { inserts, updates, log }; } async updateBackups(activeSync, progLog) { progLog || (progLog = s => s); const auth = await this.getAuth(true); return await this.runAsSync(async (sync) => { let log = progLog(`BACKUP CURRENT ACTIVE TO ${this._backups.length} STORES\n`); for (const backup of this._backups) { const stwr = await this.syncToWriter(auth, backup.storage, sync, undefined, progLog); log += stwr.log; } return log; }, activeSync); } /** * Updates backups and switches to new active storage provider from among current backup providers. * * Also resolves conflicting actives. * * @param storageIdentityKey of current backup storage provider that is to become the new active provider. */ async setActive(storageIdentityKey, progLog) { progLog || (progLog = s => s); if (!this.isAvailable()) await this.makeAvailable(); // Confirm a valid storageIdentityKey: must match one of the _stores. const newActiveIndex = this._stores.findIndex(s => s.settings.storageIdentityKey === storageIdentityKey); if (newActiveIndex < 0) throw new sdk.WERR_INVALID_PARAMETER('storageIdentityKey', `registered with this "WalletStorageManager". ${storageIdentityKey} does not match any managed store.`); const identityKey = (await this.getAuth()).identityKey; const newActive = this._stores[newActiveIndex]; let log = progLog(`setActive to ${newActive.settings.storageName}`); if (storageIdentityKey === this.getActiveStore() && this.isActiveEnabled) /** Setting the current active as the new active is a permitted no-op. */ return log + progLog(` unchanged\n`); log += progLog('\n'); log += await this.runAsSync(async (sync) => { let log = ''; if (this._conflictingActives.length > 0) { // Merge state from conflicting actives into `newActive`. // Handle case where new active is current active to resolve conflicts. // And where new active is one of the current conflict actives. this._conflictingActives.push(this._active); // Remove the new active from conflicting actives and // set new active as the conflicting active that matches the target `storageIdentityKey` this._conflictingActives = this._conflictingActives.filter(ca => { const isNewActive = ca.settings.storageIdentityKey === storageIdentityKey; return !isNewActive; }); // Merge state from conflicting actives into `newActive`. for (const conflict of this._conflictingActives) { log += progLog('MERGING STATE FROM CONFLICTING ACTIVES:\n'); const sfr = await this.syncToWriter({ identityKey, userId: newActive.user.userId, isActive: false }, newActive.storage, conflict.storage, undefined, progLog); log += sfr.log; } log += progLog('PROPAGATE MERGED ACTIVE STATE TO NON-ACTIVES\n'); } else { log += progLog('BACKUP CURRENT ACTIVE STATE THEN SET NEW ACTIVE\n'); } // If there were conflicting actives, // Push state merged from all merged actives into newActive to all stores other than the now single active. // Otherwise, // Push state from current active to all other stores. const backupSource = this._conflictingActives.length > 0 ? newActive : this._active; // Update the backupSource's user record with the new activeStorage // which will propagate to all other stores in the following backup loop. await backupSource.storage.setActive({ identityKey, userId: backupSource.user.userId }, storageIdentityKey); for (const store of this._stores) { // Update cached user.activeStorage of all stores store.user.activeStorage = storageIdentityKey; if (store.settings.storageIdentityKey !== backupSource.settings.storageIdentityKey) { // If this store is not the backupSource store push state from backupSource to this store. const stwr = await this.syncToWriter({ identityKey, userId: store.user.userId, isActive: false }, store.storage, backupSource.storage, undefined, progLog); log += stwr.log; } } this._isAvailable = false; await this.makeAvailable(); return log; }); return log; } getStoreEndpointURL(store) { if (store.storage.constructor.name === 'StorageClient') return store.storage.endpointUrl; return undefined; } getStores() { const stores = []; if (this._active) { stores.push({ isActive: true, isEnabled: this.isActiveEnabled, isBackup: false, isConflicting: false, userId: this._active.user.userId, storageIdentityKey: this._active.settings.storageIdentityKey, storageName: this._active.settings.storageName, storageClass: this._active.storage.constructor.name, endpointURL: this.getStoreEndpointURL(this._active) }); } for (const store of this._conflictingActives || []) { stores.push({ isActive: true, isEnabled: false, isBackup: false, isConflicting: true, userId: store.user.userId, storageIdentityKey: store.settings.storageIdentityKey, storageName: store.settings.storageName, storageClass: store.storage.constructor.name, endpointURL: this.getStoreEndpointURL(store) }); } for (const store of this._backups || []) { stores.push({ isActive: false, isEnabled: false, isBackup: true, isConflicting: false, userId: store.user.userId, storageIdentityKey: store.settings.storageIdentityKey, storageName: store.settings.storageName, storageClass: store.storage.constructor.name, endpointURL: this.getStoreEndpointURL(store) }); } return stores; } } exports.WalletStorageManager = WalletStorageManager; //# sourceMappingURL=WalletStorageManager.js.map