@shuttle.technology/authorization 0.1.0 → 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 (52) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +1082 -666
  3. package/dist/accountLists.d.ts +28 -0
  4. package/dist/accountLists.js +27 -0
  5. package/dist/auth.d.ts +11 -11
  6. package/dist/auth.js +28 -54
  7. package/dist/client.d.ts +62 -174
  8. package/dist/client.js +381 -354
  9. package/dist/clientTypes.d.ts +56 -0
  10. package/dist/clientTypes.js +5 -0
  11. package/dist/configuration.d.ts +48 -0
  12. package/dist/configuration.js +72 -0
  13. package/dist/consentDebit.d.ts +36 -0
  14. package/dist/consentDebit.js +95 -0
  15. package/dist/credentials.d.ts +116 -0
  16. package/dist/credentials.js +315 -0
  17. package/dist/debitPermissionWire.d.ts +90 -0
  18. package/dist/debitPermissionWire.js +118 -0
  19. package/dist/debitPermissions.d.ts +39 -0
  20. package/dist/debitPermissions.js +117 -0
  21. package/dist/deviceRecovery.d.ts +40 -0
  22. package/dist/deviceRecovery.js +143 -0
  23. package/dist/deviceShares.d.ts +91 -0
  24. package/dist/deviceShares.js +204 -0
  25. package/dist/errors.d.ts +280 -0
  26. package/dist/errors.js +85 -0
  27. package/dist/linkedRecovery.d.ts +40 -0
  28. package/dist/linkedRecovery.js +49 -0
  29. package/dist/linkedRegistrations.d.ts +11 -0
  30. package/dist/linkedRegistrations.js +109 -0
  31. package/dist/linkedWallets.d.ts +46 -18
  32. package/dist/linkedWallets.js +85 -120
  33. package/dist/me.d.ts +27 -0
  34. package/dist/me.js +30 -0
  35. package/dist/mobile.d.ts +64 -0
  36. package/dist/mobile.js +69 -0
  37. package/dist/redirects.d.ts +45 -0
  38. package/dist/redirects.js +108 -0
  39. package/dist/serviceGrants.d.ts +52 -0
  40. package/dist/serviceGrants.js +46 -0
  41. package/dist/slice4.d.ts +20 -6
  42. package/dist/slice4.js +22 -8
  43. package/dist/streams.js +1 -1
  44. package/dist/walletCreateGrant.d.ts +62 -0
  45. package/dist/walletCreateGrant.js +89 -0
  46. package/dist/walletDisplayParts.d.ts +2 -0
  47. package/dist/walletDisplayParts.js +4 -0
  48. package/dist/walletOperations.d.ts +23 -6
  49. package/dist/walletOperations.js +31 -22
  50. package/dist/webRenewal.d.ts +92 -0
  51. package/dist/webRenewal.js +179 -0
  52. package/package.json +25 -23
@@ -2,6 +2,7 @@ import { type NextAction } from './auth.ts';
2
2
  import { type WalletEventPage } from './streams.ts';
3
3
  export { resumeCursor } from './streams.ts';
4
4
  import { type LocalOperation, type ResultRecipient } from './slice4.ts';
5
+ export { linkedEventsPath, linkedRegistrationsPath, parseLinkedRegistrationOperation, parseLinkedRegistrationOperationEnvelope, parseLinkedRegistrationResult, parseLinkedStreamsRegistrationList, validateLinkedRegistrationCreate } from './linkedRegistrations.ts';
5
6
  import { type LinkedWalletSignalEnd, type LinkedWalletSignalHandlers } from './linkedWalletSignals.ts';
6
7
  export { linkedSignalsPath, readLinkedWalletSignals, MaxSignalWallets, type LinkedWalletSignalEnd, type LinkedWalletSignalHandlers } from './linkedWalletSignals.ts';
7
8
  import { type TransferStatus } from './transferStatus.ts';
@@ -14,6 +15,9 @@ import type { PageOptions } from './pages.ts';
14
15
  export { parseTransferStatus, transferFinal, transferStatusPath, TransferStates, type TransferState, type TransferStatus } from './transferStatus.ts';
15
16
  import { type WalletLimits, type WalletLimitsInput } from './walletLimits.ts';
16
17
  export { WalletLimitRefusals, isWalletLimitRefusal, MaxLimitCeilings, MaxLimitRecipients, type LimitAsset, type WalletCeiling, type WalletLimitRefusal, type WalletLimits, type WalletLimitsDisplay, type WalletLimitsInput } from './walletLimits.ts';
