@bsv/wallet-toolbox 1.1.24 → 1.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/docs/client.md +2319 -84
  2. package/docs/wallet.md +2319 -84
  3. package/out/src/CWIStyleWalletManager.d.ts +411 -0
  4. package/out/src/CWIStyleWalletManager.d.ts.map +1 -0
  5. package/out/src/CWIStyleWalletManager.js +1131 -0
  6. package/out/src/CWIStyleWalletManager.js.map +1 -0
  7. package/out/src/SetupClient.d.ts +249 -0
  8. package/out/src/SetupClient.d.ts.map +1 -0
  9. package/out/src/SetupClient.js +252 -0
  10. package/out/src/SetupClient.js.map +1 -0
  11. package/out/src/SimpleWalletManager.d.ts +169 -0
  12. package/out/src/SimpleWalletManager.d.ts.map +1 -0
  13. package/out/src/SimpleWalletManager.js +315 -0
  14. package/out/src/SimpleWalletManager.js.map +1 -0
  15. package/out/src/Wallet.d.ts +6 -1
  16. package/out/src/Wallet.d.ts.map +1 -1
  17. package/out/src/Wallet.js +29 -2
  18. package/out/src/Wallet.js.map +1 -1
  19. package/out/src/WalletAuthenticationManager.d.ts +33 -0
  20. package/out/src/WalletAuthenticationManager.d.ts.map +1 -0
  21. package/out/src/WalletAuthenticationManager.js +107 -0
  22. package/out/src/WalletAuthenticationManager.js.map +1 -0
  23. package/out/src/WalletPermissionsManager.d.ts +575 -0
  24. package/out/src/WalletPermissionsManager.d.ts.map +1 -0
  25. package/out/src/WalletPermissionsManager.js +1807 -0
  26. package/out/src/WalletPermissionsManager.js.map +1 -0
  27. package/out/src/WalletSettingsManager.d.ts +59 -0
  28. package/out/src/WalletSettingsManager.d.ts.map +1 -0
  29. package/out/src/WalletSettingsManager.js +168 -0
  30. package/out/src/WalletSettingsManager.js.map +1 -0
  31. package/out/src/__tests/CWIStyleWalletManager.test.d.ts +2 -0
  32. package/out/src/__tests/CWIStyleWalletManager.test.d.ts.map +1 -0
  33. package/out/src/__tests/CWIStyleWalletManager.test.js +472 -0
  34. package/out/src/__tests/CWIStyleWalletManager.test.js.map +1 -0
  35. package/out/src/__tests/WalletPermissionsManager.callbacks.test.d.ts +2 -0
  36. package/out/src/__tests/WalletPermissionsManager.callbacks.test.d.ts.map +1 -0
  37. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js +239 -0
  38. package/out/src/__tests/WalletPermissionsManager.callbacks.test.js.map +1 -0
  39. package/out/src/__tests/WalletPermissionsManager.checks.test.d.ts +2 -0
  40. package/out/src/__tests/WalletPermissionsManager.checks.test.d.ts.map +1 -0
  41. package/out/src/__tests/WalletPermissionsManager.checks.test.js +644 -0
  42. package/out/src/__tests/WalletPermissionsManager.checks.test.js.map +1 -0
  43. package/out/src/__tests/WalletPermissionsManager.encryption.test.d.ts +2 -0
  44. package/out/src/__tests/WalletPermissionsManager.encryption.test.d.ts.map +1 -0
  45. package/out/src/__tests/WalletPermissionsManager.encryption.test.js +295 -0
  46. package/out/src/__tests/WalletPermissionsManager.encryption.test.js.map +1 -0
  47. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts +82 -0
  48. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts.map +1 -0
  49. package/out/src/__tests/WalletPermissionsManager.fixtures.js +260 -0
  50. package/out/src/__tests/WalletPermissionsManager.fixtures.js.map +1 -0
  51. package/out/src/__tests/WalletPermissionsManager.flows.test.d.ts +2 -0
  52. package/out/src/__tests/WalletPermissionsManager.flows.test.d.ts.map +1 -0
  53. package/out/src/__tests/WalletPermissionsManager.flows.test.js +389 -0
  54. package/out/src/__tests/WalletPermissionsManager.flows.test.js.map +1 -0
  55. package/out/src/__tests/WalletPermissionsManager.initialization.test.d.ts +2 -0
  56. package/out/src/__tests/WalletPermissionsManager.initialization.test.d.ts.map +1 -0
  57. package/out/src/__tests/WalletPermissionsManager.initialization.test.js +227 -0
  58. package/out/src/__tests/WalletPermissionsManager.initialization.test.js.map +1 -0
  59. package/out/src/__tests/WalletPermissionsManager.proxying.test.d.ts +2 -0
  60. package/out/src/__tests/WalletPermissionsManager.proxying.test.d.ts.map +1 -0
  61. package/out/src/__tests/WalletPermissionsManager.proxying.test.js +566 -0
  62. package/out/src/__tests/WalletPermissionsManager.proxying.test.js.map +1 -0
  63. package/out/src/__tests/WalletPermissionsManager.tokens.test.d.ts +2 -0
  64. package/out/src/__tests/WalletPermissionsManager.tokens.test.d.ts.map +1 -0
  65. package/out/src/__tests/WalletPermissionsManager.tokens.test.js +460 -0
  66. package/out/src/__tests/WalletPermissionsManager.tokens.test.js.map +1 -0
  67. package/out/src/index.all.d.ts +9 -0
  68. package/out/src/index.all.d.ts.map +1 -1
  69. package/out/src/index.all.js +9 -0
  70. package/out/src/index.all.js.map +1 -1
  71. package/out/src/index.client.d.ts +9 -0
  72. package/out/src/index.client.d.ts.map +1 -1
  73. package/out/src/index.client.js +9 -0
  74. package/out/src/index.client.js.map +1 -1
  75. package/out/src/utility/identityUtils.d.ts +31 -0
  76. package/out/src/utility/identityUtils.d.ts.map +1 -0
  77. package/out/src/utility/identityUtils.js +114 -0
  78. package/out/src/utility/identityUtils.js.map +1 -0
  79. package/out/src/wab-client/WABClient.d.ts +38 -0
  80. package/out/src/wab-client/WABClient.d.ts.map +1 -0
  81. package/out/src/wab-client/WABClient.js +95 -0
  82. package/out/src/wab-client/WABClient.js.map +1 -0
  83. package/out/src/wab-client/__tests/WABClient.test.d.ts +2 -0
  84. package/out/src/wab-client/__tests/WABClient.test.d.ts.map +1 -0
  85. package/out/src/wab-client/__tests/WABClient.test.js +47 -0
  86. package/out/src/wab-client/__tests/WABClient.test.js.map +1 -0
  87. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts +34 -0
  88. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.d.ts.map +1 -0
  89. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js +16 -0
  90. package/out/src/wab-client/auth-method-interactors/AuthMethodInteractor.js.map +1 -0
  91. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.d.ts +7 -0
  92. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.d.ts.map +1 -0
  93. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.js +40 -0
  94. package/out/src/wab-client/auth-method-interactors/PersonaIDInteractor.js.map +1 -0
  95. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts +28 -0
  96. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.d.ts.map +1 -0
  97. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js +73 -0
  98. package/out/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.js.map +1 -0
  99. package/out/test/Wallet/action/abortAction.test.d.ts.map +1 -0
  100. package/out/test/{wallet → Wallet}/action/abortAction.test.js.map +1 -1
  101. package/out/test/Wallet/action/createAction.test.d.ts.map +1 -0
  102. package/out/test/{wallet → Wallet}/action/createAction.test.js.map +1 -1
  103. package/out/test/{wallet → Wallet}/action/createAction2.test.d.ts.map +1 -1
  104. package/out/test/{wallet → Wallet}/action/createAction2.test.js.map +1 -1
  105. package/out/test/Wallet/action/createActionToGenerateBeefs.man.test.d.ts.map +1 -0
  106. package/out/test/{wallet → Wallet}/action/createActionToGenerateBeefs.man.test.js.map +1 -1
  107. package/out/test/Wallet/action/internalizeAction.test.d.ts.map +1 -0
  108. package/out/test/{wallet → Wallet}/action/internalizeAction.test.js.map +1 -1
  109. package/out/test/Wallet/action/relinquishOutput.test.d.ts.map +1 -0
  110. package/out/test/{wallet → Wallet}/action/relinquishOutput.test.js.map +1 -1
  111. package/out/test/Wallet/construct/Wallet.constructor.test.d.ts.map +1 -0
  112. package/out/test/{wallet → Wallet}/construct/Wallet.constructor.test.js.map +1 -1
  113. package/out/test/Wallet/list/listActions.test.d.ts.map +1 -0
  114. package/out/test/{wallet → Wallet}/list/listActions.test.js.map +1 -1
  115. package/out/test/Wallet/list/listActions2.test.d.ts.map +1 -0
  116. package/out/test/{wallet → Wallet}/list/listActions2.test.js.map +1 -1
  117. package/out/test/Wallet/list/listCertificates.test.d.ts.map +1 -0
  118. package/out/test/{wallet → Wallet}/list/listCertificates.test.js.map +1 -1
  119. package/out/test/Wallet/list/listOutputs.test.d.ts.map +1 -0
  120. package/out/test/{wallet → Wallet}/list/listOutputs.test.js.map +1 -1
  121. package/out/test/Wallet/sync/Wallet.sync.test.d.ts.map +1 -0
  122. package/out/test/{wallet → Wallet}/sync/Wallet.sync.test.js.map +1 -1
  123. package/out/tsconfig.all.tsbuildinfo +1 -1
  124. package/package.json +3 -3
  125. package/src/CWIStyleWalletManager.ts +1891 -0
  126. package/src/SimpleWalletManager.ts +553 -0
  127. package/src/Wallet.ts +47 -3
  128. package/src/WalletAuthenticationManager.ts +183 -0
  129. package/src/WalletPermissionsManager.ts +2639 -0
  130. package/src/WalletSettingsManager.ts +241 -0
  131. package/src/__tests/CWIStyleWalletManager.test.ts +709 -0
  132. package/src/__tests/WalletPermissionsManager.callbacks.test.ts +328 -0
  133. package/src/__tests/WalletPermissionsManager.checks.test.ts +857 -0
  134. package/src/__tests/WalletPermissionsManager.encryption.test.ts +407 -0
  135. package/src/__tests/WalletPermissionsManager.fixtures.ts +283 -0
  136. package/src/__tests/WalletPermissionsManager.flows.test.ts +490 -0
  137. package/src/__tests/WalletPermissionsManager.initialization.test.ts +333 -0
  138. package/src/__tests/WalletPermissionsManager.proxying.test.ts +753 -0
  139. package/src/__tests/WalletPermissionsManager.tokens.test.ts +584 -0
  140. package/src/index.all.ts +9 -0
  141. package/src/index.client.ts +9 -0
  142. package/src/utility/identityUtils.ts +170 -0
  143. package/src/wab-client/WABClient.ts +103 -0
  144. package/src/wab-client/__tests/WABClient.test.ts +58 -0
  145. package/src/wab-client/auth-method-interactors/AuthMethodInteractor.ts +47 -0
  146. package/src/wab-client/auth-method-interactors/PersonaIDInteractor.ts +45 -0
  147. package/src/wab-client/auth-method-interactors/TwilioPhoneInteractor.ts +82 -0
  148. package/out/test/wallet/action/abortAction.test.d.ts.map +0 -1
  149. package/out/test/wallet/action/createAction.test.d.ts.map +0 -1
  150. package/out/test/wallet/action/createActionToGenerateBeefs.man.test.d.ts.map +0 -1
  151. package/out/test/wallet/action/internalizeAction.test.d.ts.map +0 -1
  152. package/out/test/wallet/action/relinquishOutput.test.d.ts.map +0 -1
  153. package/out/test/wallet/construct/Wallet.constructor.test.d.ts.map +0 -1
  154. package/out/test/wallet/list/listActions.test.d.ts.map +0 -1
  155. package/out/test/wallet/list/listActions2.test.d.ts.map +0 -1
  156. package/out/test/wallet/list/listCertificates.test.d.ts.map +0 -1
  157. package/out/test/wallet/list/listOutputs.test.d.ts.map +0 -1
  158. package/out/test/wallet/sync/Wallet.sync.test.d.ts.map +0 -1
  159. /package/out/test/{wallet → Wallet}/action/abortAction.test.d.ts +0 -0
  160. /package/out/test/{wallet → Wallet}/action/abortAction.test.js +0 -0
  161. /package/out/test/{wallet → Wallet}/action/createAction.test.d.ts +0 -0
  162. /package/out/test/{wallet → Wallet}/action/createAction.test.js +0 -0
  163. /package/out/test/{wallet → Wallet}/action/createAction2.test.d.ts +0 -0
  164. /package/out/test/{wallet → Wallet}/action/createAction2.test.js +0 -0
  165. /package/out/test/{wallet → Wallet}/action/createActionToGenerateBeefs.man.test.d.ts +0 -0
  166. /package/out/test/{wallet → Wallet}/action/createActionToGenerateBeefs.man.test.js +0 -0
  167. /package/out/test/{wallet → Wallet}/action/internalizeAction.test.d.ts +0 -0
  168. /package/out/test/{wallet → Wallet}/action/internalizeAction.test.js +0 -0
  169. /package/out/test/{wallet → Wallet}/action/relinquishOutput.test.d.ts +0 -0
  170. /package/out/test/{wallet → Wallet}/action/relinquishOutput.test.js +0 -0
  171. /package/out/test/{wallet → Wallet}/construct/Wallet.constructor.test.d.ts +0 -0
  172. /package/out/test/{wallet → Wallet}/construct/Wallet.constructor.test.js +0 -0
  173. /package/out/test/{wallet → Wallet}/list/listActions.test.d.ts +0 -0
  174. /package/out/test/{wallet → Wallet}/list/listActions.test.js +0 -0
  175. /package/out/test/{wallet → Wallet}/list/listActions2.test.d.ts +0 -0
  176. /package/out/test/{wallet → Wallet}/list/listActions2.test.js +0 -0
  177. /package/out/test/{wallet → Wallet}/list/listCertificates.test.d.ts +0 -0
  178. /package/out/test/{wallet → Wallet}/list/listCertificates.test.js +0 -0
  179. /package/out/test/{wallet → Wallet}/list/listOutputs.test.d.ts +0 -0
  180. /package/out/test/{wallet → Wallet}/list/listOutputs.test.js +0 -0
  181. /package/out/test/{wallet → Wallet}/sync/Wallet.sync.test.d.ts +0 -0
  182. /package/out/test/{wallet → Wallet}/sync/Wallet.sync.test.js +0 -0
  183. /package/test/{wallet → Wallet}/action/abortAction.test.ts +0 -0
  184. /package/test/{wallet → Wallet}/action/createAction.test.ts +0 -0
  185. /package/test/{wallet → Wallet}/action/createAction2.test.ts +0 -0
  186. /package/test/{wallet → Wallet}/action/createActionToGenerateBeefs.man.test.ts +0 -0
  187. /package/test/{wallet → Wallet}/action/internalizeAction.test.ts +0 -0
  188. /package/test/{wallet → Wallet}/action/relinquishOutput.test.ts +0 -0
  189. /package/test/{wallet → Wallet}/construct/Wallet.constructor.test.ts +0 -0
  190. /package/test/{wallet → Wallet}/list/listActions.test.ts +0 -0
  191. /package/test/{wallet → Wallet}/list/listActions2.test.ts +0 -0
  192. /package/test/{wallet → Wallet}/list/listCertificates.test.ts +0 -0
  193. /package/test/{wallet → Wallet}/list/listOutputs.test.ts +0 -0
  194. /package/test/{wallet → Wallet}/sync/Wallet.sync.test.ts +0 -0
