UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

301 lines (260 loc) 11.3 kB
import { mockUnderlyingWallet, MockedBSV_SDK } from './WalletPermissionsManager.fixtures' import { WalletPermissionsManager, PermissionsManagerConfig } from '../WalletPermissionsManager' jest.mock('@bsv/sdk', () => MockedBSV_SDK) describe('WalletPermissionsManager - Initialization & Configuration', () => { let underlying: jest.Mocked<any> beforeEach(() => { // Create a fresh underlying mock wallet before each test underlying = mockUnderlyingWallet() }) afterEach(() => { jest.clearAllMocks() }) it('should initialize with default config if none is provided', () => { const manager = new WalletPermissionsManager(underlying, 'admin.domain.com') // The manager internally defaults all config flags to true. const internalConfig = (manager as any).config as PermissionsManagerConfig expect(internalConfig.seekProtocolPermissionsForSigning).toBe(true) expect(internalConfig.seekProtocolPermissionsForEncrypting).toBe(true) expect(internalConfig.seekPermissionsForIdentityKeyRevelation).toBe(true) expect(internalConfig.encryptWalletMetadata).toBe(true) // The manager should store the admin originator const admin = (manager as any).adminOriginator expect(admin).toBe('admin.domain.com') }) it('should initialize with partial config overrides, merging with defaults', () => { const partialConfig: PermissionsManagerConfig = { seekProtocolPermissionsForSigning: false, encryptWalletMetadata: false // The rest remain default = true } const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', partialConfig) const internalConfig = (manager as any).config // Overridden to false expect(internalConfig.seekProtocolPermissionsForSigning).toBe(false) expect(internalConfig.encryptWalletMetadata).toBe(false) // Remaining defaults still true expect(internalConfig.seekBasketInsertionPermissions).toBe(true) expect(internalConfig.seekSpendingPermissions).toBe(true) }) it('should initialize with all config flags set to false', () => { const allFalse: PermissionsManagerConfig = { seekProtocolPermissionsForSigning: false, seekProtocolPermissionsForEncrypting: false, seekProtocolPermissionsForHMAC: false, seekPermissionsForKeyLinkageRevelation: false, seekPermissionsForPublicKeyRevelation: false, seekPermissionsForIdentityKeyRevelation: false, seekPermissionsForIdentityResolution: false, seekBasketInsertionPermissions: false, seekBasketRemovalPermissions: false, seekBasketListingPermissions: false, seekPermissionWhenApplyingActionLabels: false, seekPermissionWhenListingActionsByLabel: false, seekCertificateDisclosurePermissions: false, seekCertificateAcquisitionPermissions: false, seekCertificateRelinquishmentPermissions: false, seekCertificateListingPermissions: false, encryptWalletMetadata: false, seekSpendingPermissions: false, differentiatePrivilegedOperations: false } const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', allFalse) const internalConfig = (manager as any).config for (const [k, v] of Object.entries(allFalse)) { expect(internalConfig[k]).toBe(v) } }) it('should consider calls from the adminOriginator as admin, bypassing checks', async () => { const manager = new WalletPermissionsManager(underlying, 'admin.domain.com') // If we call a method that normally triggers permission checks (like createAction with a basket), // but pass in originator="admin.domain.com", we expect NO permission prompt or error. // We'll do a minimal createAction call. const result = await manager.createAction( { description: 'Insertion to user basket', outputs: [ { lockingScript: 'abcd', satoshis: 1000, outputDescription: 'some out desc', basket: 'some-user-basket' } ] }, 'admin.domain.com' ) // If the manager truly bypassed checks for the admin, it won't queue a request // nor throw an error. The call should just succeed. expect(result).toBeDefined() // Confirm the underlying createAction was actually called expect(underlying.createAction).toHaveBeenCalledTimes(1) // activeRequests map should be empty const activeRequests = (manager as any).activeRequests as Map<string, any[]> expect(activeRequests.size).toBe(0) }) it('should skip protocol permission checks for signing if seekProtocolPermissionsForSigning=false', async () => { const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', { seekProtocolPermissionsForSigning: false }) // Non-admin origin attempts "createSignature" with a protocolID // Normally, if config was true, we'd expect a request for permission. // But here we expect it to skip and proceed. await expect( manager.createSignature( { protocolID: [1, 'some-protocol'], privileged: false, data: [0x01, 0x02], keyID: '1' }, 'app.nonadmin.com' ) ).resolves.not.toThrow() // underlying createSignature is invoked expect(underlying.createSignature).toHaveBeenCalledTimes(1) // The manager’s internal request queue should remain empty const activeRequests = (manager as any).activeRequests as Map<string, any[]> expect(activeRequests.size).toBe(0) }) it('should enforce protocol permission checks for signing if seekProtocolPermissionsForSigning=true', async () => { // By default, or explicitly set to true, the manager enforces permission checks const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', { seekProtocolPermissionsForSigning: true }) // Non-admin origin tries createSignature -> must prompt for protocol permission const createSigPromise = manager.createSignature( { protocolID: [1, 'test-protocol'], keyID: '1', data: [0x10, 0x20], privileged: false }, 'nonadmin.com' ) // The manager triggers a request. Let's see if the request queue has an entry: const activeRequests = (manager as any).activeRequests as Map<string, any> // We may not see an entry synchronously because `ensureProtocolPermission()` is async, // but once the promise gets to that stage, it populates the queue. // Wait a short tick to let the async code run await new Promise(res => setTimeout(res, 10)) expect(activeRequests.size).toBeGreaterThan(0) // We'll forcibly deny the request so the test can conclude: const firstRequestKey = Array.from(activeRequests.keys())[0] const firstRequestQueue = activeRequests.get(firstRequestKey) if (firstRequestQueue && firstRequestQueue.pending.length > 0) { manager.denyPermission(firstRequestKey) } // The promise eventually rejects with "Permission denied." await expect(createSigPromise).rejects.toThrow(/Permission denied/) }) it('should skip basket insertion permission checks if seekBasketInsertionPermissions=false', async () => { const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', { seekBasketInsertionPermissions: false }) // Spending authorization is still required, grant it. manager.bindCallback( 'onSpendingAuthorizationRequested', jest.fn(x => { manager.grantPermission({ requestID: x.requestID, ephemeral: true }) }) as any ) // Non-admin origin tries to createAction specifying a basket await expect( manager.createAction( { description: 'Insert to user basket', outputs: [ { lockingScript: '1234', satoshis: 888, basket: 'somebasket', outputDescription: 'some out desc' } ] }, 'some-user.com' ) ).resolves.not.toThrow() // Because insertion checks are disabled, no permission request should be queued const activeRequests = (manager as any).activeRequests as Map<string, any> expect(activeRequests.size).toBe(0) }) it('should skip all permission checks if all relevant config flags are false (except admin-only baskets, etc.)', async () => { const manager = new WalletPermissionsManager(underlying, 'admin.domain.com', { // Only turning off relevant categories, i.e. we might set all false except we keep // differentiatePrivilegedOperations at default just to verify. Or set it to false as well. seekProtocolPermissionsForSigning: false, seekProtocolPermissionsForEncrypting: false, seekProtocolPermissionsForHMAC: false, seekPermissionsForKeyLinkageRevelation: false, seekPermissionsForPublicKeyRevelation: false, seekPermissionsForIdentityKeyRevelation: false, seekPermissionsForIdentityResolution: false, seekBasketInsertionPermissions: false, seekBasketRemovalPermissions: false, seekBasketListingPermissions: false, seekPermissionWhenApplyingActionLabels: false, seekPermissionWhenListingActionsByLabel: false, seekCertificateDisclosurePermissions: false, seekCertificateAcquisitionPermissions: false, seekCertificateRelinquishmentPermissions: false, seekCertificateListingPermissions: false, encryptWalletMetadata: false, seekSpendingPermissions: false, differentiatePrivilegedOperations: false }) // We'll do a few calls that would normally require checks: // 1) createSignature from non-admin await expect( manager.createSignature({ protocolID: [1, 'some-protocol'], data: [0x01], keyID: '1' }, 'nonadmin.com') ).resolves.not.toThrow() // 2) createAction to insert in a basket await expect( manager.createAction( { description: 'Inserting stuff', outputs: [ { lockingScript: '012345', satoshis: 1, basket: 'user-basket', outputDescription: 'some out desc' } ] }, 'nonadmin.com' ) ).resolves.not.toThrow() // 3) Acquire certificate await expect( manager.acquireCertificate( { type: 'base64-cert-type', certifier: '02abc...', acquisitionProtocol: 'direct', fields: { name: 'Bob' } }, 'nonadmin.com' ) ).resolves.not.toThrow() // Confirm no queued requests const activeRequests = (manager as any).activeRequests as Map<string, any[]> expect(activeRequests.size).toBe(0) }) it('should block usage of an admin-only protocol name if not called by admin', async () => { const manager = new WalletPermissionsManager(underlying, 'admin.domain.com') // A protocol name that starts with "admin" await expect( manager.createSignature( { protocolID: [1, 'admin super-secret-protocol'], data: [1, 2, 3], keyID: '1', privileged: false }, 'nonadmin.com' ) ).rejects.toThrow(/admin-only/i) }) })