18
+ import { DebitPermissions } from './debitPermissions.ts';
19
+ export { DebitPermissions, MaxDebitAssets, MaxDebitRecipients, type DebitAsset, type DebitAssetBound, type DebitPermission, type DebitPermissionDisplay, type DebitPermissionInput, type DebitPermissionList, type DebitPermissionRevokeDisplay, type DebitPermissionState, type DebitShareKeys } from './debitPermissions.ts';
20
+ import type { DeviceRecoveryOptions, LinkedDeviceShares } from './deviceShares.ts';
17
21
  import { type LinkedSendAsset, type ReplaceAction, type DeviceSharePackage, type UserWalletOperation, type UserWalletState, type UserWalletEffectState, type UserWalletAuthorizationBasis } from './walletOperations.ts';
18
22
  /** session_authorized: the account's session registered its own linked wallet; no confirmation exists. */
19
23
  export type LinkedRegistrationAuthorizationBasis = UserWalletAuthorizationBasis;
@@ -76,7 +80,6 @@ export interface LinkedRegistrationCreate {
76
80
  assets?: string[];
77
81
  activity_types?: string[];
78
82
  };
79
- result_recipient?: ResultRecipient;
80
83
  }
81
84
  export interface LinkedRegistrationDisplay {
82
85
  kind: 'streams.register';
@@ -154,7 +157,6 @@ export interface LinkedSendInput {
154
157
  recipient: string;
155
158
  amountRaw: string;
156
159
  comment?: string;
157
- resultRecipient?: ResultRecipient;
158
160
  }
159
161
  /**
160
162
  * The replacement of a send the network has not included. operationId is the
@@ -165,7 +167,6 @@ export interface LinkedReplaceInput {
165
167
  linkedWalletId: string;
166
168
  operationId: string;
167
169
  action: ReplaceAction;
168
- resultRecipient?: ResultRecipient;
169
170
  }
170
171
  /** The own-wallet system's transport key, as that system publishes it. It carries no proof of where the key lives. */
171
172
  export interface LinkedTransportKey {
@@ -218,6 +219,8 @@ export interface LinkedWalletTransport {
218
219
  }>;
219
220
  accountId(): string;
220
221
  now(): number;
222
+ /** The device shares the client keeps (deviceShares.ts); absent, every call that asks the SDK for a share refuses with device_share_unavailable. */
223
+ devices?: LinkedDeviceShares;
221
224
  }
222
225
  export declare class LinkedWallets {
223
226
  #private;
@@ -275,7 +278,6 @@ export declare class LinkedWallets {
275
278
  network: string;
276
279
  assets?: string[];
277
280
  activityTypes?: string[];
278
- resultRecipient?: ResultRecipient;
279
281
  }, idempotencyKey: string, signal?: AbortSignal): Promise<LinkedRegistrationOperation>;
280
282
  operation(operationId: string, signal?: AbortSignal): Promise<LinkedRegistrationOperation>;
281
283
  executeRegistration(operationId: string, version: string, idempotencyKey: string, signal?: AbortSignal): Promise<LinkedRegistrationOperation>;
@@ -323,11 +325,45 @@ export declare class LinkedWallets {
323
325
  transportKey(linkedWalletId: string, signal?: AbortSignal): Promise<LinkedTransportKey>;
324
326
  /**
325
327
  * Executes a prepared send, on the owner's confirm, with the device share
326
- * resealed for the own-wallet system (resealDeviceShare). The provider
327
- * cannot open the share. A send never prepared is refused with
328
- * VERSION_CONFLICT, an expired one with OPERATION_EXPIRED.
328
+ * resealed for the own-wallet system. Without deviceShare the SDK supplies
329
+ * the share it keeps (deviceShareFor()), recovered first with one passkey
330
+ * confirmation when this browser holds none; an app that keeps its own share
331
+ * passes it resealed (resealDeviceShare). The provider cannot open the share.
332
+ * A send never prepared is refused with VERSION_CONFLICT, an expired one
333
+ * with OPERATION_EXPIRED.
329
334
  */
330
- executeSend(operationId: string, version: string, idempotencyKey: string, deviceShare: DeviceSharePackage, signal?: AbortSignal): Promise<UserWalletOperation>;
335
+ executeSend(operationId: string, version: string, idempotencyKey: string, deviceShare?: DeviceSharePackage, signal?: AbortSignal): Promise<UserWalletOperation>;
336
+ /**
337
+ * The device share the SDK keeps for this wallet, resealed to the
338
+ * own-wallet system's transport key read right before: what executeSend()
339
+ * sends and what a Mode C request is signed with (signLinkedMediatedRequest).
340
+ * A browser that holds no share of the wallet (another domain, cleared site
341
+ * data, another browser or device) recovers it first: the person confirms
342
+ * once with the passkey on the provider's page, and the share the wallet had
343
+ * elsewhere stops working. Two tabs never recover the same wallet at once.
344
+ */
345
+ deviceShareFor(linkedWalletId: string, signal?: AbortSignal): Promise<DeviceSharePackage>;
346
+ /**
347
+ * The device key of the wallet's latest creation or recovery the provider
348
+ * made, as 33-byte compressed hex; absent when it knows none. A share kept
349
+ * here that is sealed to another key was replaced by a recovery elsewhere.
350
+ */
351
+ deviceKey(linkedWalletId: string, signal?: AbortSignal): Promise<{
352
+ requestId: string;
353
+ devicePublicKey?: string;
354
+ }>;
355
+ /** Whether this browser holds a current device share of the wallet; without one the next send recovers it with the passkey. */
356
+ hasDeviceShare(linkedWalletId: string, signal?: AbortSignal): Promise<boolean>;
357
+ /**
358
+ * Recovers the wallet's device share onto this browser when it holds no
359
+ * current one, as deviceShareFor() would, for an app that does it ahead of a
360
+ * transfer; force recovers even then. credentialId names the passkey that
361
+ * confirms it, by default the account's newest. Answers whether a recovery
362
+ * ran. Optional: sends recover by themselves.
363
+ */
364
+ recoverDevice(linkedWalletId: string, options?: DeviceRecoveryOptions & {
365
+ force?: boolean;
366
+ }): Promise<boolean>;
331
367
  /**
332
368
  * What became of the transfer a send prepared: on its way, confirmed by the
333
369
  * network, or failed and why. The send's operation succeeds when the
@@ -352,6 +388,8 @@ export declare class LinkedWallets {
352
388
  * executeOperation() apply it. A send the limits refuse answers one of WalletLimitRefusals.
353
389
  */
354
390
  setLimits(linkedWalletId: string, limits: WalletLimitsInput, idempotencyKey: string, signal?: AbortSignal): Promise<LocalOperation>;
391
+ /** The permissions the account lets services debit its linked wallets by, without the device (debitPermissions.ts). */
392
+ debitPermissions(): DebitPermissions;
355
393
  }
356
394
  export declare function parseWalletConnection(payload: unknown): WalletConnection;
357
395
  export declare function parseLinkedWallet(payload: unknown): LinkedWallet;
@@ -361,13 +399,3 @@ export declare function parseLinkedWalletList(payload: unknown): {
361
399
  };
362
400
  export declare function parseWalletLinkChallenge(payload: unknown, now: number): WalletLinkChallenge;
363
401
  export declare function validateWalletLinkAssertion(value: WalletLinkAssertion): WalletLinkAssertion;
364
- export declare function validateLinkedRegistrationCreate(value: LinkedRegistrationCreate, ownerId: string): LinkedRegistrationCreate;
365
- export declare function parseLinkedRegistrationOperationEnvelope(payload: unknown): {
366
- requestId: string;
367
- operation: LinkedRegistrationOperation;
368
- };
369
- export declare function parseLinkedRegistrationOperation(payload: unknown): LinkedRegistrationOperation;
370
- export declare function parseLinkedRegistrationResult(payload: unknown, operationId: string): LinkedRegistrationResult;
371
- export declare function linkedRegistrationsPath(linkedWalletId: string, options?: LinkedRegistrationListOptions): string;
372
- export declare function linkedEventsPath(linkedWalletId: string, options?: LinkedEventsOptions): string;
373
- export declare function parseLinkedStreamsRegistrationList(payload: unknown, linkedWalletId: string): LinkedStreamsRegistrationList;
@@ -8,10 +8,12 @@
8
8
  // shown, executeSend() sends it on the owner's confirm). No passkey is asked
9
9
  // for either (owner decisions of 8 and 14 October 2026); a passkey still
10
10
  // confirms creating a wallet.
11
- import { ClientFlowError, canonicalDecimal, deadlineAfter, integer, isNextAction, opaqueId, record, rfc3339UTC, unexpiredRFC3339 } from './auth.js';
12
- import { appendEventTypes, parseWalletEventPage, MaxStreamsActivityTypes, MaxStreamsCursorLength, MaxStreamsPageLimit } from './streams.js';
11
+ import { ClientFlowError, canonicalDecimal, integer, opaqueId, record, rfc3339UTC, unexpiredRFC3339 } from './auth.js';
12
+ import { parseWalletEventPage } from './streams.js';
13
13
  export { resumeCursor } from './streams.js';
14
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';
15
17
  import { linkedSignalsPath, readLinkedWalletSignals } from './linkedWalletSignals.js';
16
18
  export { linkedSignalsPath, readLinkedWalletSignals, MaxSignalWallets } from './linkedWalletSignals.js';
17
19
  import { parseTransferStatus, transferStatusPath } from './transferStatus.js';
@@ -22,7 +24,9 @@ export { eventOfTransaction, parseSendPage, sendEvents, sendsPath } from './send
22
24
  export { parseTransferStatus, transferFinal, transferStatusPath, TransferStates } from './transferStatus.js';
23
25
  import { parseWalletLimits, walletLimitsCreate } from './walletLimits.js';
24
26
  export { WalletLimitRefusals, isWalletLimitRefusal, MaxLimitCeilings, MaxLimitRecipients } from './walletLimits.js';
25
- import { activityTypes, assets, b64, digest, fault, hex, https, network, publicKey, recipient, requestID, requestUUID, safeRevision, text, token, uuid } from './linkedChecks.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';
26
30
  import { linkedSendAsset, parseUserWalletOperationEnvelope, replaceAction, transferComment } from './walletOperations.js';
27
31
  export class LinkedWallets {
28
32
  #transport;
@@ -140,15 +144,13 @@ export class LinkedWallets {
140
144
  * executeRegistration() with its version dispatches it.
141
145
  */
142
146
  register(input, idempotencyKey, signal) {
143
- record(input, ['linkedWalletId', 'network'], ['assets', 'activityTypes', 'resultRecipient']);
147
+ record(input, ['linkedWalletId', 'network'], ['assets', 'activityTypes']);
144
148
  const create = { kind: 'streams.register', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: input.linkedWalletId, payload: { network: input.network } };
145
149
  if (input.assets !== undefined)
146
150
  create.payload.assets = input.assets;
147
151
  // An empty selection means deposits only and is not sent; any other value is checked with the whole request.
148
152
  if (input.activityTypes !== undefined && !(Array.isArray(input.activityTypes) && input.activityTypes.length === 0))
149
153
  create.payload.activity_types = input.activityTypes;
150
- if (input.resultRecipient !== undefined)
151
- create.result_recipient = input.resultRecipient;
152
154
  return this.createRegistration(create, idempotencyKey, signal);
153
155
  }
154
156
  async operation(operationId, signal) {
@@ -181,7 +183,7 @@ export class LinkedWallets {
181
183
  * executeSend() with the device share.
182
184
  */
183
185
  async send(input, idempotencyKey, signal) {
184
- const declared = record(input, ['linkedWalletId', 'network', 'asset', 'recipient', 'amountRaw'], ['comment', 'resultRecipient']);
186
+ const declared = record(input, ['linkedWalletId', 'network', 'asset', 'recipient', 'amountRaw'], ['comment']);
185
187
  requestID(idempotencyKey);
186
188
  if (!uuid(declared.linkedWalletId) || !network(declared.network) || !linkedSendAsset(declared.asset) || typeof declared.recipient !== 'string' || !/^[\x21-\x7e]{1,256}$/.test(declared.recipient) ||
187
189
  !canonicalDecimal(declared.amountRaw) || declared.amountRaw === '0' || declared.amountRaw.length > 78 ||
@@ -192,8 +194,6 @@ export class LinkedWallets {
192
194
  if (declared.comment !== undefined)
193
195
  sent.comment = declared.comment;
194
196
  const create = { kind: 'wallet.send', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: declared.linkedWalletId, payload: sent };
195
- if (declared.resultRecipient !== undefined)
196
- create.result_recipient = recipient(declared.resultRecipient);
197
197
  const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
198
198
  const operation = parseUserWalletOperationEnvelope(payload).operation;
199
199
  const shown = operation.display;
@@ -214,14 +214,12 @@ export class LinkedWallets {
214
214
  * transferStatus() of both.
215
215
  */
216
216
  async replaceSend(input, idempotencyKey, signal) {
217
- const declared = record(input, ['linkedWalletId', 'operationId', 'action'], ['resultRecipient']);
217
+ const declared = record(input, ['linkedWalletId', 'operationId', 'action']);
218
218
  requestID(idempotencyKey);
219
219
  if (!uuid(declared.linkedWalletId) || !opaqueId(declared.operationId) || !replaceAction(declared.action))
220
220
  throw new ClientFlowError('invalid_operation_create');
221
221
  const create = { kind: 'wallet.send', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: declared.linkedWalletId,
222
222
  payload: { action: declared.action, replaces_operation_id: declared.operationId } };
223
- if (declared.resultRecipient !== undefined)
224
- create.result_recipient = recipient(declared.resultRecipient);
225
223
  const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
226
224
  const operation = parseUserWalletOperationEnvelope(payload).operation;
227
225
  const shown = operation.display;
@@ -267,15 +265,26 @@ export class LinkedWallets {
267
265
  }
268
266
  /**
269
267
  * Executes a prepared send, on the owner's confirm, with the device share
270
- * resealed for the own-wallet system (resealDeviceShare). The provider
271
- * cannot open the share. A send never prepared is refused with
272
- * VERSION_CONFLICT, an expired one with OPERATION_EXPIRED.
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.
273
274
  */
274
275
  async executeSend(operationId, version, idempotencyKey, deviceShare, signal) {
275
276
  requestID(operationId);
276
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
+ }
277
286
  const share = record(deviceShare, ['ciphertext', 'nonce', 'ephemeralPublicKey', 'encryptionAlgorithm', 'encryptionContext']);
278
- if (!canonicalDecimal(version) || ![share.ciphertext, share.nonce, share.ephemeralPublicKey, share.encryptionContext].every(hex) || typeof share.encryptionAlgorithm !== 'string')
287
+ if (![share.ciphertext, share.nonce, share.ephemeralPublicKey, share.encryptionContext].every(hex) || typeof share.encryptionAlgorithm !== 'string')
279
288
  throw new ClientFlowError('invalid_execute_request');
280
289
  const body = { device_share: { ciphertext: share.ciphertext, nonce: share.nonce, ephemeral_public_key: share.ephemeralPublicKey, encryption_algorithm: share.encryptionAlgorithm, encryption_context: share.encryptionContext } };
281
290
  const operation = parseUserWalletOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/execute`, 'POST', body, signal, idempotencyKey, version)).payload).operation;
@@ -283,6 +292,64 @@ export class LinkedWallets {
283
292
  fault();
284
293
  return operation;
285
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
+ }
286
353
  /**
287
354
  * What became of the transfer a send prepared: on its way, confirmed by the
288
355
  * network, or failed and why. The send's operation succeeds when the
@@ -323,6 +390,8 @@ export class LinkedWallets {
323
390
  fault();
324
391
  return operation;
325
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)); }
326
395
  }
327
396
  export function parseWalletConnection(payload) {
328
397
  const r = record(payload, ['request_id', 'id', 'connector_id', 'state', 'created_at', 'last_synced_at'], ['revoked_at']);
@@ -365,107 +434,3 @@ export function validateWalletLinkAssertion(value) {
365
434
  throw new ClientFlowError('invalid_linked_wallet_request');
366
435
  return value;
367
436
  }
368
- export function validateLinkedRegistrationCreate(value, ownerId) {
369
- const r = record(value, ['kind', 'owner_type', 'owner_id', 'linked_wallet_id', 'payload'], ['result_recipient']);
370
- const p = record(r.payload, ['network'], ['assets', 'activity_types']);
371
- if (r.kind !== 'streams.register' || r.owner_type !== 'account' || !opaqueId(r.owner_id) || r.owner_id !== ownerId || !uuid(r.linked_wallet_id) || !network(p.network) || (p.assets !== undefined && !assets(p.assets)) || (p.activity_types !== undefined && !activityTypes(p.activity_types)))
372
- throw new ClientFlowError('invalid_operation_create');
373
- if (r.result_recipient !== undefined)
374
- recipient(r.result_recipient);
375
- return value;
376
- }
377
- export function parseLinkedRegistrationOperationEnvelope(payload) {
378
- const r = record(payload, ['request_id', 'operation']);
379
- if (!opaqueId(r.request_id))
380
- fault();
381
- return { requestId: r.request_id, operation: parseLinkedRegistrationOperation(r.operation) };
382
- }
383
- export function parseLinkedRegistrationOperation(payload) {
384
- const r = record(payload, ['id', 'version', 'kind', 'state', 'effect_state', 'authorization_basis', 'owner_type', 'owner_id', 'requester_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'request_fingerprint', 'operation_fingerprint', 'display', 'created_at', 'expires_at', 'next_action'], ['confirmation_location']);
385
- if (!opaqueId(r.id) || !canonicalDecimal(r.version) || r.kind !== 'streams.register' || r.owner_type !== 'account' || !opaqueId(r.owner_id) || !opaqueId(r.requester_id) || r.binding_source !== 'linked_wallet' || !uuid(r.connection_id) || !uuid(r.linked_wallet_id) || !safeRevision(r.binding_revision) || !digest(r.request_fingerprint) || !digest(r.operation_fingerprint) || !rfc3339UTC(r.created_at) || !rfc3339UTC(r.expires_at) || !deadlineAfter(r.created_at, r.expires_at) || !isNextAction(r.next_action) || !['awaiting_confirmation', 'authorized', 'dispatching', 'outcome_unknown', 'succeeded', 'failed', 'denied', 'canceled', 'expired'].includes(r.state) || !['no_effect', 'possible', 'known'].includes(r.effect_state) || !['', 'user_confirmed', 'session_authorized'].includes(r.authorization_basis) || (r.confirmation_location !== undefined && !https(r.confirmation_location)))
386
- fault();
387
- const purged = typeof r.display === 'object' && r.display !== null && 'material_purged' in r.display;
388
- const d = record(r.display, ['kind', 'owner_id', 'requester_id', 'result_recipient', ...(purged ? ['material_purged'] : ['binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'assets', 'network', 'address', 'streams_mapping_id', 'streams_mapping_version', 'target_customer_id', 'target_project_id', 'target_environment_id'])]);
389
- if (d.kind !== r.kind || d.owner_id !== r.owner_id || d.requester_id !== r.requester_id)
390
- fault();
391
- const common = { kind: 'streams.register', ownerId: r.owner_id, requesterId: r.requester_id, resultRecipient: recipient(d.result_recipient) };
392
- let display;
393
- if (purged) {
394
- if (d.material_purged !== true)
395
- fault();
396
- display = { ...common, materialPurged: true };
397
- }
398
- else {
399
- if (d.binding_source !== r.binding_source || d.connection_id !== r.connection_id || d.linked_wallet_id !== r.linked_wallet_id || d.binding_revision !== r.binding_revision || !assets(d.assets) || !network(d.network) || !text(d.address, 256) || !safeRevision(d.streams_mapping_version) || ![d.streams_mapping_id, d.target_customer_id, d.target_project_id, d.target_environment_id].every(opaqueId))
400
- fault();
401
- display = { ...common, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, assets: Object.freeze([...d.assets]), network: d.network, address: d.address, streamsMappingId: d.streams_mapping_id, streamsMappingVersion: d.streams_mapping_version, targetCustomerId: d.target_customer_id, targetProjectId: d.target_project_id, targetEnvironmentId: d.target_environment_id };
402
- }
403
- const operation = { id: r.id, version: r.version, kind: 'streams.register', state: r.state, effectState: r.effect_state, authorizationBasis: r.authorization_basis, ownerType: 'account', ownerId: r.owner_id, requesterId: r.requester_id, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, requestFingerprint: r.request_fingerprint, operationFingerprint: r.operation_fingerprint, display, createdAt: r.created_at, expiresAt: r.expires_at, nextAction: r.next_action };
404
- if (r.confirmation_location !== undefined)
405
- operation.confirmationLocation = r.confirmation_location;
406
- return operation;
407
- }
408
- export function parseLinkedRegistrationResult(payload, operationId) {
409
- const r = record(payload, ['request_id', 'result']);
410
- const v = record(r.result, ['kind', 'operation_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'source_registration_id', 'registration_digest', 'receipt_id']);
411
- if (!opaqueId(r.request_id) || !opaqueId(v.operation_id) || v.operation_id !== operationId || v.kind !== 'streams_registration' || v.binding_source !== 'linked_wallet' || !uuid(v.connection_id) || !uuid(v.linked_wallet_id) || !safeRevision(v.binding_revision) || !text(v.source_registration_id, 128) || !text(v.receipt_id, 128) || !digest(v.registration_digest))
412
- fault();
413
- return { requestId: r.request_id, result: { kind: 'streams_registration', operationId: v.operation_id, bindingSource: 'linked_wallet', connectionId: v.connection_id, linkedWalletId: v.linked_wallet_id, bindingRevision: v.binding_revision, sourceRegistrationId: v.source_registration_id, registrationDigest: v.registration_digest, receiptId: v.receipt_id } };
414
- }
415
- export function linkedRegistrationsPath(linkedWalletId, options = {}) {
416
- requestUUID(linkedWalletId);
417
- const declared = record(options, [], ['afterRegistrationId', 'limit', 'signal']);
418
- const query = new URLSearchParams();
419
- if (declared.afterRegistrationId !== undefined) {
420
- requestUUID(declared.afterRegistrationId);
421
- query.set('after_registration_id', declared.afterRegistrationId);
422
- }
423
- readLimit(query, declared.limit);
424
- return linkedReadPath(linkedWalletId, 'registrations', query);
425
- }
426
- export function linkedEventsPath(linkedWalletId, options = {}) {
427
- requestUUID(linkedWalletId);
428
- const declared = record(options, [], ['registrationId', 'cursor', 'limit', 'eventTypes', 'signal']);
429
- const query = new URLSearchParams();
430
- if (declared.registrationId !== undefined) {
431
- requestUUID(declared.registrationId);
432
- query.set('registration_id', declared.registrationId);
433
- }
434
- if (declared.cursor !== undefined) {
435
- if (typeof declared.cursor !== 'string' || declared.cursor.length < 1 || declared.cursor.length > MaxStreamsCursorLength)
436
- throw new ClientFlowError('invalid_streams_request');
437
- query.set('cursor', declared.cursor);
438
- }
439
- readLimit(query, declared.limit);
440
- appendEventTypes(query, declared.eventTypes);
441
- return linkedReadPath(linkedWalletId, 'events', query);
442
- }
443
- export function parseLinkedStreamsRegistrationList(payload, linkedWalletId) {
444
- const root = record(payload, ['items', 'has_more'], ['next_after_registration_id']);
445
- if (!Array.isArray(root.items) || root.items.length > MaxStreamsPageLimit || typeof root.has_more !== 'boolean' || root.has_more !== Object.hasOwn(root, 'next_after_registration_id'))
446
- fault();
447
- const items = root.items.map(item => parseLinkedStreamsRegistration(item, linkedWalletId));
448
- if (items.some((item, index) => index > 0 && items[index - 1].registrationId >= item.registrationId))
449
- fault();
450
- if (root.has_more && (!uuid(root.next_after_registration_id) || items.length === 0 || root.next_after_registration_id !== items.at(-1).registrationId))
451
- fault();
452
- const page = { items: Object.freeze(items), hasMore: root.has_more };
453
- return Object.freeze(root.has_more ? { ...page, nextAfterRegistrationId: root.next_after_registration_id } : page);
454
- }
455
- function parseLinkedStreamsRegistration(payload, linkedWalletId) {
456
- const r = record(payload, ['registration_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'source_registration_id', 'network', 'address', 'assets', 'status', 'effective_from', 'revision', 'source_version']);
457
- if (!uuid(r.registration_id) || r.binding_source !== 'linked_wallet' || !uuid(r.connection_id) || !uuid(r.linked_wallet_id) || r.linked_wallet_id !== linkedWalletId || !safeRevision(r.binding_revision) || !opaqueId(r.source_registration_id) || !network(r.network) || typeof r.address !== 'string' || !/^[A-Za-z0-9_:-]{1,256}$/.test(r.address) || !assets(r.assets) || !['pending', 'active'].includes(r.status) || typeof r.effective_from !== 'string' || new TextEncoder().encode(r.effective_from).length < 1 || new TextEncoder().encode(r.effective_from).length > 256 || !integer(r.revision) || r.revision < 1 || typeof r.source_version !== 'string' || !/^[0-9]+\.[0-9]+$/.test(r.source_version))
458
- fault();
459
- return Object.freeze({ registrationId: r.registration_id, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, sourceRegistrationId: r.source_registration_id, network: r.network, address: r.address, assets: Object.freeze([...r.assets]), status: r.status, effectiveFrom: r.effective_from, revision: r.revision, sourceVersion: r.source_version });
460
- }
461
- function linkedReadPath(id, kind, query) {
462
- const path = `/v1/linked-wallets/${id}/${kind}`;
463
- return query.size === 0 ? path : `${path}?${query.toString()}`;
464
- }
465
- function readLimit(query, value) {
466
- if (value === undefined)
467
- return;
468
- if (!integer(value) || value < 1 || value > MaxStreamsPageLimit)
469
- throw new ClientFlowError('invalid_streams_request');
470
- query.set('limit', String(value));
471
- }
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,64 @@
1
+ export interface MobileSecureStorage {
2
+ get(key: string): Promise<string | null>;
3
+ set(key: string, value: string): Promise<void>;
4
+ delete(key: string): Promise<void>;
5
+ }
6
+ export interface MobileAssociationInput {
7
+ redirectUri: string;
8
+ applicationIdentifier: string;
9
+ }
10
+ export interface MobileAssociationVerification {
11
+ source: "os_verified_https_association";
12
+ redirectUri: string;
13
+ applicationIdentifier: string;
14
+ }
15
+ /**
16
+ * Host/native trust boundary. Implementations must ask the operating system to
17
+ * verify the configured HTTPS app/universal-link association. On iOS, where no
18
+ * API reports that verification, the system makes it when an authentication
19
+ * session with an HTTPS callback starts, and refuses to start one for an app
20
+ * the domain does not name; a bridge whose openSystemBrowser opens exactly
21
+ * that session answers for it. This result is a port contract, not
22
+ * cryptographic proof produced by this TypeScript package.
23
+ */
24
+ export interface MobileBrowserBridge {
25
+ verifyAssociatedRedirect(input: MobileAssociationInput): Promise<MobileAssociationVerification | null>;
26
+ /**
27
+ * Opens the URL in the system browser's authentication session
28
+ * (ASWebAuthenticationSession on iOS, a Custom Tab on Android), never an
29
+ * embedded web view, and resolves with the URL that session ended on: the
30
+ * registered app link the provider redirected back to. It resolves with
31
+ * nothing when no app link came back, for example when the person closed
32
+ * the browser. A sign-in needs that URL; the other hosted flows take it
33
+ * from the host through their `complete…` method.
34
+ */
35
+ openSystemBrowser(url: string): Promise<string | void> | string | void;
36
+ }
37
+ /**
38
+ * verifyMobileAssociation asks the host to have the operating system verify
39
+ * the app link of the exact configured redirect for the configured app, and
40
+ * accepts only the exact answer for both. It runs when the client is
41
+ * initialized and again before every sign-in opens the system browser.
42
+ */
43
+ export declare function verifyMobileAssociation(bridge: MobileBrowserBridge, redirectUri: string, applicationIdentifier: string): Promise<void>;
44
+ /** mobileBridgeComplete checks that the host supplied every mobile port. */
45
+ export declare function mobileBridgeComplete(bridge: MobileBrowserBridge | undefined, storage: MobileSecureStorage | undefined): bridge is MobileBrowserBridge;
46
+ /** mobileApplicationIdentifier is a bundle or package identifier as configuration names it. */
47
+ export declare function mobileApplicationIdentifier(value: unknown): value is string;
48
+ /**
49
+ * mobileSignInReturn reads what the system browser handed back from a hosted
50
+ * sign-in: the exact registered redirect with the one-use code and this
51
+ * sign-in's own state, and nothing else. No app link back, a redirect of
52
+ * another sign-in or an attempt's redirect is refused before any exchange.
53
+ */
54
+ export declare function mobileSignInReturn(returned: string | void, redirectUri: string, state: string): {
55
+ code: string;
56
+ state: string;
57
+ };
58
+ /**
59
+ * mobileRefreshKey names the refresh token of one public client in the secure
60
+ * storage. Its parts are opaque identifiers, which hold no dot, so the key is
61
+ * unambiguous, and it uses only letters, digits, dots, hyphens and
62
+ * underscores, which every platform keystore takes as a key.
63
+ */
64
+ export declare function mobileRefreshKey(tenantId: string, applicationId: string, environmentId: string, clientId: string): string;
package/dist/mobile.js ADDED
@@ -0,0 +1,69 @@
1
+ // The mobile host boundary: the ports a native host supplies (the system
2
+ // browser with its verified app link, and the OS secure storage that holds the
3
+ // refresh token), the check of the app link the host reports, and the return
4
+ // of a sign-in from the system browser's authentication session.
5
+ //
6
+ // A mobile app sends no browser Origin. The provider binds its sign-in to the
7
+ // exact registered redirect, an HTTPS app link (iOS universal link, Android
8
+ // app link) on one of the client's registered origins, and to the state and
9
+ // the S256 verifier this client keeps in memory. The browser comes back to the
10
+ // app with the one-use code and the state alone; the access token stays in
11
+ // memory and the refresh token goes only to `MobileSecureStorage`.
12
+ import { ClientFlowError } from "./auth.js";
13
+ import { handoffCallback } from "./redirects.js";
14
+ /**
15
+ * verifyMobileAssociation asks the host to have the operating system verify
16
+ * the app link of the exact configured redirect for the configured app, and
17
+ * accepts only the exact answer for both. It runs when the client is
18
+ * initialized and again before every sign-in opens the system browser.
19
+ */
20
+ export async function verifyMobileAssociation(bridge, redirectUri, applicationIdentifier) {
21
+ let verification;
22
+ try {
23
+ verification = await bridge.verifyAssociatedRedirect({ redirectUri, applicationIdentifier });
24
+ }
25
+ catch {
26
+ throw new ClientFlowError("mobile_association_unverified");
27
+ }
28
+ if (!exactMobileAssociation(verification, redirectUri, applicationIdentifier))
29
+ throw new ClientFlowError("mobile_association_unverified");
30
+ }
31
+ /** mobileBridgeComplete checks that the host supplied every mobile port. */
32
+ export function mobileBridgeComplete(bridge, storage) {
33
+ return Boolean(storage) && Boolean(bridge) && typeof bridge?.verifyAssociatedRedirect === "function" && typeof bridge?.openSystemBrowser === "function";
34
+ }
35
+ /** mobileApplicationIdentifier is a bundle or package identifier as configuration names it. */
36
+ export function mobileApplicationIdentifier(value) {
37
+ return typeof value === "string" && /^[A-Za-z0-9._-]{1,255}$/.test(value);
38
+ }
39
+ /**
40
+ * mobileSignInReturn reads what the system browser handed back from a hosted
41
+ * sign-in: the exact registered redirect with the one-use code and this
42
+ * sign-in's own state, and nothing else. No app link back, a redirect of
43
+ * another sign-in or an attempt's redirect is refused before any exchange.
44
+ */
45
+ export function mobileSignInReturn(returned, redirectUri, state) {
46
+ if (typeof returned !== "string")
47
+ throw new ClientFlowError("mobile_redirect_unavailable");
48
+ const callback = handoffCallback(returned, redirectUri);
49
+ if (callback.attemptId !== undefined)
50
+ throw new ClientFlowError("invalid_callback");
51
+ if (callback.state !== state)
52
+ throw new ClientFlowError("state_mismatch");
53
+ return { code: callback.code, state: callback.state };
54
+ }
55
+ /**
56
+ * mobileRefreshKey names the refresh token of one public client in the secure
57
+ * storage. Its parts are opaque identifiers, which hold no dot, so the key is
58
+ * unambiguous, and it uses only letters, digits, dots, hyphens and
59
+ * underscores, which every platform keystore takes as a key.
60
+ */
61
+ export function mobileRefreshKey(tenantId, applicationId, environmentId, clientId) {
62
+ return ["authorization", tenantId, applicationId, environmentId, clientId, "refresh"].join(".");
63
+ }
64
+ function exactMobileAssociation(value, redirectUri, applicationIdentifier) {
65
+ if (value === null || typeof value !== "object" || Array.isArray(value))
66
+ return false;
67
+ const result = value;
68
+ return Object.keys(result).length === 3 && result.source === "os_verified_https_association" && result.redirectUri === redirectUri && result.applicationIdentifier === applicationIdentifier;
69
+ }