@shuttle.technology/authorization 0.0.0-stage → 0.2.0

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 (93) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +1124 -2
  3. package/dist/accountLists.d.ts +28 -0
  4. package/dist/accountLists.js +27 -0
  5. package/dist/aesGcmSiv.d.ts +14 -0
  6. package/dist/aesGcmSiv.js +160 -0
  7. package/dist/auth.d.ts +68 -0
  8. package/dist/auth.js +189 -0
  9. package/dist/balances.d.ts +31 -0
  10. package/dist/balances.js +44 -0
  11. package/dist/canonicalize.d.ts +19 -0
  12. package/dist/canonicalize.js +107 -0
  13. package/dist/client.d.ts +152 -0
  14. package/dist/client.js +781 -0
  15. package/dist/clientTypes.d.ts +56 -0
  16. package/dist/clientTypes.js +5 -0
  17. package/dist/configuration.d.ts +48 -0
  18. package/dist/configuration.js +72 -0
  19. package/dist/consentDebit.d.ts +36 -0
  20. package/dist/consentDebit.js +95 -0
  21. package/dist/credentials.d.ts +116 -0
  22. package/dist/credentials.js +315 -0
  23. package/dist/debitPermissionWire.d.ts +90 -0
  24. package/dist/debitPermissionWire.js +118 -0
  25. package/dist/debitPermissions.d.ts +39 -0
  26. package/dist/debitPermissions.js +117 -0
  27. package/dist/deviceKey.d.ts +13 -0
  28. package/dist/deviceKey.js +28 -0
  29. package/dist/deviceRecovery.d.ts +40 -0
  30. package/dist/deviceRecovery.js +143 -0
  31. package/dist/deviceShare.d.ts +9 -0
  32. package/dist/deviceShare.js +24 -0
  33. package/dist/deviceShareEnvelope.d.ts +13 -0
  34. package/dist/deviceShareEnvelope.js +139 -0
  35. package/dist/deviceShares.d.ts +91 -0
  36. package/dist/deviceShares.js +204 -0
  37. package/dist/errors.d.ts +280 -0
  38. package/dist/errors.js +85 -0
  39. package/dist/linkedChecks.d.ts +17 -0
  40. package/dist/linkedChecks.js +31 -0
  41. package/dist/linkedRecovery.d.ts +40 -0
  42. package/dist/linkedRecovery.js +49 -0
  43. package/dist/linkedRegistrations.d.ts +11 -0
  44. package/dist/linkedRegistrations.js +109 -0
  45. package/dist/linkedSendDisplay.d.ts +73 -0
  46. package/dist/linkedSendDisplay.js +84 -0
  47. package/dist/linkedWalletSignals.d.ts +22 -0
  48. package/dist/linkedWalletSignals.js +101 -0
  49. package/dist/linkedWallets.d.ts +401 -0
  50. package/dist/linkedWallets.js +436 -0
  51. package/dist/me.d.ts +27 -0
  52. package/dist/me.js +30 -0
  53. package/dist/mediatedOperation.d.ts +27 -0
  54. package/dist/mediatedOperation.js +68 -0
  55. package/dist/mobile.d.ts +64 -0
  56. package/dist/mobile.js +69 -0
  57. package/dist/modeC.d.ts +131 -0
  58. package/dist/modeC.js +248 -0
  59. package/dist/modeCLinked.d.ts +207 -0
  60. package/dist/modeCLinked.js +270 -0
  61. package/dist/pages.d.ts +22 -0
  62. package/dist/pages.js +45 -0
  63. package/dist/poll.d.ts +15 -0
  64. package/dist/poll.js +76 -0
  65. package/dist/redirects.d.ts +45 -0
  66. package/dist/redirects.js +108 -0
  67. package/dist/requestGrant.d.ts +169 -0
  68. package/dist/requestGrant.js +243 -0
  69. package/dist/sends.d.ts +89 -0
  70. package/dist/sends.js +127 -0
  71. package/dist/serviceGrants.d.ts +52 -0
  72. package/dist/serviceGrants.js +46 -0
  73. package/dist/slice4.d.ts +187 -0
  74. package/dist/slice4.js +299 -0
  75. package/dist/streams.d.ts +212 -0
  76. package/dist/streams.js +282 -0
  77. package/dist/trackedWallets.d.ts +53 -0
  78. package/dist/trackedWallets.js +104 -0
  79. package/dist/transferStatus.d.ts +45 -0
  80. package/dist/transferStatus.js +43 -0
  81. package/dist/walletCreateGrant.d.ts +62 -0
  82. package/dist/walletCreateGrant.js +89 -0
  83. package/dist/walletDisplayParts.d.ts +17 -0
  84. package/dist/walletDisplayParts.js +47 -0
  85. package/dist/walletEvents.d.ts +43 -0
  86. package/dist/walletEvents.js +97 -0
  87. package/dist/walletLimits.d.ts +65 -0
  88. package/dist/walletLimits.js +102 -0
  89. package/dist/walletOperations.d.ts +209 -0
  90. package/dist/walletOperations.js +317 -0
  91. package/dist/webRenewal.d.ts +92 -0
  92. package/dist/webRenewal.js +179 -0
  93. package/package.json +65 -3