@@ -0,0 +1,857 @@
1
+ import {
2
+ mockUnderlyingWallet,
3
+ MockedBSV_SDK
4
+ } from './WalletPermissionsManager.fixtures'
5
+ import {
6
+ WalletPermissionsManager,
7
+ PermissionToken
8
+ } from '../WalletPermissionsManager'
9
+
10
+ jest.mock('@bsv/sdk', () => MockedBSV_SDK)
11
+
12
+ describe('WalletPermissionsManager - Permission Checks', () => {
13
+ let underlying: jest.Mocked<any>
14
+ let manager: WalletPermissionsManager
15
+
16
+ beforeEach(() => {
17
+ // Fresh mock wallet before each test
18
+ underlying = mockUnderlyingWallet() as jest.Mocked<any>
19
+ })
20
+
21
+ afterEach(() => {
22
+ jest.clearAllMocks()
23
+ })
24
+
25
+ /* ------------------------------------------------------
26
+ * 5) PROTOCOL USAGE (DPACP) TESTS
27
+ * ------------------------------------------------------ */
28
+ describe('Protocol Usage (DPACP)', () => {
29
+ it('should skip permission prompt if secLevel=0 (open usage)', async () => {
30
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
31
+ seekProtocolPermissionsForSigning: true // Typically enforced
32
+ })
33
+
34
+ // Attempt createSignature with protocolID=[0, "someProtocol"]
35
+ // Because securityLevel=0, the manager should skip checks
36
+ await expect(
37
+ manager.createSignature(
38
+ {
39
+ protocolID: [0, 'open-protocol'],
40
+ data: [0x01, 0x02],
41
+ keyID: '1'
42
+ },
43
+ 'some-user.com'
44
+ )
45
+ ).resolves.not.toThrow()
46
+
47
+ // No permission request
48
+ const activeRequests = (manager as any).activeRequests as Map<string, any>
49
+ expect(activeRequests.size).toBe(0)
50
+
51
+ // Underlying createSignature called once
52
+ expect(underlying.createSignature).toHaveBeenCalledTimes(1)
53
+ })
54
+
55
+ it('should prompt for protocol usage if securityLevel=1 and no existing token', async () => {
56
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
57
+ seekProtocolPermissionsForSigning: true
58
+ })
59
+
60
+ // We'll bind a callback that grants ephemeral permission automatically
61
+ manager.bindCallback('onProtocolPermissionRequested', async request => {
62
+ // For tests, automatically grant ephemeral permission
63
+ await manager.grantPermission({
64
+ requestID: request.requestID,
65
+ ephemeral: true
66
+ })
67
+ })
68
+
69
+ // Because secLevel=1, we need a valid DPACP token
70
+ // We have no token => manager triggers a request => callback grants ephemeral => passes
71
+ await expect(
72
+ manager.createSignature(
73
+ {
74
+ protocolID: [1, 'test-protocol'],
75
+ data: [0x99, 0xaa],
76
+ keyID: '1'
77
+ },
78
+ 'some-nonadmin.com'
79
+ )
80
+ ).resolves.not.toThrow()
81
+
82
+ // The underlying signature should succeed
83
+ expect(underlying.createSignature).toHaveBeenCalledTimes(1)
84
+ })
85
+
86
+ it('should deny protocol usage if user denies permission', async () => {
87
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {})
88
+
89
+ // The callback denies the request
90
+ manager.bindCallback('onProtocolPermissionRequested', request => {
91
+ manager.denyPermission(request.requestID)
92
+ })
93
+
94
+ // Attempt an operation that requires protocol permission
95
+ await expect(
96
+ manager.encrypt(
97
+ {
98
+ protocolID: [1, 'needs-perm'],
99
+ plaintext: [1, 2, 3],
100
+ keyID: 'xyz'
101
+ },
102
+ 'external-app.com'
103
+ )
104
+ ).rejects.toThrow(/Permission denied/)
105
+
106
+ // Underlying encrypt was never called
107
+ expect(underlying.encrypt).toHaveBeenCalledTimes(0)
108
+ })
109
+
110
+ it('should enforce privileged token if differentiatePrivilegedOperations=true', async () => {
111
+ // By default, differentiatePrivilegedOperations is true.
112
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
113
+ seekProtocolPermissionsForSigning: true
114
+ })
115
+
116
+ manager.bindCallback('onProtocolPermissionRequested', async req => {
117
+ // The request has `privileged=true`, so the resulting token must also be privileged.
118
+ // We'll grant ephemeral to simulate success quickly.
119
+ await manager.grantPermission({
120
+ requestID: req.requestID,
121
+ ephemeral: true
122
+ })
123
+ })
124
+
125
+ // Attempt a privileged signature
126
+ await expect(
127
+ manager.createSignature(
128
+ {
129
+ protocolID: [1, 'high-level-crypto'],
130
+ privileged: true,
131
+ data: [0xc0, 0xff, 0xee],
132
+ keyID: '1'
133
+ },
134
+ 'nonadmin.app'
135
+ )
136
+ ).resolves.not.toThrow()
137
+
138
+ // Confirm underlying was ultimately called
139
+ expect(underlying.createSignature).toHaveBeenCalledTimes(1)
140
+ })
141
+
142
+ it('should ignore `privileged=true` if differentiatePrivilegedOperations=false', async () => {
143
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
144
+ differentiatePrivilegedOperations: false, // Forces privileged usage to be treated as non-privileged
145
+ seekProtocolPermissionsForSigning: true
146
+ })
147
+
148
+ // Because we treat privileged as false, the permission request does not need privileged credentials.
149
+ manager.bindCallback('onProtocolPermissionRequested', async req => {
150
+ await manager.grantPermission({
151
+ requestID: req.requestID,
152
+ ephemeral: true
153
+ })
154
+ })
155
+
156
+ await expect(
157
+ manager.createSignature(
158
+ {
159
+ protocolID: [1, 'some-protocol'],
160
+ privileged: true, // This flag will be ignored
161
+ data: [0x99],
162
+ keyID: 'keyXYZ'
163
+ },
164
+ 'nonadmin.com'
165
+ )
166
+ ).resolves.not.toThrow()
167
+ })
168
+
169
+ it('should fail if protocol name is admin-reserved and caller is not admin', async () => {
170
+ // admin-reserved means protocol name starts with "admin" or "p ".
171
+ manager = new WalletPermissionsManager(underlying, 'secure.admin.com')
172
+
173
+ // Non-admin tries to do e.g. `createHmac` with protocol name "admin super-secret"
174
+ await expect(
175
+ manager.createHmac(
176
+ {
177
+ protocolID: [1, 'admin super-secret'],
178
+ data: [0x01, 0x02],
179
+ keyID: '1'
180
+ },
181
+ 'not-an-admin.com'
182
+ )
183
+ ).rejects.toThrow(/admin-only/i)
184
+
185
+ // Underlying call never invoked
186
+ expect(underlying.createHmac).toHaveBeenCalledTimes(0)
187
+ })
188
+
189
+ it('should prompt for renewal if token is found but expired', async () => {
190
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {})
191
+
192
+ // Suppose the user already had a token but it’s expired. We mock `findProtocolToken` so that
193
+ // it returns an expired token, forcing a renewal request.
194
+ const expiredToken: PermissionToken = {
195
+ txid: 'oldtxid123',
196
+ outputIndex: 0,
197
+ outputScript: 'deadbeef',
198
+ satoshis: 1,
199
+ originator: 'some-nonadmin.com',
200
+ expiry: 1, // definitely in the past
201
+ privileged: false,
202
+ securityLevel: 1,
203
+ protocol: 'test-protocol',
204
+ counterparty: 'self'
205
+ }
206
+ jest
207
+ .spyOn(manager as any, 'findProtocolToken')
208
+ .mockResolvedValue(expiredToken)
209
+
210
+ // We'll bind a callback that grants a renewal ephemeral
211
+ manager.bindCallback('onProtocolPermissionRequested', async req => {
212
+ expect(req.renewal).toBe(true)
213
+ expect(req.previousToken).toEqual(expiredToken)
214
+ await manager.grantPermission({
215
+ requestID: req.requestID,
216
+ ephemeral: true
217
+ })
218
+ })
219
+
220
+ // Now call an operation that requires protocol usage
221
+ await manager.createSignature(
222
+ {
223
+ protocolID: [1, 'test-protocol'],
224
+ data: [0xfe],
225
+ keyID: '1'
226
+ },
227
+ 'some-nonadmin.com'
228
+ )
229
+ // Should succeed after renewal
230
+ expect(underlying.createSignature).toHaveBeenCalledTimes(1)
231
+ })
232
+ })
233
+
234
+ /* ------------------------------------------------------
235
+ * 6) BASKET USAGE (DBAP) TESTS
236
+ * ------------------------------------------------------ */
237
+ describe('Basket Usage (DBAP)', () => {
238
+ it('should fail immediately if using an admin-only basket as non-admin', async () => {
239
+ manager = new WalletPermissionsManager(underlying, 'admin.com')
240
+ // Attempt to createAction to insert into "admin secret-basket" from a non-admin origin
241
+ await expect(
242
+ manager.createAction(
243
+ {
244
+ description: 'Insert into admin basket',
245
+ outputs: [
246
+ {
247
+ lockingScript: 'abcd',
248
+ satoshis: 100,
249
+ basket: 'admin secret-basket',
250
+ outputDescription: 'Nothing to see here'
251
+ }
252
+ ]
253
+ },
254
+ 'non-admin.com'
255
+ )
256
+ ).rejects.toThrow(/admin-only/i)
257
+
258
+ // Underlying createAction never called
259
+ expect(underlying.createAction).toHaveBeenCalledTimes(0)
260
+ })
261
+
262
+ it('should fail immediately if using the reserved basket "default" as non-admin', async () => {
263
+ manager = new WalletPermissionsManager(underlying, 'admin.com')
264
+ await expect(
265
+ manager.createAction(
266
+ {
267
+ description: 'Insert to default basket',
268
+ outputs: [
269
+ {
270
+ lockingScript: '0x1234',
271
+ satoshis: 1,
272
+ basket: 'default',
273
+ outputDescription: 'Nothing to see here'
274
+ }
275
+ ]
276
+ },
277
+ 'some-nonadmin.com'
278
+ )
279
+ ).rejects.toThrow(/admin-only/i)
280
+ })
281
+
282
+ it('should prompt for insertion permission if seekBasketInsertionPermissions=true', async () => {
283
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
284
+ seekBasketInsertionPermissions: true
285
+ })
286
+
287
+ // auto-grant ephemeral
288
+ manager.bindCallback('onBasketAccessRequested', async req => {
289
+ await manager.grantPermission({
290
+ requestID: req.requestID,
291
+ ephemeral: true
292
+ })
293
+ })
294
+
295
+ // Also auto-grant unrelated spending authorization (since this is createAction)
296
+ manager.bindCallback('onSpendingAuthorizationRequested', async req => {
297
+ await manager.grantPermission({
298
+ requestID: req.requestID,
299
+ ephemeral: true
300
+ })
301
+ })
302
+
303
+ await expect(
304
+ manager.createAction(
305
+ {
306
+ description: 'Insert to user-basket',
307
+ outputs: [
308
+ {
309
+ lockingScript: 'op_return',
310
+ satoshis: 1,
311
+ basket: 'user-basket',
312
+ outputDescription: 'Nothing to see here'
313
+ }
314
+ ]
315
+ },
316
+ 'some-nonadmin.com'
317
+ )
318
+ ).resolves.not.toThrow()
319
+
320
+ // Confirm underlying createAction was eventually invoked
321
+ expect(underlying.createAction).toHaveBeenCalledTimes(1)
322
+ })
323
+
324
+ it('should skip insertion permission if seekBasketInsertionPermissions=false', async () => {
325
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
326
+ seekBasketInsertionPermissions: false
327
+ })
328
+
329
+ // Auto-grant unrelated spending authorization (since this is createAction)
330
+ manager.bindCallback('onSpendingAuthorizationRequested', async req => {
331
+ await manager.grantPermission({
332
+ requestID: req.requestID,
333
+ ephemeral: true
334
+ })
335
+ })
336
+
337
+ await manager.createAction(
338
+ {
339
+ description: 'Insert to user-basket',
340
+ outputs: [
341
+ {
342
+ lockingScript: 'op_return',
343
+ satoshis: 1,
344
+ basket: 'some-basket',
345
+ outputDescription: 'Nothing to see here'
346
+ }
347
+ ]
348
+ },
349
+ 'nonadmin.com'
350
+ )
351
+ // No requests queued, underlying is called
352
+ const activeRequests = (manager as any).activeRequests as Map<string, any>
353
+ expect(activeRequests.size).toBe(0)
354
+ expect(underlying.createAction).toHaveBeenCalledTimes(1)
355
+ })
356
+
357
+ it('should require listing permission if seekBasketListingPermissions=true and no token', async () => {
358
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
359
+ seekBasketListingPermissions: true
360
+ })
361
+
362
+ manager.bindCallback('onBasketAccessRequested', async req => {
363
+ // Deny for test
364
+ manager.denyPermission(req.requestID)
365
+ })
366
+
367
+ // Attempt to list a user basket
368
+ await expect(
369
+ manager.listOutputs({ basket: 'user-basket' }, 'some-user.com')
370
+ ).rejects.toThrow(/Permission denied/)
371
+
372
+ // There is one underlying call: internally, we called listOutputs to check if we had permission
373
+ // (we did not, we sought it, and the user denied). So we see this call here, but we DO NOT see
374
+ // the actual proxied call (for listing outputs in user-basket), since it was denied.
375
+ expect(underlying.listOutputs).toHaveBeenCalledTimes(1)
376
+ expect(underlying.listOutputs).toHaveBeenLastCalledWith(
377
+ {
378
+ basket: 'admin basket-access',
379
+ include: 'locking scripts',
380
+ tagQueryMode: 'all',
381
+ tags: ['originator some-user.com', 'basket user-basket']
382
+ },
383
+ 'admin.com'
384
+ )
385
+ })
386
+
387
+ it('should prompt for removal permission if seekBasketRemovalPermissions=true', async () => {
388
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
389
+ seekBasketRemovalPermissions: true
390
+ })
391
+ manager.bindCallback('onBasketAccessRequested', async req => {
392
+ // auto-grant ephemeral
393
+ await manager.grantPermission({
394
+ requestID: req.requestID,
395
+ ephemeral: true
396
+ })
397
+ })
398
+
399
+ await expect(
400
+ manager.relinquishOutput(
401
+ {
402
+ output: 'someTxid.1',
403
+ basket: 'user-basket'
404
+ },
405
+ 'some-user.com'
406
+ )
407
+ ).resolves.not.toThrow()
408
+
409
+ expect(underlying.relinquishOutput).toHaveBeenCalledTimes(1)
410
+ })
411
+ })
412
+
413
+ /* ------------------------------------------------------
414
+ * 7) CERTIFICATE USAGE (DCAP) TESTS
415
+ * ------------------------------------------------------ */
416
+ describe('Certificate Usage (DCAP)', () => {
417
+ it('should skip certificate disclosure permission if config.seekCertificateDisclosurePermissions=false', async () => {
418
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
419
+ seekCertificateDisclosurePermissions: false
420
+ })
421
+ // Directly call proveCertificate with no token => no prompt => immediate success
422
+ await expect(
423
+ manager.proveCertificate(
424
+ {
425
+ certificate: {
426
+ type: 'KYC',
427
+ subject: '02abcdef...',
428
+ serialNumber: '123',
429
+ certifier: '02ccc...',
430
+ fields: { name: 'Alice', dob: '2000-01-01' }
431
+ },
432
+ fieldsToReveal: ['name'],
433
+ verifier: '02xyz...',
434
+ privileged: false
435
+ },
436
+ 'nonadmin.com'
437
+ )
438
+ ).resolves.not.toThrow()
439
+
440
+ expect(underlying.proveCertificate).toHaveBeenCalledTimes(1)
441
+ })
442
+
443
+ it('should require permission if seekCertificateDisclosurePermissions=true, no valid token', async () => {
444
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
445
+ seekCertificateDisclosurePermissions: true
446
+ })
447
+
448
+ // Auto-grant ephemeral for test
449
+ manager.bindCallback('onCertificateAccessRequested', async req => {
450
+ await manager.grantPermission({
451
+ requestID: req.requestID,
452
+ ephemeral: true
453
+ })
454
+ })
455
+
456
+ // Because we don't have a stored token, it triggers request -> ephemeral granted -> success
457
+ await manager.proveCertificate(
458
+ {
459
+ certificate: {
460
+ type: 'KYC',
461
+ subject: '02abc..',
462
+ serialNumber: 'xyz',
463
+ certifier: '02dddd...',
464
+ fields: { name: 'Bob', nationality: 'Mars' }
465
+ },
466
+ fieldsToReveal: ['name'],
467
+ verifier: '02xxxx..',
468
+ privileged: false
469
+ },
470
+ 'some-user.com'
471
+ )
472
+
473
+ expect(underlying.proveCertificate).toHaveBeenCalledTimes(1)
474
+ })
475
+
476
+ it('should check that requested fields are a subset of the token’s fields', async () => {
477
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
478
+ seekCertificateDisclosurePermissions: true
479
+ })
480
+
481
+ // Suppose we find an existing token that covers fields: ['name', 'dob', 'nationality']
482
+ const existingToken: PermissionToken = {
483
+ txid: 'aabbcc',
484
+ outputIndex: 0,
485
+ outputScript: 'scriptHex',
486
+ satoshis: 1,
487
+ originator: 'some-user.com',
488
+ expiry: 9999999999, // not expired
489
+ privileged: false,
490
+ certType: 'KYC',
491
+ certFields: ['name', 'dob', 'nationality'],
492
+ verifier: '02eeee...'
493
+ }
494
+ jest
495
+ .spyOn(manager as any, 'findCertificateToken')
496
+ .mockImplementation(async (orig, priv, verif, ct, requestedFields) => {
497
+ // if requestedFields includes "someMissingField", return undefined
498
+ // else return the existingToken
499
+ if ((requestedFields as string[]).includes('someMissingField')) {
500
+ return undefined // forces a request
501
+ }
502
+ return existingToken // forces immediate success
503
+ })
504
+
505
+ // Attempt to prove certificate revealing only 'name' -> Should pass without prompt
506
+ await manager.proveCertificate(
507
+ {
508
+ certificate: {
509
+ type: 'KYC',
510
+ certifier: '02eeee...',
511
+ subject: '02some...',
512
+ serialNumber: '',
513
+ fields: { name: 'Charlie', dob: '1999-01-01', nationality: 'EU' }
514
+ },
515
+ fieldsToReveal: ['name'],
516
+ verifier: '02eeee...',
517
+ privileged: false
518
+ },
519
+ 'some-user.com'
520
+ )
521
+ expect(underlying.proveCertificate).toHaveBeenCalledTimes(1)
522
+
523
+ // Attempt to reveal a field the token does NOT cover -> triggers request
524
+ // Since the existing token does not cover 'someMissingField', we expect a prompt. Let’s deny it:
525
+ manager.bindCallback('onCertificateAccessRequested', async req => {
526
+ manager.denyPermission(req.requestID)
527
+ })
528
+ const secondAttempt = manager.proveCertificate(
529
+ {
530
+ certificate: {
531
+ type: 'KYC',
532
+ certifier: '02eeee...',
533
+ fields: { name: 'Charlie', dob: '1999-01-01', nationality: 'EU' }
534
+ },
535
+ fieldsToReveal: ['dob', 'someMissingField'],
536
+ verifier: '02eeee...',
537
+ privileged: false
538
+ },
539
+ 'some-user.com'
540
+ )
541
+ await expect(secondAttempt).rejects.toThrow(/Permission denied/)
542
+
543
+ // Underlying proveCertificate not called for second attempt
544
+ expect(underlying.proveCertificate).toHaveBeenCalledTimes(1)
545
+ })
546
+
547
+ it('should prompt for renewal if token is expired', async () => {
548
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
549
+ seekCertificateDisclosurePermissions: true
550
+ })
551
+
552
+ // Mock an expired token
553
+ const expiredCertToken: PermissionToken = {
554
+ txid: 'old-expired',
555
+ outputIndex: 0,
556
+ outputScript: 'deadbeef',
557
+ satoshis: 1,
558
+ originator: 'app.com',
559
+ expiry: 1,
560
+ privileged: false,
561
+ certType: 'KYC',
562
+ certFields: ['name', 'dob'],
563
+ verifier: '02verifier'
564
+ }
565
+ jest
566
+ .spyOn(manager as any, 'findCertificateToken')
567
+ .mockResolvedValue(expiredCertToken)
568
+
569
+ // Callback that grants renewal ephemeral
570
+ manager.bindCallback('onCertificateAccessRequested', async req => {
571
+ expect(req.renewal).toBe(true)
572
+ await manager.grantPermission({
573
+ requestID: req.requestID,
574
+ ephemeral: true
575
+ })
576
+ })
577
+
578
+ await manager.proveCertificate(
579
+ {
580
+ certificate: {
581
+ type: 'KYC',
582
+ fields: { name: 'Bob', dob: '1970' },
583
+ certifier: '02verifier'
584
+ },
585
+ fieldsToReveal: ['name'],
586
+ verifier: '02verifier',
587
+ privileged: false
588
+ },
589
+ 'app.com'
590
+ )
591
+ // Succeeds after ephemeral renewal
592
+ expect(underlying.proveCertificate).toHaveBeenCalledTimes(1)
593
+ })
594
+ })
595
+
596
+ /* ------------------------------------------------------
597
+ * 8) SPENDING AUTHORIZATION (DSAP) TESTS
598
+ * ------------------------------------------------------ */
599
+ describe('Spending Authorization (DSAP)', () => {
600
+ it('should skip if seekSpendingPermissions=false', async () => {
601
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
602
+ seekSpendingPermissions: false
603
+ })
604
+
605
+ // createAction that tries to net spend 200 sats
606
+ const result = await manager.createAction(
607
+ {
608
+ description: 'Some spend transaction',
609
+ outputs: [
610
+ {
611
+ lockingScript: 'script1',
612
+ satoshis: 200,
613
+ outputDescription: 'Nothing to see here'
614
+ }
615
+ ]
616
+ },
617
+ 'user.com'
618
+ )
619
+
620
+ // No prompt triggered
621
+ const activeRequests = (manager as any).activeRequests as Map<string, any>
622
+ expect(activeRequests.size).toBe(0)
623
+
624
+ // Underlying createAction definitely called
625
+ expect(underlying.createAction).toHaveBeenCalledTimes(1)
626
+ expect(result.signableTransaction).toBeDefined()
627
+ })
628
+
629
+ it('should require spending token if netSpent > 0 and seekSpendingPermissions=true', async () => {
630
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
631
+ seekSpendingPermissions: true
632
+ })
633
+
634
+ // We’ll also mock the signableTransaction return to help manager compute netSpent
635
+ underlying.createAction.mockResolvedValueOnce({
636
+ signableTransaction: {
637
+ tx: [0x00], // minimal
638
+ reference: 'ref1'
639
+ }
640
+ })
641
+ // The manager tries to parse the transaction to find netSpent.
642
+ // By default, netSpent = totalOutput + fee - totalExplicitInputs
643
+ // We haven't provided any explicit inputs in the createAction call, so netSpent = 200 + fee
644
+
645
+ // Auto-grant ephemeral for test
646
+ manager.bindCallback('onSpendingAuthorizationRequested', async req => {
647
+ await manager.grantPermission({
648
+ requestID: req.requestID,
649
+ ephemeral: true,
650
+ amount: 1000
651
+ })
652
+ })
653
+
654
+ await expect(
655
+ manager.createAction(
656
+ {
657
+ description: 'Spend 200 sats with no input from user',
658
+ outputs: [
659
+ {
660
+ outputDescription: 'Nothing to see here',
661
+ lockingScript: '0xabc',
662
+ satoshis: 200
663
+ }
664
+ ]
665
+ },
666
+ 'some-user.com'
667
+ )
668
+ ).resolves.not.toThrow()
669
+
670
+ // underlying createAction called
671
+ expect(underlying.createAction).toHaveBeenCalledTimes(1)
672
+ })
673
+
674
+ it('should check monthly limit usage and prompt renewal if insufficient', async () => {
675
+ manager = new WalletPermissionsManager(underlying, 'admin.com')
676
+
677
+ // Suppose we find an existing DSAP token with authorizedAmount=500
678
+ // manager.findSpendingToken() is used internally, so let's mock it
679
+ const existingSpendingToken: PermissionToken = {
680
+ txid: 'dsap-old',
681
+ outputIndex: 0,
682
+ outputScript: 'scriptHex',
683
+ satoshis: 1,
684
+ originator: 'shopper.com',
685
+ authorizedAmount: 500,
686
+ expiry: 0 // indefinite
687
+ }
688
+ jest
689
+ .spyOn(manager as any, 'findSpendingToken')
690
+ .mockResolvedValue(existingSpendingToken)
691
+
692
+ // Next, manager.querySpentSince(token) sums the user’s monthly spending from labeled actions
693
+ // Let’s stub that to say they've already spent 400.
694
+ jest.spyOn(manager as any, 'querySpentSince').mockResolvedValue(400)
695
+
696
+ // Attempt spending 200 => total usage would be 600 which exceeds 500 => prompt renewal
697
+ // We'll auto-deny for test
698
+ manager.bindCallback('onSpendingAuthorizationRequested', req => {
699
+ manager.denyPermission(req.requestID)
700
+ })
701
+
702
+ await expect(
703
+ manager.createAction(
704
+ {
705
+ description: 'Buy something for 200 sats',
706
+ outputs: [
707
+ {
708
+ outputDescription: 'Nothing to see here',
709
+ lockingScript: 'op_return',
710
+ satoshis: 200
711
+ }
712
+ ]
713
+ },
714
+ 'shopper.com'
715
+ )
716
+ ).rejects.toThrow(/Permission denied/)
717
+
718
+ // The underlying createAction call was started but the manager calls abortAction upon denial
719
+ expect(underlying.abortAction).toHaveBeenCalledTimes(1)
720
+ })
721
+
722
+ it('should pass if usage plus new spend is within the monthly limit', async () => {
723
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {})
724
+
725
+ // existing DSAP token with authorizedAmount=1000
726
+ const dsapToken: PermissionToken = {
727
+ txid: 'dsap123',
728
+ outputIndex: 0,
729
+ outputScript: 'scriptHex',
730
+ satoshis: 1,
731
+ originator: 'shopper.com',
732
+ authorizedAmount: 1000,
733
+ expiry: 0
734
+ }
735
+ jest
736
+ .spyOn(manager as any, 'findSpendingToken')
737
+ .mockResolvedValue(dsapToken)
738
+
739
+ // Suppose they've spent 200 so far
740
+ jest.spyOn(manager as any, 'querySpentSince').mockResolvedValue(200)
741
+
742
+ // Attempt new spending of 500 => total=700 which is <= 1000 => no prompt
743
+ await manager.createAction(
744
+ {
745
+ description: 'Spend 500 sats',
746
+ outputs: [
747
+ {
748
+ outputDescription: 'Nothing to see here',
749
+ lockingScript: '0xabc',
750
+ satoshis: 500
751
+ }
752
+ ]
753
+ },
754
+ 'shopper.com'
755
+ )
756
+ // Success, no new permission requested
757
+ const activeRequests = (manager as any).activeRequests as Map<string, any>
758
+ expect(activeRequests.size).toBe(0)
759
+
760
+ expect(underlying.createAction).toHaveBeenCalledTimes(1)
761
+ })
762
+ })
763
+
764
+ /* ------------------------------------------------------
765
+ * 9) LABEL USAGE PERMISSION TESTS
766
+ * ------------------------------------------------------ */
767
+ describe('Label Usage Permission', () => {
768
+ it('should fail if label starts with "admin" and caller is not admin', async () => {
769
+ manager = new WalletPermissionsManager(underlying, 'admin.com')
770
+
771
+ // Attempt to createAction with a label "admin secret-stuff"
772
+ await expect(
773
+ manager.createAction(
774
+ {
775
+ description: 'Applying admin label?',
776
+ labels: ['admin secret-stuff']
777
+ },
778
+ 'nonadmin.com'
779
+ )
780
+ ).rejects.toThrow(/admin-only/)
781
+
782
+ // Underlying createAction never called
783
+ expect(underlying.createAction).toHaveBeenCalledTimes(0)
784
+ })
785
+
786
+ it('should skip label permission if seekPermissionWhenApplyingActionLabels=false', async () => {
787
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
788
+ seekPermissionWhenApplyingActionLabels: false
789
+ })
790
+
791
+ // Non-admin applies label "my-app-label"
792
+ await expect(
793
+ manager.createAction(
794
+ { description: 'Add label', labels: ['my-app-label'] },
795
+ 'some-app.com'
796
+ )
797
+ ).resolves.not.toThrow()
798
+
799
+ // No prompt
800
+ const activeRequests = (manager as any).activeRequests as Map<string, any>
801
+ expect(activeRequests.size).toBe(0)
802
+
803
+ // Called underlying
804
+ expect(underlying.createAction).toHaveBeenCalledTimes(1)
805
+ })
806
+
807
+ it('should prompt for label usage if seekPermissionWhenApplyingActionLabels=true', async () => {
808
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
809
+ seekPermissionWhenApplyingActionLabels: true
810
+ })
811
+
812
+ manager.bindCallback('onProtocolPermissionRequested', async req => {
813
+ // This request will have protocolID=[1, "action label <label>"], etc.
814
+ await manager.grantPermission({
815
+ requestID: req.requestID,
816
+ ephemeral: true
817
+ })
818
+ })
819
+
820
+ await manager.createAction(
821
+ {
822
+ description: 'Add label "user-label-123"',
823
+ labels: ['user-label-123']
824
+ },
825
+ 'nonadmin.com'
826
+ )
827
+
828
+ // Underlying is called
829
+ expect(underlying.createAction).toHaveBeenCalledTimes(1)
830
+ })
831
+
832
+ it('should also prompt for listing actions by label if seekPermissionWhenListingActionsByLabel=true', async () => {
833
+ manager = new WalletPermissionsManager(underlying, 'admin.com', {
834
+ seekPermissionWhenListingActionsByLabel: true
835
+ })
836
+
837
+ manager.bindCallback('onProtocolPermissionRequested', async req => {
838
+ // auto-grant ephemeral
839
+ await manager.grantPermission({
840
+ requestID: req.requestID,
841
+ ephemeral: true
842
+ })
843
+ })
844
+
845
+ await expect(
846
+ manager.listActions(
847
+ {
848
+ labels: ['search-this-label']
849
+ },
850
+ 'external-app.com'
851
+ )
852
+ ).resolves.not.toThrow()
853
+
854
+ expect(underlying.listActions).toHaveBeenCalledTimes(1)
855
+ })
856
+ })
857
+ })