UNPKG

@bsv/wallet-toolbox-client

Version:
668 lines 31 kB
import { WalletInterface, WalletProtocol, Base64String, PubKeyHex } from '@bsv/sdk'; /** * Describes a group of permissions that can be requested together. * This structure is based on BRC-73. */ export interface GroupedPermissions { description?: string; spendingAuthorization?: { amount: number; description: string; }; protocolPermissions?: Array<{ protocolID: WalletProtocol; counterparty?: string; description: string; }>; basketAccess?: Array<{ basket: string; description: string; }>; certificateAccess?: Array<{ type: string; fields: string[]; verifierPublicKey: string; description: string; }>; } /** * The object passed to the UI when a grouped permission is requested. */ export interface GroupedPermissionRequest { originator: string; requestID: string; permissions: GroupedPermissions; } /** * Signature for functions that handle a grouped permission request event. */ export type GroupedPermissionEventHandler = (request: GroupedPermissionRequest) => void | Promise<void>; /** * Describes a single requested permission that the user must either grant or deny. * * Four categories of permission are supported, each with a unique protocol: * 1) protocol - "DPACP" (Domain Protocol Access Control Protocol) * 2) basket - "DBAP" (Domain Basket Access Protocol) * 3) certificate - "DCAP" (Domain Certificate Access Protocol) * 4) spending - "DSAP" (Domain Spending Authorization Protocol) * * This model underpins "requests" made to the user for permission, which the user can * either grant or deny. The manager can then create on-chain tokens (PushDrop outputs) * if permission is granted. Denying requests cause the underlying operation to throw, * and no token is created. An "ephemeral" grant is also possible, denoting a one-time * authorization without an associated persistent on-chain token. */ export interface PermissionRequest { type: 'protocol' | 'basket' | 'certificate' | 'spending'; originator: string; privileged?: boolean; protocolID?: WalletProtocol; counterparty?: string; basket?: string; certificate?: { verifier: string; certType: string; fields: string[]; }; spending?: { satoshis: number; lineItems?: Array<{ type: 'input' | 'output' | 'fee'; description: string; satoshis: number; }>; }; reason?: string; renewal?: boolean; previousToken?: PermissionToken; } /** * Signature for functions that handle a permission request event, e.g. "Please ask the user to allow basket X". */ export type PermissionEventHandler = (request: PermissionRequest & { requestID: string; }) => void | Promise<void>; /** * Data structure representing an on-chain permission token. * It is typically stored as a single unspent PushDrop output in a special "internal" admin basket belonging to * the user, held in their underlying wallet. * * It can represent any of the four permission categories by having the relevant fields: * - DPACP: originator, privileged, protocol, securityLevel, counterparty * - DBAP: originator, basketName * - DCAP: originator, privileged, verifier, certType, certFields * - DSAP: originator, authorizedAmount */ export interface PermissionToken { /** The transaction ID where this token resides. */ txid: string; /** The current transaction encapsulating the token. */ tx: number[]; /** The output index within that transaction. */ outputIndex: number; /** The exact script hex for the locking script. */ outputScript: string; /** The amount of satoshis assigned to the permission output (often 1). */ satoshis: number; /** The originator domain or FQDN that is allowed to use this permission. */ originator: string; /** The expiration time for this token in UNIX epoch seconds. (0 or omitted for spending authorizations, which are indefinite) */ expiry: number; /** Whether this token grants privileged usage (for protocol or certificate). */ privileged?: boolean; /** The protocol name, if this is a DPACP token. */ protocol?: string; /** The security level (0,1,2) for DPACP. */ securityLevel?: 0 | 1 | 2; /** The counterparty, for DPACP. */ counterparty?: string; /** The name of a basket, if this is a DBAP token. */ basketName?: string; /** The certificate type, if this is a DCAP token. */ certType?: string; /** The certificate fields that this token covers, if DCAP token. */ certFields?: string[]; /** The "verifier" public key string, if DCAP. */ verifier?: string; /** For DSAP, the maximum authorized spending for the month. */ authorizedAmount?: number; } /** * The set of callbacks that external code can bind to, e.g. to display UI prompts or logs * when a permission is requested. */ export interface WalletPermissionsManagerCallbacks { onProtocolPermissionRequested?: PermissionEventHandler[]; onBasketAccessRequested?: PermissionEventHandler[]; onCertificateAccessRequested?: PermissionEventHandler[]; onSpendingAuthorizationRequested?: PermissionEventHandler[]; onGroupedPermissionRequested?: GroupedPermissionEventHandler[]; } /** * Configuration object for the WalletPermissionsManager. If a given option is `false`, * the manager will skip or alter certain permission checks or behaviors. * * By default, all of these are `true` unless specified otherwise. This is the most secure configuration. */ export interface PermissionsManagerConfig { /** * For `createSignature` and `verifySignature`, * require a "protocol usage" permission check? */ seekProtocolPermissionsForSigning?: boolean; /** * For methods that perform encryption (encrypt/decrypt), require * a "protocol usage" permission check? */ seekProtocolPermissionsForEncrypting?: boolean; /** * For methods that perform HMAC creation or verification (createHmac, verifyHmac), * require a "protocol usage" permission check? */ seekProtocolPermissionsForHMAC?: boolean; /** * For revealing counterparty-level or specific key linkage revelation information, * should we require permission? */ seekPermissionsForKeyLinkageRevelation?: boolean; /** * For revealing any user public key (getPublicKey) **other** than the identity key, * should we require permission? */ seekPermissionsForPublicKeyRevelation?: boolean; /** * If getPublicKey is requested with `identityKey=true`, do we require permission? */ seekPermissionsForIdentityKeyRevelation?: boolean; /** * If discoverByIdentityKey / discoverByAttributes are called, do we require permission * for "identity resolution" usage? */ seekPermissionsForIdentityResolution?: boolean; /** * When we do internalizeAction with `basket insertion`, or include outputs in baskets * with `createAction, do we ask for basket permission? */ seekBasketInsertionPermissions?: boolean; /** * When relinquishOutput is called, do we ask for basket permission? */ seekBasketRemovalPermissions?: boolean; /** * When listOutputs is called, do we ask for basket permission? */ seekBasketListingPermissions?: boolean; /** * When createAction is called with labels, do we ask for "label usage" permission? */ seekPermissionWhenApplyingActionLabels?: boolean; /** * When listActions is called with labels, do we ask for "label usage" permission? */ seekPermissionWhenListingActionsByLabel?: boolean; /** * If proving a certificate (proveCertificate) or revealing certificate fields, * do we require a "certificate access" permission? */ seekCertificateDisclosurePermissions?: boolean; /** * If acquiring a certificate (acquireCertificate), do we require a permission check? */ seekCertificateAcquisitionPermissions?: boolean; /** * If relinquishing a certificate (relinquishCertificate), do we require a permission check? */ seekCertificateRelinquishmentPermissions?: boolean; /** * If listing a user's certificates (listCertificates), do we require a permission check? */ seekCertificateListingPermissions?: boolean; /** * Should transaction descriptions, input descriptions, and output descriptions be encrypted * when before they are passed to the underlying wallet, and transparently decrypted when retrieved? */ encryptWalletMetadata?: boolean; /** * If the originator tries to spend wallet funds (netSpent > 0 in createAction), * do we seek spending authorization? */ seekSpendingPermissions?: boolean; /** * If true, triggers a grouped permission request flow based on the originator's `manifest.json`. */ seekGroupedPermission?: boolean; /** * If false, permissions are checked without regard for whether we are in * privileged mode. Privileged status is ignored with respect to whether * permissions are granted. Internally, they are always sought and checked * with privileged=false, regardless of the actual value. */ differentiatePrivilegedOperations?: boolean; } /** * @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). */ export declare class WalletPermissionsManager implements WalletInterface { /** A reference to the BRC-100 wallet instance. */ private underlying; /** The "admin" domain or FQDN that is implicitly allowed to do everything. */ private adminOriginator; /** * Event callbacks that external code can subscribe to, e.g. to show a UI prompt * or log events. Each event can have multiple handlers. */ private callbacks; /** * 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. */ private activeRequests; /** Cache recently confirmed permissions to avoid repeated lookups. */ private permissionCache; /** How long a cached permission remains valid (5 minutes). */ private static readonly CACHE_TTL_MS; /** * Configuration that determines whether to skip or apply various checks and encryption. */ private config; /** * 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: WalletInterface, adminOriginator: string, config?: PermissionsManagerConfig); /** * 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: keyof WalletPermissionsManagerCallbacks, handler: PermissionEventHandler | GroupedPermissionEventHandler): number; /** * 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: keyof WalletPermissionsManagerCallbacks, reference: number | Function): boolean; /** * 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 */ private callEvent; /** * 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. */ grantPermission(params: { requestID: string; expiry?: number; ephemeral?: boolean; amount?: number; }): Promise<void>; /** * Denies a previously requested permission. * This method rejects all pending promise calls waiting on that request * * @param requestID requestID identifying which request to deny */ denyPermission(requestID: string): Promise<void>; /** * 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. */ grantGroupedPermission(params: { requestID: string; granted: Partial<GroupedPermissions>; expiry?: number; }): Promise<void>; /** * Denies a previously requested grouped permission. * @param requestID The ID of the request being denied. */ denyGroupedPermission(requestID: string): Promise<void>; /** * Ensures the originator has protocol usage permission. * If no valid (unexpired) permission token is found, triggers a permission request flow. */ ensureProtocolPermission({ originator, privileged, protocolID, counterparty, reason, seekPermission, usageType }: { originator: string; privileged: boolean; protocolID: WalletProtocol; counterparty: string; reason?: string; seekPermission?: boolean; usageType: 'signing' | 'encrypting' | 'hmac' | 'publicKey' | 'identityKey' | 'linkageRevelation' | 'generic'; }): Promise<boolean>; /** * Ensures the originator has basket usage permission for the specified basket. * If not, triggers a permission request flow. */ ensureBasketAccess({ originator, basket, reason, seekPermission, usageType }: { originator: string; basket: string; reason?: string; seekPermission?: boolean; usageType: 'insertion' | 'removal' | 'listing'; }): Promise<boolean>; /** * Ensures the originator has a valid certificate permission. * This is relevant when revealing certificate fields in DCAP contexts. */ ensureCertificateAccess({ originator, privileged, verifier, certType, fields, reason, seekPermission, usageType }: { originator: string; privileged: boolean; verifier: string; certType: string; fields: string[]; reason?: string; seekPermission?: boolean; usageType: 'disclosure'; }): Promise<boolean>; /** * 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. */ ensureSpendingAuthorization({ originator, satoshis, lineItems, reason, seekPermission }: { originator: string; satoshis: number; lineItems?: Array<{ type: 'input' | 'output' | 'fee'; description: string; satoshis: number; }>; reason?: string; seekPermission?: boolean; }): Promise<boolean>; /** * Ensures the originator has label usage permission. * If no valid (unexpired) permission token is found, triggers a permission request flow. */ ensureLabelAccess({ originator, label, reason, seekPermission, usageType }: { originator: string; label: string; reason?: string; seekPermission?: boolean; usageType: 'apply' | 'list'; }): Promise<boolean>; /** * 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. */ private requestPermissionFlow; /** * We will use a administrative "permission token encryption" protocol to store fields * in each permission's PushDrop script. This ensures that only the user's wallet * can decrypt them. In practice, this data is not super sensitive, but we still * follow the principle of least exposure. */ private static readonly PERM_TOKEN_ENCRYPTION_PROTOCOL; /** * Similarly, we will use a "metadata encryption" protocol to preserve the confidentiality * of transaction descriptions and input/output descriptions from lower storage layers. */ private static readonly METADATA_ENCRYPTION_PROTOCOL; /** We always use `keyID="1"` and `counterparty="self"` for these encryption ops. */ private encryptPermissionTokenField; private decryptPermissionTokenField; /** * 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. */ private maybeEncryptMetadata; /** * 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. */ private maybeDecryptMetadata; /** Helper to see if a token's expiry is in the past. */ private isTokenExpired; /** Looks for a DPACP permission token matching origin/domain, privileged, protocol, cpty. */ private findProtocolToken; /** Looks for a DBAP token matching (originator, basket). */ private findBasketToken; /** Looks for a DCAP token matching (origin, privileged, verifier, certType, fields subset). */ private findCertificateToken; /** Looks for a DSAP token matching origin, returning the first one found. */ private findSpendingToken; /** * Returns the current month and year in UTC as a string in the format "YYYY-MM". * * @returns {string} The current month and year in UTC. */ private getCurrentMonthYearUTC; /** * Returns spending for an originator in the current calendar month. */ querySpentSince(token: PermissionToken): Promise<number>; /** * Creates a brand-new permission token as a single-output PushDrop script in the relevant admin basket. * * The main difference between each type of token is in the "fields" we store in the PushDrop script. * * @param r The permission request * @param expiry The expiry epoch time * @param amount For DSAP, the authorized spending limit */ private createPermissionOnChain; /** * Renews a permission token by spending the old token as input and creating a new token output. * This invalidates the old token and replaces it with a new one. * * @param oldToken The old token to consume * @param r The permission request being renewed * @param newExpiry The new expiry epoch time * @param newAmount For DSAP, the new authorized amount */ private renewPermissionOnChain; /** * Builds the encrypted array of fields for a PushDrop permission token * (protocol / basket / certificate / spending). */ private buildPushdropFields; /** * Helper to build an array of tags for the new output, matching the user request's * origin, basket, privileged, protocol name, etc. */ private buildTagsForRequest; /** * Lists all protocol permission tokens (DPACP) with optional filters. * @param originator Optional originator domain to filter by * @param privileged Optional boolean to filter by privileged status * @param protocolName Optional protocol name to filter by * @param protocolSecurityLevel Optional protocol security level to filter by * @param counterparty Optional counterparty to filter by * @returns Array of permission tokens that match the filter criteria */ listProtocolPermissions({ originator, privileged, protocolName, protocolSecurityLevel, counterparty }?: { originator?: string; privileged?: boolean; protocolName?: string; protocolSecurityLevel?: number; counterparty?: string; }): Promise<PermissionToken[]>; /** * Returns true if the originator already holds a valid unexpired protocol permission. * This calls `ensureProtocolPermission` with `seekPermission=false`, so it won't prompt. */ hasProtocolPermission(params: { originator: string; privileged: boolean; protocolID: WalletProtocol; counterparty: string; }): Promise<boolean>; /** * Lists basket permission tokens (DBAP) for a given originator or basket (or for all if not specified). * @param params.originator Optional originator to filter by * @param params.basket Optional basket name to filter by * @returns Array of permission tokens that match the filter criteria */ listBasketAccess(params?: { originator?: string; basket?: string; }): Promise<PermissionToken[]>; /** * Returns `true` if the originator already holds a valid unexpired basket permission for `basket`. */ hasBasketAccess(params: { originator: string; basket: string; }): Promise<boolean>; /** * Lists spending authorization tokens (DSAP) for a given originator (or all). */ listSpendingAuthorizations(params: { originator?: string; }): Promise<PermissionToken[]>; /** * Returns `true` if the originator already holds a valid spending authorization token * with enough available monthly spend. We do not prompt (seekPermission=false). */ hasSpendingAuthorization(params: { originator: string; satoshis: number; }): Promise<boolean>; /** * Lists certificate permission tokens (DCAP) with optional filters. * @param originator Optional originator domain to filter by * @param privileged Optional boolean to filter by privileged status * @param certType Optional certificate type to filter by * @param verifier Optional verifier to filter by * @returns Array of permission tokens that match the filter criteria */ listCertificateAccess(params?: { originator?: string; privileged?: boolean; certType?: Base64String; verifier?: PubKeyHex; }): Promise<PermissionToken[]>; /** * Returns `true` if the originator already holds a valid unexpired certificate access * for the given certType/fields. Does not prompt the user. */ hasCertificateAccess(params: { originator: string; privileged: boolean; verifier: string; certType: string; fields: string[]; }): Promise<boolean>; /** * Revokes a permission token by spending it with no replacement output. * The manager builds a BRC-100 transaction that consumes the token, effectively invalidating it. */ revokePermission(oldToken: PermissionToken): Promise<void>; createAction(args: Parameters<WalletInterface['createAction']>[0], originator?: string): ReturnType<WalletInterface['createAction']>; signAction(...args: Parameters<WalletInterface['signAction']>): ReturnType<WalletInterface['signAction']>; abortAction(...args: Parameters<WalletInterface['abortAction']>): ReturnType<WalletInterface['abortAction']>; listActions(...args: Parameters<WalletInterface['listActions']>): ReturnType<WalletInterface['listActions']>; internalizeAction(...args: Parameters<WalletInterface['internalizeAction']>): ReturnType<WalletInterface['internalizeAction']>; listOutputs(...args: Parameters<WalletInterface['listOutputs']>): ReturnType<WalletInterface['listOutputs']>; relinquishOutput(...args: Parameters<WalletInterface['relinquishOutput']>): ReturnType<WalletInterface['relinquishOutput']>; getPublicKey(...args: Parameters<WalletInterface['getPublicKey']>): ReturnType<WalletInterface['getPublicKey']>; revealCounterpartyKeyLinkage(...args: Parameters<WalletInterface['revealCounterpartyKeyLinkage']>): ReturnType<WalletInterface['revealCounterpartyKeyLinkage']>; revealSpecificKeyLinkage(...args: Parameters<WalletInterface['revealSpecificKeyLinkage']>): ReturnType<WalletInterface['revealSpecificKeyLinkage']>; encrypt(...args: Parameters<WalletInterface['encrypt']>): ReturnType<WalletInterface['encrypt']>; decrypt(...args: Parameters<WalletInterface['decrypt']>): ReturnType<WalletInterface['decrypt']>; createHmac(...args: Parameters<WalletInterface['createHmac']>): ReturnType<WalletInterface['createHmac']>; verifyHmac(...args: Parameters<WalletInterface['verifyHmac']>): ReturnType<WalletInterface['verifyHmac']>; createSignature(...args: Parameters<WalletInterface['createSignature']>): ReturnType<WalletInterface['createSignature']>; verifySignature(...args: Parameters<WalletInterface['verifySignature']>): ReturnType<WalletInterface['verifySignature']>; acquireCertificate(...args: Parameters<WalletInterface['acquireCertificate']>): ReturnType<WalletInterface['acquireCertificate']>; listCertificates(...args: Parameters<WalletInterface['listCertificates']>): ReturnType<WalletInterface['listCertificates']>; proveCertificate(...args: Parameters<WalletInterface['proveCertificate']>): ReturnType<WalletInterface['proveCertificate']>; relinquishCertificate(...args: Parameters<WalletInterface['relinquishCertificate']>): ReturnType<WalletInterface['relinquishCertificate']>; discoverByIdentityKey(...args: Parameters<WalletInterface['discoverByIdentityKey']>): ReturnType<WalletInterface['discoverByIdentityKey']>; discoverByAttributes(...args: Parameters<WalletInterface['discoverByAttributes']>): ReturnType<WalletInterface['discoverByAttributes']>; isAuthenticated(...args: Parameters<WalletInterface['isAuthenticated']>): ReturnType<WalletInterface['isAuthenticated']>; waitForAuthentication(...args: Parameters<WalletInterface['waitForAuthentication']>): ReturnType<WalletInterface['waitForAuthentication']>; getHeight(...args: Parameters<WalletInterface['getHeight']>): ReturnType<WalletInterface['getHeight']>; getHeaderForHeight(...args: Parameters<WalletInterface['getHeaderForHeight']>): ReturnType<WalletInterface['getHeaderForHeight']>; getNetwork(...args: Parameters<WalletInterface['getNetwork']>): ReturnType<WalletInterface['getNetwork']>; getVersion(...args: Parameters<WalletInterface['getVersion']>): ReturnType<WalletInterface['getVersion']>; /** Returns true if the specified origin is the admin originator. */ private isAdminOriginator; /** * Checks if the given protocol is admin-reserved per BRC-100 rules: * * - Must not start with `admin` (admin-reserved) * - Must not start with `p ` (allows for future specially permissioned protocols) * * If it violates these rules and the caller is not admin, we consider it "admin-only." */ private isAdminProtocol; /** * Checks if the given label is admin-reserved per BRC-100 rules: * * - Must not start with `admin` (admin-reserved) * * If it violates these rules and the caller is not admin, we consider it "admin-only." */ private isAdminLabel; /** * Checks if the given basket is admin-reserved per BRC-100 rules: * * - Must not start with `admin` * - Must not be `default` (some wallets use this for internal operations) * - Must not start with `p ` (future specially permissioned baskets) */ private isAdminBasket; /** * Returns true if we have a cached record that the permission identified by * `key` is valid and unexpired. */ private isPermissionCached; /** Caches the fact that the permission for `key` is valid until `expiry`. */ private cachePermission; /** * Builds a "map key" string so that identical requests (e.g. "protocol:domain:true:protoName:counterparty") * do not produce multiple user prompts. */ private buildRequestKey; } //# sourceMappingURL=WalletPermissionsManager.d.ts.map