@@ -0,0 +1,436 @@
1
+ // HTTP-only account links and explicit linked streams.register. Provider tokens
2
+ // are arguments of individual writes; this facade never retains them.
3
+ //
4
+ // The account's session authorizes what is done with a linked wallet: its
5
+ // registration for tracking (register() comes back authorized and
6
+ // executeRegistration() follows at once), and a send from it (send() and
7
+ // replaceSend() come back authorized, prepare() has the transfer prepared and
8
+ // shown, executeSend() sends it on the owner's confirm). No passkey is asked
9
+ // for either (owner decisions of 8 and 14 October 2026); a passkey still
10
+ // confirms creating a wallet.
11
+ import { ClientFlowError, canonicalDecimal, integer, opaqueId, record, rfc3339UTC, unexpiredRFC3339 } from './auth.js';
12
+ import { parseWalletEventPage } from './streams.js';
13
+ export { resumeCursor } from './streams.js';
14
+ import { parseOperationEnvelope } from './slice4.js';
15
+ import { linkedEventsPath, linkedRegistrationsPath, parseLinkedRegistrationOperationEnvelope, parseLinkedRegistrationResult, parseLinkedStreamsRegistrationList, validateLinkedRegistrationCreate } from './linkedRegistrations.js';
16
+ export { linkedEventsPath, linkedRegistrationsPath, parseLinkedRegistrationOperation, parseLinkedRegistrationOperationEnvelope, parseLinkedRegistrationResult, parseLinkedStreamsRegistrationList, validateLinkedRegistrationCreate } from './linkedRegistrations.js';
17
+ import { linkedSignalsPath, readLinkedWalletSignals } from './linkedWalletSignals.js';
18
+ export { linkedSignalsPath, readLinkedWalletSignals, MaxSignalWallets } from './linkedWalletSignals.js';
19
+ import { parseTransferStatus, transferStatusPath } from './transferStatus.js';
20
+ import { parseLinkedWalletBalances } from './balances.js';
21
+ export { parseLinkedWalletBalances } from './balances.js';
22
+ import { parseSendPage, sendsPath } from './sends.js';
23
+ export { eventOfTransaction, parseSendPage, sendEvents, sendsPath } from './sends.js';
24
+ export { parseTransferStatus, transferFinal, transferStatusPath, TransferStates } from './transferStatus.js';
25
+ import { parseWalletLimits, walletLimitsCreate } from './walletLimits.js';
26
+ export { WalletLimitRefusals, isWalletLimitRefusal, MaxLimitCeilings, MaxLimitRecipients } from './walletLimits.js';
27
+ import { DebitPermissions } from './debitPermissions.js';
28
+ export { DebitPermissions, MaxDebitAssets, MaxDebitRecipients } from './debitPermissions.js';
29
+ import { b64, fault, hex, network, publicKey, requestID, requestUUID, safeRevision, text, token, uuid } from './linkedChecks.js';
30
+ import { linkedSendAsset, parseUserWalletOperationEnvelope, replaceAction, transferComment } from './walletOperations.js';
31
+ export class LinkedWallets {
32
+ #transport;
33
+ constructor(transport) { this.#transport = transport; }
34
+ async challenge(cognitoAccessToken, credentialId, signal) {
35
+ token(cognitoAccessToken);
36
+ requestID(credentialId);
37
+ const { payload } = await this.#transport.write('/v1/wallet-connections/challenges', 'POST', { cognito_access_token: cognitoAccessToken, passkey_credential_id: credentialId }, signal);
38
+ const challenge = parseWalletLinkChallenge(payload, this.#transport.now());
39
+ if (challenge.publicKey.allowCredentials[0].id !== credentialId)
40
+ fault();
41
+ return challenge;
42
+ }
43
+ async connect(challengeId, cognitoAccessToken, assertion, signal) {
44
+ requestUUID(challengeId);
45
+ token(cognitoAccessToken);
46
+ validateWalletLinkAssertion(assertion);
47
+ const encoded = JSON.stringify(assertion);
48
+ if (new TextEncoder().encode(encoded).length > 65536)
49
+ throw new ClientFlowError('invalid_linked_wallet_request');
50
+ return parseWalletConnection((await this.#transport.write('/v1/wallet-connections', 'POST', { challenge_id: challengeId, cognito_access_token: cognitoAccessToken, response: encoded }, signal)).payload);
51
+ }
52
+ async connection(connectionId, signal) {
53
+ requestUUID(connectionId);
54
+ const connection = parseWalletConnection(await this.#transport.get(`/v1/wallet-connections/${connectionId}`, signal));
55
+ if (connection.id !== connectionId)
56
+ fault();
57
+ return connection;
58
+ }
59
+ async sync(connectionId, cognitoAccessToken, signal) {
60
+ requestUUID(connectionId);
61
+ token(cognitoAccessToken);
62
+ const connection = parseWalletConnection((await this.#transport.write(`/v1/wallet-connections/${connectionId}/sync`, 'POST', { cognito_access_token: cognitoAccessToken }, signal)).payload);
63
+ if (connection.id !== connectionId)
64
+ fault();
65
+ return connection;
66
+ }
67
+ /**
68
+ * refresh binds the account to the Wallet identity the provider owns for it
69
+ * and synchronizes its public wallet inventory. It carries no Wallet token
70
+ * and no body; repeating it converges on the same connection. Call it after
71
+ * a wallet was created so the wallet appears in list().
72
+ */
73
+ async refresh(signal) {
74
+ const connection = parseWalletConnection((await this.#transport.write('/v1/wallet-connections/brokered', 'POST', undefined, signal)).payload);
75
+ if (connection.state !== 'connected')
76
+ fault();
77
+ return connection;
78
+ }
79
+ async disconnect(connectionId, signal) {
80
+ requestUUID(connectionId);
81
+ const connection = parseWalletConnection((await this.#transport.write(`/v1/wallet-connections/${connectionId}`, 'DELETE', undefined, signal)).payload);
82
+ if (connection.id !== connectionId || connection.state !== 'revoked')
83
+ fault();
84
+ return connection;
85
+ }
86
+ async list(signal) {
87
+ return parseLinkedWalletList(await this.#transport.get('/v1/linked-wallets', signal));
88
+ }
89
+ async wallet(linkedWalletId, signal) {
90
+ requestUUID(linkedWalletId);
91
+ const root = record(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}`, signal), ['request_id', 'wallet']);
92
+ if (!opaqueId(root.request_id))
93
+ fault();
94
+ const wallet = parseLinkedWallet(root.wallet);
95
+ if (wallet.id !== linkedWalletId)
96
+ fault();
97
+ return { requestId: root.request_id, wallet };
98
+ }
99
+ /** Every metadata page is fresh; restart at the beginning to discover earlier insertions. */
100
+ async registrations(linkedWalletId, options = {}) {
101
+ const path = linkedRegistrationsPath(linkedWalletId, options);
102
+ const page = parseLinkedStreamsRegistrationList(await this.#transport.get(path, options.signal), linkedWalletId);
103
+ if (page.items.length > (options.limit ?? 50) || (options.afterRegistrationId !== undefined && page.items.some(item => item.registrationId <= options.afterRegistrationId)))
104
+ fault();
105
+ return page;
106
+ }
107
+ /**
108
+ * One page of the linked wallet's retained events as wallet events 3.0:
109
+ * what came in, what went out and what else happened, each with kind,
110
+ * status and revision, those of the kinds eventTypes names. Continuations
111
+ * are sealed opaque cursors.
112
+ */
113
+ async events(linkedWalletId, options = {}) {
114
+ return parseWalletEventPage(await this.#transport.get(linkedEventsPath(linkedWalletId, options), options.signal));
115
+ }
116
+ /**
117
+ * Opens one signal stream for these linked wallets and reads it to its end.
118
+ * onOpen says the stream is live: read every wallet once then. onWallets
119
+ * names wallets with new events: read them with events(). The promise
120
+ * resolves with the reason the stream ended; a stream lives no longer than
121
+ * an access token, so the caller opens the next one. It rejects with the
122
+ * server's refusal, with the abort reason, or when the connection fails;
123
+ * keep an ordinary timed read for those times.
124
+ */
125
+ async signals(linkedWalletIds, handlers) {
126
+ const body = await this.#transport.stream(linkedSignalsPath(linkedWalletIds), handlers.signal);
127
+ return readLinkedWalletSignals(body, linkedWalletIds, handlers);
128
+ }
129
+ async createRegistration(create, idempotencyKey, signal) {
130
+ validateLinkedRegistrationCreate(create, this.#transport.accountId());
131
+ requestID(idempotencyKey);
132
+ const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
133
+ const operation = parseLinkedRegistrationOperationEnvelope(payload).operation;
134
+ if (operation.ownerId !== accountId || operation.linkedWalletId !== create.linked_wallet_id)
135
+ fault();
136
+ return operation;
137
+ }
138
+ /**
139
+ * Starts tracking a linked wallet on one network. An EVM wallet has one
140
+ * address on every EVM network, so it is registered on each of them with its
141
+ * own call. activityTypes names the kinds of activity events asked for beside
142
+ * deposits; omitted or empty means deposits only, and Streams refuses a kind
143
+ * the network does not have. The operation comes back authorized;
144
+ * executeRegistration() with its version dispatches it.
145
+ */
146
+ register(input, idempotencyKey, signal) {
147
+ record(input, ['linkedWalletId', 'network'], ['assets', 'activityTypes']);
148
+ const create = { kind: 'streams.register', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: input.linkedWalletId, payload: { network: input.network } };
149
+ if (input.assets !== undefined)
150
+ create.payload.assets = input.assets;
151
+ // An empty selection means deposits only and is not sent; any other value is checked with the whole request.
152
+ if (input.activityTypes !== undefined && !(Array.isArray(input.activityTypes) && input.activityTypes.length === 0))
153
+ create.payload.activity_types = input.activityTypes;
154
+ return this.createRegistration(create, idempotencyKey, signal);
155
+ }
156
+ async operation(operationId, signal) {
157
+ requestID(operationId);
158
+ const operation = parseLinkedRegistrationOperationEnvelope(await this.#transport.get(`/v1/operations/${operationId}`, signal)).operation;
159
+ if (operation.id !== operationId)
160
+ fault();
161
+ return operation;
162
+ }
163
+ async executeRegistration(operationId, version, idempotencyKey, signal) {
164
+ requestID(operationId);
165
+ requestID(idempotencyKey);
166
+ if (!canonicalDecimal(version))
167
+ throw new ClientFlowError('invalid_execute_request');
168
+ const operation = parseLinkedRegistrationOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/execute`, 'POST', undefined, signal, idempotencyKey, version)).payload).operation;
169
+ if (operation.id !== operationId)
170
+ fault();
171
+ return operation;
172
+ }
173
+ async registrationResult(operationId, signal) {
174
+ requestID(operationId);
175
+ return parseLinkedRegistrationResult(await this.#transport.get(`/v1/operations/${operationId}/result`, signal), operationId);
176
+ }
177
+ /**
178
+ * Starts a send from a linked wallet. The account's session authorizes it:
179
+ * the operation comes back authorized (authorizationBasis
180
+ * session_authorized), with no passkey and no confirmation window. Then
181
+ * prepare() has the own-wallet system prepare the transfer and name its
182
+ * fee; show the owner what was prepared and, on their confirm,
183
+ * executeSend() with the device share.
184
+ */
185
+ async send(input, idempotencyKey, signal) {
186
+ const declared = record(input, ['linkedWalletId', 'network', 'asset', 'recipient', 'amountRaw'], ['comment']);
187
+ requestID(idempotencyKey);
188
+ if (!uuid(declared.linkedWalletId) || !network(declared.network) || !linkedSendAsset(declared.asset) || typeof declared.recipient !== 'string' || !/^[\x21-\x7e]{1,256}$/.test(declared.recipient) ||
189
+ !canonicalDecimal(declared.amountRaw) || declared.amountRaw === '0' || declared.amountRaw.length > 78 ||
190
+ // Only a TON transfer carries a comment.
191
+ (declared.comment !== undefined && (!transferComment(declared.comment) || !declared.network.startsWith('ton:'))))
192
+ throw new ClientFlowError('invalid_operation_create');
193
+ const sent = { network: declared.network, asset: declared.asset, recipient: declared.recipient, amount_raw: declared.amountRaw };
194
+ if (declared.comment !== undefined)
195
+ sent.comment = declared.comment;
196
+ const create = { kind: 'wallet.send', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: declared.linkedWalletId, payload: sent };
197
+ const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
198
+ const operation = parseUserWalletOperationEnvelope(payload).operation;
199
+ const shown = operation.display;
200
+ if (operation.kind !== 'wallet.send' || operation.ownerId !== accountId || operation.linkedWallet?.linkedWalletId !== declared.linkedWalletId ||
201
+ shown.asset !== declared.asset || shown.comment !== declared.comment)
202
+ fault();
203
+ return operation;
204
+ }
205
+ /**
206
+ * Replaces a send the own-wallet system broadcast on an EVM network and the
207
+ * network has not included: "speed_up" sends the same transfer again with a
208
+ * higher fee, "cancel" sends nothing to the wallet's own address in its
209
+ * place. The answer is a send of its own, authorized by the session like
210
+ * send(): prepare() has the own-wallet system prepare the replacement (it
211
+ * may answer that the transfer no longer waits), show it with its new fee,
212
+ * and on the owner's confirm executeSend(). Whichever of the two
213
+ * transactions the network includes, the other fails; read
214
+ * transferStatus() of both.
215
+ */
216
+ async replaceSend(input, idempotencyKey, signal) {
217
+ const declared = record(input, ['linkedWalletId', 'operationId', 'action']);
218
+ requestID(idempotencyKey);
219
+ if (!uuid(declared.linkedWalletId) || !opaqueId(declared.operationId) || !replaceAction(declared.action))
220
+ throw new ClientFlowError('invalid_operation_create');
221
+ const create = { kind: 'wallet.send', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: declared.linkedWalletId,
222
+ payload: { action: declared.action, replaces_operation_id: declared.operationId } };
223
+ const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
224
+ const operation = parseUserWalletOperationEnvelope(payload).operation;
225
+ const shown = operation.display;
226
+ if (operation.kind !== 'wallet.send' || operation.ownerId !== accountId || operation.linkedWallet?.linkedWalletId !== declared.linkedWalletId ||
227
+ shown.replacesOperationId !== declared.operationId || shown.replaceAction !== declared.action)
228
+ fault();
229
+ return operation;
230
+ }
231
+ /**
232
+ * What the wallet holds on each of its networks: the coin and the tokens
233
+ * the stand names there, in minimal units with their decimals. The provider
234
+ * reads its own stand's nodes. A network whose node gave no amount is in
235
+ * the answer with its status and without amounts; it fails nothing.
236
+ */
237
+ async balances(linkedWalletId, signal) {
238
+ requestUUID(linkedWalletId);
239
+ return parseLinkedWalletBalances(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/balances`, signal), linkedWalletId);
240
+ }
241
+ /**
242
+ * Has the own-wallet system prepare the transfer of a send or replacement
243
+ * the session authorized, and returns the send with what was prepared:
244
+ * display.preparedSummary, the sending address and the fee estimate. Show
245
+ * them to the owner; executeSend() sends exactly that transfer. The
246
+ * transfer is prepared once: calling again returns the same prepared send
247
+ * and asks the own-wallet system nothing. A prepared send expires within
248
+ * 150 seconds; one that expired is sent again from send(). The request
249
+ * carries no body, idempotency key or version.
250
+ */
251
+ async prepare(operationId, signal) {
252
+ requestID(operationId);
253
+ const operation = parseUserWalletOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/prepare`, 'POST', undefined, signal)).payload).operation;
254
+ if (operation.id !== operationId || operation.kind !== 'wallet.send' || operation.linkedWallet === undefined)
255
+ fault();
256
+ return operation;
257
+ }
258
+ /** The key the device seals its share to, read right before executeSend(). */
259
+ async transportKey(linkedWalletId, signal) {
260
+ requestUUID(linkedWalletId);
261
+ const r = record(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/transport-key`, signal), ['request_id', 'public_key', 'encryption_algorithm']);
262
+ if (!opaqueId(r.request_id) || typeof r.public_key !== 'string' || !/^(0[23][0-9a-f]{64}|04[0-9a-f]{128})$/.test(r.public_key) || typeof r.encryption_algorithm !== 'string' || !/^[a-z0-9_-]{1,128}$/.test(r.encryption_algorithm))
263
+ fault();
264
+ return { requestId: r.request_id, publicKey: r.public_key, encryptionAlgorithm: r.encryption_algorithm };
265
+ }
266
+ /**
267
+ * Executes a prepared send, on the owner's confirm, with the device share
268
+ * resealed for the own-wallet system. Without deviceShare the SDK supplies
269
+ * the share it keeps (deviceShareFor()), recovered first with one passkey
270
+ * confirmation when this browser holds none; an app that keeps its own share
271
+ * passes it resealed (resealDeviceShare). The provider cannot open the share.
272
+ * A send never prepared is refused with VERSION_CONFLICT, an expired one
273
+ * with OPERATION_EXPIRED.
274
+ */
275
+ async executeSend(operationId, version, idempotencyKey, deviceShare, signal) {
276
+ requestID(operationId);
277
+ requestID(idempotencyKey);
278
+ if (!canonicalDecimal(version))
279
+ throw new ClientFlowError('invalid_execute_request');
280
+ if (deviceShare === undefined) {
281
+ const linkedWalletId = parseUserWalletOperationEnvelope(await this.#transport.get(`/v1/operations/${operationId}`, signal)).operation.linkedWallet?.linkedWalletId;
282
+ if (linkedWalletId === undefined)
283
+ throw new ClientFlowError('invalid_execute_request');
284
+ deviceShare = await this.deviceShareFor(linkedWalletId, signal);
285
+ }
286
+ const share = record(deviceShare, ['ciphertext', 'nonce', 'ephemeralPublicKey', 'encryptionAlgorithm', 'encryptionContext']);
287
+ if (![share.ciphertext, share.nonce, share.ephemeralPublicKey, share.encryptionContext].every(hex) || typeof share.encryptionAlgorithm !== 'string')
288
+ throw new ClientFlowError('invalid_execute_request');
289
+ const body = { device_share: { ciphertext: share.ciphertext, nonce: share.nonce, ephemeral_public_key: share.ephemeralPublicKey, encryption_algorithm: share.encryptionAlgorithm, encryption_context: share.encryptionContext } };
290
+ const operation = parseUserWalletOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/execute`, 'POST', body, signal, idempotencyKey, version)).payload).operation;
291
+ if (operation.id !== operationId)
292
+ fault();
293
+ return operation;
294
+ }
295
+ /**
296
+ * The device share the SDK keeps for this wallet, resealed to the
297
+ * own-wallet system's transport key read right before: what executeSend()
298
+ * sends and what a Mode C request is signed with (signLinkedMediatedRequest).
299
+ * A browser that holds no share of the wallet (another domain, cleared site
300
+ * data, another browser or device) recovers it first: the person confirms
301
+ * once with the passkey on the provider's page, and the share the wallet had
302
+ * elsewhere stops working. Two tabs never recover the same wallet at once.
303
+ */
304
+ async deviceShareFor(linkedWalletId, signal) {
305
+ return this.#sealedTo(linkedWalletId, async () => (await this.transportKey(linkedWalletId, signal)).publicKey, signal);
306
+ }
307
+ /** The kept share, recovered first when this browser holds none (with recover false: refused), resealed to the key read right before. */
308
+ async #sealedTo(linkedWalletId, publicKey, signal, recover = true) {
309
+ const devices = this.#devices();
310
+ const { wallet } = await this.wallet(linkedWalletId, signal);
311
+ if (recover)
312
+ await devices.ensure(wallet, this.#serverKey(linkedWalletId, signal), { signal });
313
+ else if (!(await devices.has(wallet, this.#serverKey(linkedWalletId, signal))))
314
+ throw new ClientFlowError('device_share_unavailable');
315
+ return devices.sealedFor(wallet, await publicKey());
316
+ }
317
+ /**
318
+ * The device key of the wallet's latest creation or recovery the provider
319
+ * made, as 33-byte compressed hex; absent when it knows none. A share kept
320
+ * here that is sealed to another key was replaced by a recovery elsewhere.
321
+ */
322
+ async deviceKey(linkedWalletId, signal) {
323
+ requestUUID(linkedWalletId);
324
+ const r = record(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/device-key`, signal), ['request_id'], ['device_public_key']);
325
+ if (!opaqueId(r.request_id) || (r.device_public_key !== undefined && (typeof r.device_public_key !== 'string' || !/^0[23][0-9a-f]{64}$/.test(r.device_public_key))))
326
+ fault();
327
+ return r.device_public_key === undefined ? { requestId: r.request_id } : { requestId: r.request_id, devicePublicKey: r.device_public_key };
328
+ }
329
+ /** Whether this browser holds a current device share of the wallet; without one the next send recovers it with the passkey. */
330
+ async hasDeviceShare(linkedWalletId, signal) {
331
+ const devices = this.#devices();
332
+ return devices.has((await this.wallet(linkedWalletId, signal)).wallet, this.#serverKey(linkedWalletId, signal));
333
+ }
334
+ /**
335
+ * Recovers the wallet's device share onto this browser when it holds no
336
+ * current one, as deviceShareFor() would, for an app that does it ahead of a
337
+ * transfer; force recovers even then. credentialId names the passkey that
338
+ * confirms it, by default the account's newest. Answers whether a recovery
339
+ * ran. Optional: sends recover by themselves.
340
+ */
341
+ async recoverDevice(linkedWalletId, options = {}) {
342
+ const devices = this.#devices();
343
+ return devices.ensure((await this.wallet(linkedWalletId, options.signal)).wallet, this.#serverKey(linkedWalletId, options.signal), options);
344
+ }
345
+ #serverKey(linkedWalletId, signal) {
346
+ return async () => (await this.deviceKey(linkedWalletId, signal)).devicePublicKey;
347
+ }
348
+ #devices() {
349
+ if (!this.#transport.devices)
350
+ throw new ClientFlowError('device_share_unavailable');
351
+ return this.#transport.devices;
352
+ }
353
+ /**
354
+ * What became of the transfer a send prepared: on its way, confirmed by the
355
+ * network, or failed and why. The send's operation succeeds when the
356
+ * own-wallet system accepted the transfer; this is what the network did
357
+ * with it. Read it until final is true. Before the send was prepared
358
+ * there is no transfer and the read is refused with NOT_FOUND.
359
+ */
360
+ async transferStatus(operationId, signal) {
361
+ return parseTransferStatus(await this.#transport.get(transferStatusPath(operationId), signal), operationId);
362
+ }
363
+ /**
364
+ * One page of the account's send history, newest first: every wallet.send
365
+ * the account can read, with its transaction hash and the recorded final
366
+ * fate of its transfer. A send with a hash and no transfer has not been
367
+ * reported final yet: read transferStatus() for it, which records the fate.
368
+ * The cursor carries the first page's limit.
369
+ */
370
+ async sends(options) {
371
+ const { path, signal } = sendsPath(options);
372
+ return parseSendPage(await this.#transport.get(path, signal));
373
+ }
374
+ /** The limits every send from the wallet obeys, read with the session (walletLimits.ts). Revision "0": never set. */
375
+ async limits(linkedWalletId, signal) {
376
+ requestUUID(linkedWalletId);
377
+ return parseWalletLimits(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/limits`, signal), linkedWalletId);
378
+ }
379
+ /**
380
+ * Asks to replace the wallet's limits as a whole; empty ceilings and recipients remove them all. The operation
381
+ * awaits the owner's passkey: the client's challengeOperation(), completeOperationConfirmation() and
382
+ * executeOperation() apply it. A send the limits refuse answers one of WalletLimitRefusals.
383
+ */
384
+ async setLimits(linkedWalletId, limits, idempotencyKey, signal) {
385
+ const create = walletLimitsCreate(this.#transport.accountId(), linkedWalletId, limits);
386
+ requestID(idempotencyKey);
387
+ const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
388
+ const operation = parseOperationEnvelope(payload).operation;
389
+ if (operation.display.kind !== 'wallet_limits.set' || operation.ownerId !== accountId || operation.display.linkedWalletId !== linkedWalletId)
390
+ fault();
391
+ return operation;
392
+ }
393
+ /** The permissions the account lets services debit its linked wallets by, without the device (debitPermissions.ts). */
394
+ debitPermissions() { return new DebitPermissions(this.#transport, (linkedWalletId, keeperKey, signal, recover) => this.#sealedTo(linkedWalletId, async () => keeperKey, signal, recover)); }
395
+ }
396
+ export function parseWalletConnection(payload) {
397
+ const r = record(payload, ['request_id', 'id', 'connector_id', 'state', 'created_at', 'last_synced_at'], ['revoked_at']);
398
+ if (!opaqueId(r.request_id) || !uuid(r.id) || !opaqueId(r.connector_id) || !['connected', 'revoked', 'reauth_required'].includes(r.state) || !rfc3339UTC(r.created_at) || !rfc3339UTC(r.last_synced_at) || (r.state === 'revoked') !== (r.revoked_at !== undefined) || (r.revoked_at !== undefined && !rfc3339UTC(r.revoked_at)))
399
+ fault();
400
+ const out = { requestId: r.request_id, id: r.id, connectorId: r.connector_id, state: r.state, createdAt: r.created_at, lastSyncedAt: r.last_synced_at };
401
+ if (r.revoked_at !== undefined)
402
+ out.revokedAt = r.revoked_at;
403
+ return Object.freeze(out);
404
+ }
405
+ export function parseLinkedWallet(payload) {
406
+ const r = record(payload, ['id', 'connection_id', 'external_wallet_id', 'external_key_id', 'network', 'address', 'algorithm', 'public_key', 'binding_revision', 'status', 'observed_at']);
407
+ if (![r.id, r.connection_id, r.external_wallet_id, r.external_key_id].every(uuid) || !text(r.network, 64) || !text(r.address, 256) || !safeRevision(r.binding_revision) || !['active', 'inactive'].includes(r.status) || !rfc3339UTC(r.observed_at) || !publicKey(r.algorithm, r.public_key))
408
+ fault();
409
+ return Object.freeze({ id: r.id, connectionId: r.connection_id, externalWalletId: r.external_wallet_id, externalKeyId: r.external_key_id, network: r.network, address: r.address, algorithm: r.algorithm, publicKey: r.public_key, bindingRevision: r.binding_revision, status: r.status, observedAt: r.observed_at });
410
+ }
411
+ export function parseLinkedWalletList(payload) {
412
+ const r = record(payload, ['request_id', 'items']);
413
+ if (!opaqueId(r.request_id) || !Array.isArray(r.items) || r.items.length > 1000)
414
+ fault();
415
+ const items = r.items.map(parseLinkedWallet);
416
+ if (items.some(w => w.status !== 'active') || new Set(items.map(w => w.id)).size !== items.length)
417
+ fault();
418
+ return { requestId: r.request_id, items: Object.freeze(items) };
419
+ }
420
+ export function parseWalletLinkChallenge(payload, now) {
421
+ const r = record(payload, ['request_id', 'challenge_id', 'expires_at', 'public_key']);
422
+ const p = record(r.public_key, ['allow_credentials', 'challenge', 'rp_id', 'timeout_ms', 'user_verification']);
423
+ if (!opaqueId(r.request_id) || !uuid(r.challenge_id) || !unexpiredRFC3339(r.expires_at, now) || !b64(p.challenge) || !text(p.rp_id, 253) || !integer(p.timeout_ms) || p.timeout_ms < 1 || p.timeout_ms > 300000 || p.user_verification !== 'required' || !Array.isArray(p.allow_credentials) || p.allow_credentials.length !== 1)
424
+ fault();
425
+ const c = record(p.allow_credentials[0], ['id', 'type']);
426
+ if (c.type !== 'public-key' || !b64(c.id))
427
+ fault();
428
+ return { requestId: r.request_id, challengeId: r.challenge_id, expiresAt: r.expires_at, publicKey: { challenge: p.challenge, rpId: p.rp_id, timeoutMs: p.timeout_ms, userVerification: 'required', allowCredentials: [{ id: c.id, type: 'public-key' }] } };
429
+ }
430
+ export function validateWalletLinkAssertion(value) {
431
+ const r = record(value, ['id', 'rawId', 'type', 'response']);
432
+ const p = record(r.response, ['clientDataJSON', 'authenticatorData', 'signature', 'userHandle']);
433
+ if (r.type !== 'public-key' || !b64(r.id) || r.rawId !== r.id || ![p.clientDataJSON, p.authenticatorData, p.signature].every(b64) || (p.userHandle !== '' && !b64(p.userHandle)))
434
+ throw new ClientFlowError('invalid_linked_wallet_request');
435
+ return value;
436
+ }
package/dist/me.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * One wallet of the account: "wallet" is a wallet the provider created,
3
+ * "linked_wallet" one its owner connected, and walletId is an identifier of
4
+ * that kind. A listed linked wallet is active; an own wallet is active or
5
+ * disabled.
6
+ */
7
+ export interface MeWallet {
8
+ walletId: string;
9
+ kind: "wallet" | "linked_wallet";
10
+ network: string;
11
+ address: string;
12
+ state: "active" | "disabled";
13
+ }
14
+ export interface MeProjection {
15
+ requestId: string;
16
+ account: {
17
+ accountId: string;
18
+ version: string;
19
+ setupRequired: boolean;
20
+ };
21
+ sessionId: string;
22
+ serverTime: number;
23
+ /** Every wallet of the account, own then linked, at most 1000 of each kind. */
24
+ wallets: MeWallet[];
25
+ }
26
+ /** parseMeProjection reads exactly one `/v1/me` answer. */
27
+ export declare function parseMeProjection(payload: unknown): MeProjection;
package/dist/me.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The live `/v1/me` projection: the current account, the session, the server
3
+ * time and every wallet of the account. It is read as strictly as every
4
+ * other answer: an unknown member, a missing one or a value outside its
5
+ * closed form refuses the whole answer.
6
+ */
7
+ import { ClientFlowError, canonicalDecimal, integer, opaqueId, record } from "./auth.js";
8
+ /** The most wallets of one kind an account lists; the answer holds two kinds. */
9
+ const maxWalletsPerKind = 1000;
10
+ /** parseMeProjection reads exactly one `/v1/me` answer. */
11
+ export function parseMeProjection(payload) {
12
+ const root = record(payload, ["request_id", "account", "session_id", "server_time", "wallets"]);
13
+ const account = record(root.account, ["account_id", "version", "setup_required"]);
14
+ if (!opaqueId(root.request_id) || !opaqueId(account.account_id) || !canonicalDecimal(account.version) || typeof account.setup_required !== "boolean" || !opaqueId(root.session_id) || !integer(root.server_time) ||
15
+ !Array.isArray(root.wallets) || root.wallets.length > 2 * maxWalletsPerKind) {
16
+ throw new ClientFlowError("invalid_server_response");
17
+ }
18
+ const wallets = root.wallets.map((value) => {
19
+ const item = record(value, ["wallet_id", "kind", "network", "address", "state"]);
20
+ if (!opaqueId(item.wallet_id) || (item.kind !== "wallet" && item.kind !== "linked_wallet") || typeof item.network !== "string" || item.network === "" ||
21
+ typeof item.address !== "string" || item.address === "" || item.address.length > 256 || (item.state !== "active" && item.state !== "disabled")) {
22
+ throw new ClientFlowError("invalid_server_response");
23
+ }
24
+ return { walletId: item.wallet_id, kind: item.kind, network: item.network, address: item.address, state: item.state };
25
+ });
26
+ if (wallets.filter((wallet) => wallet.kind === "wallet").length > maxWalletsPerKind || wallets.filter((wallet) => wallet.kind === "linked_wallet").length > maxWalletsPerKind) {
27
+ throw new ClientFlowError("invalid_server_response");
28
+ }
29
+ return { requestId: root.request_id, account: { accountId: account.account_id, version: account.version, setupRequired: account.setup_required }, sessionId: root.session_id, serverTime: root.server_time, wallets };
30
+ }
@@ -0,0 +1,27 @@
1
+ import { type MediatedLinkedSendDisplay } from "./linkedSendDisplay.ts";
2
+ import type { UserWalletOperation } from "./walletOperations.ts";
3
+ export type { MediatedLinkedSendDisplay } from "./linkedSendDisplay.ts";
4
+ /** The display of a Mode C transfer from a wallet bound in the provider: the network coin only. */
5
+ export interface MediatedSendDisplay {
6
+ kind: "wallet.send";
7
+ ownerId: string;
8
+ requesterId: string;
9
+ walletId: string;
10
+ network: string;
11
+ asset: "native";
12
+ recipient: string;
13
+ amountRaw: string;
14
+ maxFeeRaw: string;
15
+ preparedSummary?: string;
16
+ }
17
+ /** A Mode C transfer past its material retention: its identity only; a linked wallet stays named by the operation's linkedWallet. */
18
+ export interface MediatedPurgedDisplay {
19
+ kind: "wallet.send";
20
+ materialPurged: true;
21
+ ownerId: string;
22
+ requesterId: string;
23
+ walletId?: string;
24
+ }
25
+ export type MediatedDisplay = MediatedSendDisplay | MediatedLinkedSendDisplay | MediatedPurgedDisplay;
26
+ /** parseMediatedOperation reads exactly the mediated status of one Mode C send. */
27
+ export declare function parseMediatedOperation(payload: unknown): UserWalletOperation;
@@ -0,0 +1,68 @@
1
+ // The status of a Mode C send as its owner reads it: a transfer a service
2
+ // forwarded on the person's signed request (authorization basis
3
+ // user_request_signed), from a wallet bound in the provider or from the
4
+ // person's linked wallet. The provider projects it as the mediated status on
5
+ // every route, the owner's GET of the operation and the account's send
6
+ // history included: it freezes no result recipient, and a linked transfer
7
+ // names its wallet by binding_source, connection_id, linked_wallet_id and
8
+ // binding_revision instead of wallet_id.
9
+ import { ClientFlowError, canonicalDecimal, deadlineAfter, isNextAction, opaqueId, record, rfc3339UTC } from "./auth.js";
10
+ import { bindingRevision, lowerUUID, rawQuantity, summaryOf, walletRecipient } from "./walletDisplayParts.js";
11
+ import { parseMediatedLinkedSendDisplay } from "./linkedSendDisplay.js";
12
+ const states = ["authorized", "dispatching", "outcome_unknown", "succeeded", "failed", "denied", "canceled", "expired"];
13
+ const ownNetwork = /^[A-Za-z0-9_.-]{1,64}$/;
14
+ const digest = (v) => typeof v === "string" && /^[0-9a-f]{64}$/.test(v);
15
+ const fault = () => { throw new ClientFlowError("invalid_server_response"); };
16
+ /** parseMediatedOperation reads exactly the mediated status of one Mode C send. */
17
+ export function parseMediatedOperation(payload) {
18
+ const r = record(payload, ["authorization_basis", "created_at", "display", "effect_state", "expires_at", "id", "kind", "next_action", "operation_fingerprint", "owner_id", "owner_type",
19
+ "request_fingerprint", "requester_id", "state", "version"], ["result_kind", "wallet_id", "binding_source", "connection_id", "linked_wallet_id", "binding_revision"]);
20
+ if (r.authorization_basis !== "user_request_signed" || r.kind !== "wallet.send" || r.owner_type !== "account" || !opaqueId(r.id) || !canonicalDecimal(r.version) ||
21
+ !opaqueId(r.owner_id) || !opaqueId(r.requester_id) || typeof r.state !== "string" || !states.includes(r.state) || !["no_effect", "possible", "known"].includes(r.effect_state) ||
22
+ !digest(r.request_fingerprint) || !digest(r.operation_fingerprint) || !rfc3339UTC(r.created_at) || !rfc3339UTC(r.expires_at) || !deadlineAfter(r.created_at, r.expires_at) ||
23
+ !isNextAction(r.next_action) || (r.result_kind !== undefined && r.result_kind !== "transfer_receipt"))
24
+ fault();
25
+ // A linked transfer is named by all of its linked identity and never beside a wallet_id.
26
+ const linked = r.binding_source !== undefined;
27
+ if (linked ? (r.binding_source !== "linked_wallet" || r.wallet_id !== undefined || !lowerUUID(r.connection_id) || !lowerUUID(r.linked_wallet_id) || !bindingRevision(r.binding_revision))
28
+ : (!opaqueId(r.wallet_id) || r.connection_id !== undefined || r.linked_wallet_id !== undefined || r.binding_revision !== undefined))
29
+ fault();
30
+ const display = mediatedDisplay(r.display, r, linked);
31
+ const operation = {
32
+ id: r.id, version: r.version, kind: "wallet.send", state: r.state, effectState: r.effect_state,
33
+ authorizationBasis: "user_request_signed", ownerType: "account", ownerId: r.owner_id, requesterId: r.requester_id,
34
+ requestFingerprint: r.request_fingerprint, operationFingerprint: r.operation_fingerprint, display,
35
+ createdAt: r.created_at, expiresAt: r.expires_at, nextAction: r.next_action,
36
+ };
37
+ if (linked)
38
+ operation.linkedWallet = { connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision };
39
+ else
40
+ operation.walletId = r.wallet_id;
41
+ if (r.result_kind !== undefined)
42
+ operation.resultKind = "transfer_receipt";
43
+ return operation;
44
+ }
45
+ function mediatedDisplay(value, r, linked) {
46
+ if (value !== null && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, "material_purged")) {
47
+ const d = record(value, ["kind", "material_purged", "owner_id", "requester_id"], [linked ? "linked_wallet_id" : "wallet_id"]);
48
+ const own = linked ? d.linked_wallet_id === r.linked_wallet_id : d.wallet_id === r.wallet_id;
49
+ if (d.kind !== "wallet.send" || d.material_purged !== true || d.owner_id !== r.owner_id || d.requester_id !== r.requester_id || !own)
50
+ fault();
51
+ const purged = { kind: "wallet.send", materialPurged: true, ownerId: d.owner_id, requesterId: d.requester_id };
52
+ if (!linked)
53
+ purged.walletId = d.wallet_id;
54
+ return purged;
55
+ }
56
+ if (linked) {
57
+ const d = parseMediatedLinkedSendDisplay(value);
58
+ if (d.ownerId !== r.owner_id || d.requesterId !== r.requester_id || d.linkedWalletId !== r.linked_wallet_id || d.connectionId !== r.connection_id || d.bindingRevision !== r.binding_revision)
59
+ fault();
60
+ return d;
61
+ }
62
+ const d = record(value, ["amount_raw", "asset", "kind", "max_fee_raw", "network", "owner_id", "recipient", "requester_id", "wallet_id"], ["prepared_summary"]);
63
+ if (d.kind !== "wallet.send" || d.asset !== "native" || !rawQuantity(d.amount_raw, true) || !rawQuantity(d.max_fee_raw, false) || typeof d.network !== "string" || !ownNetwork.test(d.network) ||
64
+ typeof d.recipient !== "string" || !walletRecipient.test(d.recipient) || d.owner_id !== r.owner_id || d.requester_id !== r.requester_id || d.wallet_id !== r.wallet_id)
65
+ fault();
66
+ return { kind: "wallet.send", ownerId: d.owner_id, requesterId: d.requester_id, walletId: d.wallet_id, network: d.network, asset: "native",
67
+ recipient: d.recipient, amountRaw: d.amount_raw, maxFeeRaw: d.max_fee_raw, ...summaryOf(d.prepared_summary) };
68
+ }