UNPKG

@bsv/wallet-toolbox-client

Version:
1,066 lines (1,065 loc) 126 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WalletPermissionsManager = void 0; const sdk_1 = require("@bsv/sdk"); ////// TODO: ADD SUPPORT FOR ADMIN COUNTERPARTIES BASED ON WALLET STORAGE ////// PROHIBITION OF SPECIAL OPERATIONS IS ALSO CRITICAL. ////// !!!!!!!! SECURITY-CRITICAL ADDITION — DO NOT USE UNTIL IMPLEMENTED. function deepEqual(object1, object2) { if (object1 === null || object1 === undefined || object2 === null || object2 === undefined) { return object1 === object2; } const keys1 = Object.keys(object1); const keys2 = Object.keys(object2); if (keys1.length !== keys2.length) { return false; } for (const key of keys1) { const val1 = object1[key]; const val2 = object2[key]; const areObjects = isObject(val1) && isObject(val2); if ((areObjects && !deepEqual(val1, val2)) || (!areObjects && val1 !== val2)) { return false; } } return true; } function isObject(object) { return object != null && typeof object === 'object'; } /** * A map from each permission type to a special "admin basket" name used for storing * the tokens. The tokens themselves are unspent transaction outputs (UTXOs) with a * specialized PushDrop script that references the originator, expiry, etc. */ const BASKET_MAP = { protocol: 'admin protocol-permission', basket: 'admin basket-access', certificate: 'admin certificate-access', spending: 'admin spending-authorization' }; /** * @class WalletPermissionsManager * * Wraps an underlying BRC-100 `Wallet` implementation with permissions management capabilities. * The manager intercepts calls from external applications (identified by originators), checks if the request is allowed, * and if not, orchestrates user permission flows. It creates or renews on-chain tokens in special * admin baskets to track these authorizations. Finally, it proxies the actual call to the underlying wallet. * * ### Key Responsibilities: * - **Permission Checking**: Before standard wallet operations (e.g. `encrypt`), * the manager checks if a valid permission token exists. If not, it attempts to request permission from the user. * - **On-Chain Tokens**: When permission is granted, the manager stores it as an unspent "PushDrop" output. * This can be spent later to revoke or renew the permission. * - **Callbacks**: The manager triggers user-defined callbacks on permission requests (to show a UI prompt), * on grants/denials, and on internal processes. * * ### Implementation Notes: * - The manager follows the BRC-100 `createAction` + `signAction` pattern for building or spending these tokens. * - Token revocation or renewal uses standard BRC-100 flows: we build a transaction that consumes * the old token UTXO and outputs a new one (or none, if fully revoked). */ class WalletPermissionsManager { /** * Constructs a new Permissions Manager instance. * * @param underlyingWallet The underlying BRC-100 wallet, where requests are forwarded after permission is granted * @param adminOriginator The domain or FQDN that is automatically allowed everything * @param config A set of boolean flags controlling how strictly permissions are enforced */ constructor(underlyingWallet, adminOriginator, config = {}) { /** * Event callbacks that external code can subscribe to, e.g. to show a UI prompt * or log events. Each event can have multiple handlers. */ this.callbacks = { onProtocolPermissionRequested: [], onBasketAccessRequested: [], onCertificateAccessRequested: [], onSpendingAuthorizationRequested: [], onGroupedPermissionRequested: [] }; /** * We queue parallel requests for the same resource so that only one * user prompt is created for a single resource. If multiple calls come * in at once for the same "protocol:domain:privileged:counterparty" etc., * they get merged. * * The key is a string derived from the operation; the value is an object with a reference to the * associated request and an array of pending promise resolve/reject pairs, one for each active * operation that's waiting on the particular resource described by the key. */ this.activeRequests = new Map(); /** Cache recently confirmed permissions to avoid repeated lookups. */ this.permissionCache = new Map(); this.recentGrants = new Map(); this.underlying = underlyingWallet; this.adminOriginator = this.normalizeOriginator(adminOriginator) || adminOriginator; // Default all config options to true unless specified this.config = { seekProtocolPermissionsForSigning: true, seekProtocolPermissionsForEncrypting: true, seekProtocolPermissionsForHMAC: true, seekPermissionsForKeyLinkageRevelation: true, seekPermissionsForPublicKeyRevelation: true, seekPermissionsForIdentityKeyRevelation: true, seekPermissionsForIdentityResolution: true, seekBasketInsertionPermissions: true, seekBasketRemovalPermissions: true, seekBasketListingPermissions: true, seekPermissionWhenApplyingActionLabels: true, seekPermissionWhenListingActionsByLabel: true, seekCertificateDisclosurePermissions: true, seekCertificateAcquisitionPermissions: true, seekCertificateRelinquishmentPermissions: true, seekCertificateListingPermissions: true, encryptWalletMetadata: true, seekSpendingPermissions: true, seekGroupedPermission: true, differentiatePrivilegedOperations: true, ...config // override with user-specified config }; } /* --------------------------------------------------------------------- * HELPER METHODS FOR P-MODULE DELEGATION * --------------------------------------------------------------------- */ /** * Delegates a wallet method call to a P-module if the basket or protocol name uses a P-scheme. * Handles the full request/response transformation flow. * * @param basketOrProtocolName - The basket or protocol name to check for p-module delegation * @param method - The wallet method name being called * @param args - The original args passed to the method * @param originator - The originator of the request * @param underlyingCall - Callback that executes the underlying wallet method with transformed args * @returns The transformed response, or null if not a P-basket/protocol (caller should continue normal flow) */ async delegateToPModuleIfNeeded(basketOrProtocolName, method, args, originator, underlyingCall) { var _a; // Check if this is a P-protocol/basket if (!basketOrProtocolName.startsWith('p ')) { return null; // If not, caller should continue normal flow } const schemeID = basketOrProtocolName.split(' ')[1]; const module = (_a = this.config.permissionModules) === null || _a === void 0 ? void 0 : _a[schemeID]; if (!module) { throw new Error(`Unsupported P-module scheme: p ${schemeID}`); } // Transform request with module const transformedReq = await module.onRequest({ method, args, originator }); // Call underlying method with transformed request const results = await underlyingCall(transformedReq.args, originator); // Transform response with module return await module.onResponse(results, { method, originator }); } /** * Decrypts custom instructions in listOutputs results if encryption is configured. */ async decryptListOutputsMetadata(results) { if (results.outputs) { for (let i = 0; i < results.outputs.length; i++) { if (results.outputs[i].customInstructions) { results.outputs[i].customInstructions = await this.maybeDecryptMetadata(results.outputs[i].customInstructions); } } } return results; } /* --------------------------------------------------------------------- * 1) PUBLIC API FOR REGISTERING CALLBACKS (UI PROMPTS, LOGGING, ETC.) * --------------------------------------------------------------------- */ /** * Binds a callback function to a named event, such as `onProtocolPermissionRequested`. * * @param eventName The name of the event to listen to * @param handler A function that handles the event * @returns A numeric ID you can use to unbind later */ bindCallback(eventName, handler) { const arr = this.callbacks[eventName]; arr.push(handler); return arr.length - 1; } /** * Unbinds a previously registered callback by either its numeric ID (returned by `bindCallback`) * or by exact function reference. * * @param eventName The event name, e.g. "onProtocolPermissionRequested" * @param reference Either the numeric ID or the function reference * @returns True if successfully unbound, false otherwise */ unbindCallback(eventName, reference) { if (!this.callbacks[eventName]) return false; const arr = this.callbacks[eventName]; if (typeof reference === 'number') { if (arr[reference]) { arr[reference] = null; return true; } return false; } else { const index = arr.indexOf(reference); if (index !== -1) { arr[index] = null; return true; } return false; } } /** * Internally triggers a named event, calling all subscribed listeners. * Each callback is awaited in turn (though errors are swallowed so that * one failing callback doesn't prevent the others). * * @param eventName The event name * @param param The parameter object passed to all listeners */ async callEvent(eventName, param) { const arr = this.callbacks[eventName] || []; for (const cb of arr) { if (typeof cb === 'function') { try { await cb(param); } catch (e) { // Intentionally swallow errors from user-provided callbacks } } } } /* --------------------------------------------------------------------- * 2) PERMISSION (GRANT / DENY) METHODS * --------------------------------------------------------------------- */ /** * Grants a previously requested permission. * This method: * 1) Resolves all pending promise calls waiting on this request * 2) Optionally creates or renews an on-chain PushDrop token (unless `ephemeral===true`) * * @param params requestID to identify which request is granted, plus optional expiry * or `ephemeral` usage, etc. */ async grantPermission(params) { // 1) Identify the matching queued requests in `activeRequests` const matching = this.activeRequests.get(params.requestID); if (!matching) { throw new Error('Request ID not found.'); } // 2) Mark all matching requests as resolved, deleting the entry for (const x of matching.pending) { x.resolve(true); } this.activeRequests.delete(params.requestID); // 3) If `ephemeral !== true`, we create or renew an on-chain token if (!params.ephemeral) { const request = matching.request; if (!request.renewal) { // brand-new permission token await this.createPermissionOnChain(request, params.expiry || 0, // default: never expires params.amount); } else { // renewal => spend the old token, produce a new one await this.renewPermissionOnChain(request.previousToken, request, params.expiry || 0, // default: never expires params.amount); } } // Only cache non-ephemeral permissions // Ephemeral permissions should not be cached as they are one-time authorizations if (!params.ephemeral) { const expiry = params.expiry || 0; // default: never expires const key = this.buildRequestKey(matching.request); this.cachePermission(key, expiry); this.markRecentGrant(matching.request); } } /** * Denies a previously requested permission. * This method rejects all pending promise calls waiting on that request * * @param requestID requestID identifying which request to deny */ async denyPermission(requestID) { // 1) Identify the matching requests const matching = this.activeRequests.get(requestID); if (!matching) { throw new Error('Request ID not found.'); } // 2) Reject all matching requests, deleting the entry for (const x of matching.pending) { x.reject(new Error('Permission denied.')); } this.activeRequests.delete(requestID); } /** * Grants a previously requested grouped permission. * @param params.requestID The ID of the request being granted. * @param params.granted A subset of the originally requested permissions that the user has granted. * @param params.expiry An optional expiry time (in seconds) for the new permission tokens. */ async grantGroupedPermission(params) { var _a, _b, _c; const matching = this.activeRequests.get(params.requestID); if (!matching) { throw new Error('Request ID not found.'); } const originalRequest = matching.request; const { originator, permissions: requestedPermissions, displayOriginator } = originalRequest; const originLookupValues = this.buildOriginatorLookupValues(displayOriginator, originator); // --- Validation: Ensure granted permissions are a subset of what was requested --- if (params.granted.spendingAuthorization && !deepEqual(params.granted.spendingAuthorization, requestedPermissions.spendingAuthorization)) { throw new Error('Granted spending authorization does not match the original request.'); } if ((_a = params.granted.protocolPermissions) === null || _a === void 0 ? void 0 : _a.some(g => { var _a; return !((_a = requestedPermissions.protocolPermissions) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) { throw new Error('Granted protocol permissions are not a subset of the original request.'); } if ((_b = params.granted.basketAccess) === null || _b === void 0 ? void 0 : _b.some(g => { var _a; return !((_a = requestedPermissions.basketAccess) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) { throw new Error('Granted basket access permissions are not a subset of the original request.'); } if ((_c = params.granted.certificateAccess) === null || _c === void 0 ? void 0 : _c.some(g => { var _a; return !((_a = requestedPermissions.certificateAccess) === null || _a === void 0 ? void 0 : _a.find(r => deepEqual(r, g))); })) { throw new Error('Granted certificate access permissions are not a subset of the original request.'); } // --- End Validation --- const expiry = params.expiry || 0; // default: never expires if (params.granted.spendingAuthorization) { await this.createPermissionOnChain({ type: 'spending', originator, spending: { satoshis: params.granted.spendingAuthorization.amount }, reason: params.granted.spendingAuthorization.description }, 0, // No expiry for spending tokens params.granted.spendingAuthorization.amount); } for (const p of params.granted.protocolPermissions || []) { const token = await this.findProtocolToken(originator, false, // No privileged protocols allowed in groups for added security. p.protocolID, p.counterparty || 'self', true, originLookupValues); if (token) { const request = { type: 'protocol', originator, privileged: false, // No privileged protocols allowed in groups for added security. protocolID: p.protocolID, counterparty: p.counterparty || 'self', reason: p.description }; await this.renewPermissionOnChain(token, request, expiry); this.markRecentGrant(request); } else { const request = { type: 'protocol', originator, privileged: false, // No privileged protocols allowed in groups for added security. protocolID: p.protocolID, counterparty: p.counterparty || 'self', reason: p.description }; await this.createPermissionOnChain(request, expiry); this.markRecentGrant(request); } } for (const b of params.granted.basketAccess || []) { const request = { type: 'basket', originator, basket: b.basket, reason: b.description }; await this.createPermissionOnChain(request, expiry); this.markRecentGrant(request); } for (const c of params.granted.certificateAccess || []) { const request = { type: 'certificate', originator, privileged: false, // No certificates on the privileged identity are allowed as part of groups. certificate: { verifier: c.verifierPublicKey, certType: c.type, fields: c.fields }, reason: c.description }; await this.createPermissionOnChain(request, expiry); this.markRecentGrant(request); } // Resolve all pending promises for this request for (const p of matching.pending) { p.resolve(true); } this.activeRequests.delete(params.requestID); } /** * Denies a previously requested grouped permission. * @param requestID The ID of the request being denied. */ async denyGroupedPermission(requestID) { const matching = this.activeRequests.get(requestID); if (!matching) { throw new Error('Request ID not found.'); } const err = new Error('The user has denied the request for permission.'); err.code = 'ERR_PERMISSION_DENIED'; for (const p of matching.pending) { p.reject(err); } this.activeRequests.delete(requestID); } /* --------------------------------------------------------------------- * 3) THE "ENSURE" METHODS: CHECK IF PERMISSION EXISTS, OTHERWISE PROMPT * --------------------------------------------------------------------- */ /** * Ensures the originator has protocol usage permission. * If no valid (unexpired) permission token is found, triggers a permission request flow. */ async ensureProtocolPermission({ originator, privileged, protocolID, counterparty, reason, seekPermission = true, usageType }) { const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator); originator = normalizedOriginator; // 1) adminOriginator can do anything if (this.isAdminOriginator(originator)) return true; // 2) If security level=0, we consider it "open" usage const [level, protoName] = protocolID; if (level === 0) return true; // 3) If protocol is admin-reserved, block if (this.isAdminProtocol(protocolID)) { throw new Error(`Protocol “${protoName}” is admin-only.`); } // Allow the configured exceptions. if (usageType === 'signing' && !this.config.seekProtocolPermissionsForSigning) { return true; } if (usageType === 'encrypting' && !this.config.seekProtocolPermissionsForEncrypting) { return true; } if (usageType === 'hmac' && !this.config.seekProtocolPermissionsForHMAC) { return true; } if (usageType === 'publicKey' && !this.config.seekPermissionsForPublicKeyRevelation) { return true; } if (usageType === 'identityKey' && !this.config.seekPermissionsForIdentityKeyRevelation) { return true; } if (usageType === 'linkageRevelation' && !this.config.seekPermissionsForKeyLinkageRevelation) { return true; } if (!this.config.differentiatePrivilegedOperations) { privileged = false; } const cacheKey = this.buildRequestKey({ type: 'protocol', originator, privileged, protocolID, counterparty }); if (this.isPermissionCached(cacheKey)) { return true; } if (this.isRecentlyGranted(cacheKey)) { return true; } // 4) Attempt to find a valid token in the internal basket const token = await this.findProtocolToken(originator, privileged, protocolID, counterparty, /*includeExpired=*/ true, lookupValues); if (token) { if (!this.isTokenExpired(token.expiry)) { // valid and unexpired this.cachePermission(cacheKey, token.expiry); return true; } else { // has a token but expired => request renewal if allowed if (!seekPermission) { throw new Error(`Protocol permission expired and no further user consent allowed (seekPermission=false).`); } return await this.requestPermissionFlow({ type: 'protocol', originator, privileged, protocolID, counterparty, reason, renewal: true, previousToken: token }); } } else { // No token found => request a new one if allowed if (!seekPermission) { throw new Error(`No protocol permission token found (seekPermission=false).`); } const granted = await this.requestPermissionFlow({ type: 'protocol', originator, privileged, protocolID, counterparty, reason, renewal: false }); return granted; } } /** * Ensures the originator has basket usage permission for the specified basket. * If not, triggers a permission request flow. */ async ensureBasketAccess({ originator, basket, reason, seekPermission = true, usageType }) { const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator); originator = normalizedOriginator; if (this.isAdminOriginator(originator)) return true; if (this.isAdminBasket(basket)) { throw new Error(`Basket “${basket}” is admin-only.`); } if (usageType === 'insertion' && !this.config.seekBasketInsertionPermissions) return true; if (usageType === 'removal' && !this.config.seekBasketRemovalPermissions) return true; if (usageType === 'listing' && !this.config.seekBasketListingPermissions) return true; const cacheKey = this.buildRequestKey({ type: 'basket', originator, basket }); if (this.isPermissionCached(cacheKey)) { return true; } if (this.isRecentlyGranted(cacheKey)) { return true; } const token = await this.findBasketToken(originator, basket, true, lookupValues); if (token) { if (!this.isTokenExpired(token.expiry)) { this.cachePermission(cacheKey, token.expiry); return true; } else { if (!seekPermission) { throw new Error(`Basket permission expired (seekPermission=false).`); } return await this.requestPermissionFlow({ type: 'basket', originator, basket, reason, renewal: true, previousToken: token }); } } else { // none if (!seekPermission) { throw new Error(`No basket permission found, and no user consent allowed (seekPermission=false).`); } const granted = await this.requestPermissionFlow({ type: 'basket', originator, basket, reason, renewal: false }); return granted; } } /** * Ensures the originator has a valid certificate permission. * This is relevant when revealing certificate fields in DCAP contexts. */ async ensureCertificateAccess({ originator, privileged, verifier, certType, fields, reason, seekPermission = true, usageType }) { const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator); originator = normalizedOriginator; if (this.isAdminOriginator(originator)) return true; if (usageType === 'disclosure' && !this.config.seekCertificateDisclosurePermissions) { return true; } if (!this.config.differentiatePrivilegedOperations) { privileged = false; } const cacheKey = this.buildRequestKey({ type: 'certificate', originator, privileged, certificate: { verifier, certType, fields } }); if (this.isPermissionCached(cacheKey)) { return true; } if (this.isRecentlyGranted(cacheKey)) { return true; } const token = await this.findCertificateToken(originator, privileged, verifier, certType, fields, /*includeExpired=*/ true, lookupValues); if (token) { if (!this.isTokenExpired(token.expiry)) { this.cachePermission(cacheKey, token.expiry); return true; } else { if (!seekPermission) { throw new Error(`Certificate permission expired (seekPermission=false).`); } return await this.requestPermissionFlow({ type: 'certificate', originator, privileged, certificate: { verifier, certType, fields }, reason, renewal: true, previousToken: token }); } } else { if (!seekPermission) { throw new Error(`No certificate permission found (seekPermission=false).`); } const granted = await this.requestPermissionFlow({ type: 'certificate', originator, privileged, certificate: { verifier, certType, fields }, reason, renewal: false }); return granted; } } /** * Ensures the originator has spending authorization (DSAP) for a certain satoshi amount. * If the existing token limit is insufficient, attempts to renew. If no token, attempts to create one. */ async ensureSpendingAuthorization({ originator, satoshis, lineItems, reason, seekPermission = true }) { const { normalized: normalizedOriginator, lookupValues } = this.prepareOriginator(originator); originator = normalizedOriginator; if (this.isAdminOriginator(originator)) return true; if (!this.config.seekSpendingPermissions) { // We skip spending permission entirely return true; } const cacheKey = this.buildRequestKey({ type: 'spending', originator, spending: { satoshis } }); if (this.isPermissionCached(cacheKey)) { return true; } const token = await this.findSpendingToken(originator, lookupValues); if (token === null || token === void 0 ? void 0 : token.authorizedAmount) { // Check how much has been spent so far const spentSoFar = await this.querySpentSince(token); if (spentSoFar + satoshis <= token.authorizedAmount) { this.cachePermission(cacheKey, token.expiry); return true; } else { // Renew if possible if (!seekPermission) { throw new Error(`Spending authorization insufficient for ${satoshis}, no user consent (seekPermission=false).`); } return await this.requestPermissionFlow({ type: 'spending', originator, spending: { satoshis, lineItems }, reason, renewal: true, previousToken: token }); } } else { // no token if (!seekPermission) { throw new Error(`No spending authorization found, (seekPermission=false).`); } return await this.requestPermissionFlow({ type: 'spending', originator, spending: { satoshis, lineItems }, reason, renewal: false }); } } /** * Ensures the originator has label usage permission. * If no valid (unexpired) permission token is found, triggers a permission request flow. */ async ensureLabelAccess({ originator, label, reason, seekPermission = true, usageType }) { const { normalized: normalizedOriginator } = this.prepareOriginator(originator); originator = normalizedOriginator; // 1) adminOriginator can do anything if (this.isAdminOriginator(originator)) return true; // 2) If label is admin-reserved, block if (this.isAdminLabel(label)) { throw new Error(`Label “${label}” is admin-only.`); } if (usageType === 'apply' && !this.config.seekPermissionWhenApplyingActionLabels) { return true; } if (usageType === 'list' && !this.config.seekPermissionWhenListingActionsByLabel) { return true; } const cacheKey = this.buildRequestKey({ type: 'protocol', originator, privileged: false, protocolID: [1, `action label ${label}`], counterparty: 'self' }); if (this.isPermissionCached(cacheKey)) { return true; } // 3) Let ensureProtocolPermission handle the rest. return await this.ensureProtocolPermission({ originator, privileged: false, protocolID: [1, `action label ${label}`], counterparty: 'self', reason, seekPermission, usageType: 'generic' }); } /** * A central method that triggers the permission request flow. * - It checks if there's already an active request for the same key * - If so, we wait on that existing request rather than creating a duplicative one * - Otherwise we create a new request queue, call the relevant "onXXXRequested" event, * and return a promise that resolves once permission is granted or rejects if denied. */ async requestPermissionFlow(r) { var _a; const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator; const preparedRequest = { ...r, originator: normalizedOriginator, displayOriginator: (_a = r.displayOriginator) !== null && _a !== void 0 ? _a : r.originator }; const key = this.buildRequestKey(preparedRequest); // If there's already a queue for the same resource, we piggyback on it const existingQueue = this.activeRequests.get(key); if (existingQueue && existingQueue.pending.length > 0) { return new Promise((resolve, reject) => { existingQueue.pending.push({ resolve, reject }); }); } // Otherwise, create a new queue with a single entry // Return a promise that resolves or rejects once the user grants/denies return new Promise(async (resolve, reject) => { this.activeRequests.set(key, { request: preparedRequest, pending: [{ resolve, reject }] }); // Fire the relevant onXXXRequested event (which one depends on r.type) switch (preparedRequest.type) { case 'protocol': await this.callEvent('onProtocolPermissionRequested', { ...preparedRequest, requestID: key }); break; case 'basket': await this.callEvent('onBasketAccessRequested', { ...preparedRequest, requestID: key }); break; case 'certificate': await this.callEvent('onCertificateAccessRequested', { ...preparedRequest, requestID: key }); break; case 'spending': await this.callEvent('onSpendingAuthorizationRequested', { ...preparedRequest, requestID: key }); break; } }); } /** We always use `keyID="1"` and `counterparty="self"` for these encryption ops. */ async encryptPermissionTokenField(plaintext) { const data = typeof plaintext === 'string' ? sdk_1.Utils.toArray(plaintext, 'utf8') : plaintext; const { ciphertext } = await this.underlying.encrypt({ plaintext: data, protocolID: WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL, keyID: '1' }, this.adminOriginator); return ciphertext; } async decryptPermissionTokenField(ciphertext) { try { const { plaintext } = await this.underlying.decrypt({ ciphertext, protocolID: WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL, keyID: '1' }, this.adminOriginator); return plaintext; } catch (e) { return ciphertext; } } /** * Encrypts wallet metadata if configured to do so, otherwise returns the original plaintext for storage. * @param plaintext The metadata to encrypt if configured to do so * @returns The encrypted metadata, or the original value if encryption was disabled. */ async maybeEncryptMetadata(plaintext) { if (!this.config.encryptWalletMetadata) { return plaintext; } const { ciphertext } = await this.underlying.encrypt({ plaintext: sdk_1.Utils.toArray(plaintext, 'utf8'), protocolID: WalletPermissionsManager.METADATA_ENCRYPTION_PROTOCOL, keyID: '1' }, this.adminOriginator); return sdk_1.Utils.toBase64(ciphertext); } /** * Attempts to decrypt metadata. if decryption fails, assumes the value is already plaintext and returns it. * @param ciphertext The metadata to attempt decryption for. * @returns The decrypted metadata. If decryption fails, returns the original value instead. */ async maybeDecryptMetadata(ciphertext) { try { const { plaintext } = await this.underlying.decrypt({ ciphertext: sdk_1.Utils.toArray(ciphertext, 'base64'), protocolID: WalletPermissionsManager.METADATA_ENCRYPTION_PROTOCOL, keyID: '1' }, this.adminOriginator); return sdk_1.Utils.toUTF8(plaintext); } catch (e) { return ciphertext; } } /** Helper to see if a token's expiry is in the past. */ isTokenExpired(expiry) { const now = Math.floor(Date.now() / 1000); return expiry > 0 && expiry < now; } /** Looks for a DPACP permission token matching origin/domain, privileged, protocol, cpty. */ async findProtocolToken(originator, privileged, protocolID, counterparty, includeExpired, originatorLookupValues) { const [secLevel, protoName] = protocolID; const originsToTry = (originatorLookupValues === null || originatorLookupValues === void 0 ? void 0 : originatorLookupValues.length) ? originatorLookupValues : [originator]; for (const originTag of originsToTry) { const tags = [ `originator ${originTag}`, `privileged ${!!privileged}`, `protocolName ${protoName}`, `protocolSecurityLevel ${secLevel}` ]; if (secLevel === 2) { tags.push(`counterparty ${counterparty}`); } const result = await this.underlying.listOutputs({ basket: BASKET_MAP.protocol, tags, tagQueryMode: 'all', include: 'entire transactions' }, this.adminOriginator); for (const out of result.outputs) { const [txid, outputIndexStr] = out.outpoint.split('.'); const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid); const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript); if (!dec || !dec.fields || dec.fields.length < 6) continue; const domainRaw = dec.fields[0]; const expiryRaw = dec.fields[1]; const privRaw = dec.fields[2]; const secLevelRaw = dec.fields[3]; const protoNameRaw = dec.fields[4]; const counterpartyRaw = dec.fields[5]; const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw)); const normalizedDomain = this.normalizeOriginator(domainDecoded); if (normalizedDomain !== originator) { continue; } const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10); const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'; const secLevelDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10); const protoNameDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw)); const cptyDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw)); if (privDecoded !== !!privileged || secLevelDecoded !== secLevel || protoNameDecoded !== protoName || (secLevelDecoded === 2 && cptyDecoded !== counterparty)) { continue; } if (!includeExpired && this.isTokenExpired(expiryDecoded)) { continue; } return { tx: tx.toBEEF(), txid: out.outpoint.split('.')[0], outputIndex: parseInt(out.outpoint.split('.')[1], 10), outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(), satoshis: out.satoshis, originator, rawOriginator: domainDecoded, privileged, protocol: protoName, securityLevel: secLevel, expiry: expiryDecoded, counterparty: cptyDecoded }; } } return undefined; } /** Finds ALL DPACP permission tokens matching origin/domain, privileged, protocol, cpty. Never filters by expiry. */ async findAllProtocolTokens(originator, privileged, protocolID, counterparty, originatorLookupValues) { const [secLevel, protoName] = protocolID; const originsToTry = (originatorLookupValues === null || originatorLookupValues === void 0 ? void 0 : originatorLookupValues.length) ? originatorLookupValues : [originator]; const matches = []; const seen = new Set(); for (const originTag of originsToTry) { const tags = [ `originator ${originTag}`, `privileged ${!!privileged}`, `protocolName ${protoName}`, `protocolSecurityLevel ${secLevel}` ]; if (secLevel === 2) { tags.push(`counterparty ${counterparty}`); } const result = await this.underlying.listOutputs({ basket: BASKET_MAP.protocol, tags, tagQueryMode: 'all', include: 'entire transactions' }, this.adminOriginator); for (const out of result.outputs) { if (seen.has(out.outpoint)) continue; const [txid, outputIndexStr] = out.outpoint.split('.'); const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid); const vout = Number(outputIndexStr); const dec = sdk_1.PushDrop.decode(tx.outputs[vout].lockingScript); if (!dec || !dec.fields || dec.fields.length < 6) continue; const domainRaw = dec.fields[0]; const expiryRaw = dec.fields[1]; const privRaw = dec.fields[2]; const secLevelRaw = dec.fields[3]; const protoNameRaw = dec.fields[4]; const counterpartyRaw = dec.fields[5]; const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw)); const normalizedDomain = this.normalizeOriginator(domainDecoded); if (normalizedDomain !== originator) { continue; } const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10); const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'; const secLevelDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(secLevelRaw)), 10); const protoNameDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(protoNameRaw)); const cptyDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(counterpartyRaw)); if (privDecoded !== !!privileged || secLevelDecoded !== secLevel || protoNameDecoded !== protoName || (secLevelDecoded === 2 && cptyDecoded !== counterparty)) { continue; } seen.add(out.outpoint); matches.push({ tx: tx.toBEEF(), txid, outputIndex: vout, outputScript: tx.outputs[vout].lockingScript.toHex(), satoshis: out.satoshis, originator, rawOriginator: domainDecoded, privileged, protocol: protoName, securityLevel: secLevel, expiry: expiryDecoded, counterparty: cptyDecoded }); } } return matches; } /** Looks for a DBAP token matching (originator, basket). */ async findBasketToken(originator, basket, includeExpired, originatorLookupValues) { const originsToTry = (originatorLookupValues === null || originatorLookupValues === void 0 ? void 0 : originatorLookupValues.length) ? originatorLookupValues : [originator]; for (const originTag of originsToTry) { const result = await this.underlying.listOutputs({ basket: BASKET_MAP.basket, tags: [`originator ${originTag}`, `basket ${basket}`], tagQueryMode: 'all', include: 'entire transactions' }, this.adminOriginator); for (const out of result.outputs) { const [txid, outputIndexStr] = out.outpoint.split('.'); const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid); const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript); if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 3) continue; const domainRaw = dec.fields[0]; const expiryRaw = dec.fields[1]; const basketRaw = dec.fields[2]; const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw)); const normalizedDomain = this.normalizeOriginator(domainDecoded); if (normalizedDomain !== originator) { continue; } const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10); const basketDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(basketRaw)); if (basketDecoded !== basket) continue; if (!includeExpired && this.isTokenExpired(expiryDecoded)) continue; return { tx: tx.toBEEF(), txid: out.outpoint.split('.')[0], outputIndex: parseInt(out.outpoint.split('.')[1], 10), outputScript: tx.outputs[Number(outputIndexStr)].lockingScript.toHex(), satoshis: out.satoshis, originator, rawOriginator: domainDecoded, basketName: basketDecoded, expiry: expiryDecoded }; } } return undefined; } /** Looks for a DCAP token matching (origin, privileged, verifier, certType, fields subset). */ async findCertificateToken(originator, privileged, verifier, certType, fields, includeExpired, originatorLookupValues) { const originsToTry = (originatorLookupValues === null || originatorLookupValues === void 0 ? void 0 : originatorLookupValues.length) ? originatorLookupValues : [originator]; for (const originTag of originsToTry) { const result = await this.underlying.listOutputs({ basket: BASKET_MAP.certificate, tags: [`originator ${originTag}`, `privileged ${!!privileged}`, `type ${certType}`, `verifier ${verifier}`], tagQueryMode: 'all', include: 'entire transactions' }, this.adminOriginator); for (const out of result.outputs) { const [txid, outputIndexStr] = out.outpoint.split('.'); const tx = sdk_1.Transaction.fromBEEF(result.BEEF, txid); const dec = sdk_1.PushDrop.decode(tx.outputs[Number(outputIndexStr)].lockingScript); if (!(dec === null || dec === void 0 ? void 0 : dec.fields) || dec.fields.length < 6) continue; const [domainRaw, expiryRaw, privRaw, typeRaw, fieldsRaw, verifierRaw] = dec.fields; const domainDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(domainRaw)); const normalizedDomain = this.normalizeOriginator(domainDecoded); if (normalizedDomain !== originator) { continue; } const expiryDecoded = parseInt(sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(expiryRaw)), 10); const privDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(privRaw)) === 'true'; const typeDecoded = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(typeRaw)); const verifierDec = sdk_1.Utils.toUTF8(await this.decryptPermissionTokenField(verifierRaw)); const fieldsJson = await this.decryptPermissionTokenField(fieldsRaw); const allFields = JSON.parse(sdk_1.Utils.toUTF8(fieldsJson));