@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,209 @@
1
+ import { type NextAction } from "./auth.ts";
2
+ import { type DisplayCommon } from "./walletDisplayParts.ts";
3
+ import { type LinkedRecoverCreate, type LinkedRecoverDisplay } from "./linkedRecovery.ts";
4
+ import { type WalletCreateGrant, type WalletCreateGrantOutcome, type WalletCreateGrantWire } from "./walletCreateGrant.ts";
5
+ import { type ConsentDebitOutcome, type DebitTerms } from "./consentDebit.ts";
6
+ import { type LinkedSendDisplay, type LinkedWalletIdentity } from "./linkedSendDisplay.ts";
7
+ import { type MediatedDisplay, type MediatedPurgedDisplay } from "./mediatedOperation.ts";
8
+ export { LinkedSendAssets, MaxTransferCommentBytes, ReplaceActions, linkedSendAsset, replaceAction, transferComment } from "./linkedSendDisplay.ts";
9
+ export type { LinkedSendAsset, LinkedSendDisplay, LinkedWalletIdentity, ReplaceAction } from "./linkedSendDisplay.ts";
10
+ export type { LinkedRecoverCreate, LinkedRecoverDisplay } from "./linkedRecovery.ts";
11
+ export type { MediatedDisplay, MediatedLinkedSendDisplay, MediatedPurgedDisplay, MediatedSendDisplay } from "./mediatedOperation.ts";
12
+ export type UserWalletOperationKind = "wallet.create" | "wallet.sign" | "wallet.send" | "wallet.export" | "wallet.recover";
13
+ export type WalletResultKind = "signature" | "transfer_receipt" | "wallet_created" | "device_share_recovered";
14
+ export type UserWalletState = "awaiting_confirmation" | "authorized" | "dispatching" | "outcome_unknown" | "succeeded" | "failed" | "denied" | "canceled" | "expired";
15
+ export type UserWalletEffectState = "no_effect" | "possible" | "known";
16
+ /**
17
+ * session_authorized: the account's live session authorized the operation
18
+ * with no passkey. A send from a linked wallet, plain or a replacement, is
19
+ * authorized so (owner decision, 14 October 2026), as is the registration of
20
+ * a linked wallet for tracking; every other wallet operation is confirmed
21
+ * with a passkey (user_confirmed). user_request_signed: a service forwarded
22
+ * the send on the person's signed request (Mode C); its display is a
23
+ * MediatedDisplay, with no result recipient.
24
+ */
25
+ export type UserWalletAuthorizationBasis = "" | "user_confirmed" | "session_authorized" | "user_request_signed";
26
+ /**
27
+ * A wallet operation's result goes to its owner account only, so no create
28
+ * body names a result recipient: the device share of a created, exported or
29
+ * recovered wallet reaches this device alone, and the server refuses a
30
+ * service named as the recipient with INVALID_REQUEST.
31
+ */
32
+ type UserWalletCommon = {
33
+ owner_type: "account";
34
+ owner_id: string;
35
+ };
36
+ /**
37
+ * UserWalletOperationCreate is the closed request body of source §2.2: the
38
+ * top-level wallet ID is required for every kind except wallet.create, and
39
+ * each payload carries exactly its kind's fields.
40
+ */
41
+ export type UserWalletOperationCreate = (UserWalletCommon & {
42
+ kind: "wallet.create";
43
+ payload: {
44
+ network: string;
45
+ recipient_public_key: string;
46
+ };
47
+ grant?: WalletCreateGrantWire;
48
+ }) | (UserWalletCommon & {
49
+ kind: "wallet.sign";
50
+ wallet_id: string;
51
+ payload: {
52
+ network: string;
53
+ message_base64url: string;
54
+ };
55
+ }) | (UserWalletCommon & {
56
+ kind: "wallet.send";
57
+ wallet_id: string;
58
+ payload: {
59
+ network: string;
60
+ asset: "native";
61
+ recipient: string;
62
+ amount_raw: string;
63
+ max_fee_raw: string;
64
+ };
65
+ }) | (UserWalletCommon & {
66
+ kind: "wallet.export";
67
+ wallet_id: string;
68
+ payload: {
69
+ recipient_public_key: string;
70
+ };
71
+ }) | (UserWalletCommon & {
72
+ kind: "wallet.recover";
73
+ wallet_id: string;
74
+ payload: {
75
+ expected_generation: string;
76
+ recipient_public_key: string;
77
+ };
78
+ }) | LinkedRecoverCreate;
79
+ /** UserWalletPurgedDisplay is the safe identity left after material retention expires. */
80
+ export type UserWalletPurgedDisplay = DisplayCommon & {
81
+ kind: UserWalletOperationKind;
82
+ materialPurged: true;
83
+ };
84
+ /**
85
+ * UserWalletDisplay is the safe display of the frozen material. A purged
86
+ * record keeps its identity and reports materialPurged instead of inventing
87
+ * material; it never carries signing output or custody material. Narrow with
88
+ * `walletMaterialPurged` before reading any per-kind material field.
89
+ */
90
+ export type UserWalletDisplay = UserWalletPurgedDisplay | (DisplayCommon & {
91
+ kind: "wallet.create";
92
+ network: string;
93
+ recipientPublicKey: string;
94
+ preparedSummary?: string;
95
+ grant?: WalletCreateGrant;
96
+ debit?: DebitTerms;
97
+ }) | (DisplayCommon & {
98
+ kind: "wallet.sign";
99
+ network: string;
100
+ messageBase64url: string;
101
+ preparedSummary?: string;
102
+ }) | (DisplayCommon & {
103
+ kind: "wallet.send";
104
+ network: string;
105
+ asset: string;
106
+ recipient: string;
107
+ amountRaw: string;
108
+ maxFeeRaw: string;
109
+ preparedSummary?: string;
110
+ }) | LinkedSendDisplay | (DisplayCommon & {
111
+ kind: "wallet.export";
112
+ recipientPublicKey: string;
113
+ preparedSummary?: string;
114
+ }) | (DisplayCommon & {
115
+ kind: "wallet.recover";
116
+ recipientPublicKey: string;
117
+ expectedGeneration: string;
118
+ preparedSummary?: string;
119
+ }) | LinkedRecoverDisplay | MediatedDisplay;
120
+ /** walletMaterialPurged separates the purged display from every material display. */
121
+ export declare function walletMaterialPurged(display: UserWalletDisplay): display is UserWalletPurgedDisplay | MediatedPurgedDisplay;
122
+ export interface UserWalletOperation {
123
+ id: string;
124
+ version: string;
125
+ kind: UserWalletOperationKind;
126
+ state: UserWalletState;
127
+ effectState: UserWalletEffectState;
128
+ authorizationBasis: UserWalletAuthorizationBasis;
129
+ ownerType: "account";
130
+ ownerId: string;
131
+ requesterId: string;
132
+ walletId?: string;
133
+ /** Present exactly when the operation names a linked wallet instead of walletId. */
134
+ linkedWallet?: LinkedWalletIdentity;
135
+ requestFingerprint: string;
136
+ operationFingerprint: string;
137
+ display: UserWalletDisplay;
138
+ /** UTC RFC 3339 date-time string (source §2.2), not NumericDate seconds. */
139
+ createdAt: string;
140
+ /** UTC RFC 3339 date-time string (source §2.2), not NumericDate seconds. */
141
+ expiresAt: string;
142
+ confirmationLocation?: string;
143
+ resultKind?: WalletResultKind;
144
+ nextAction: NextAction;
145
+ }
146
+ /** CreatedWallet is the public identity of a wallet the own-wallet system created. */
147
+ export interface CreatedWallet {
148
+ walletId: string;
149
+ keyId: string;
150
+ network: string;
151
+ address: string;
152
+ algorithm: string;
153
+ publicKey: string;
154
+ }
155
+ /**
156
+ * DeviceSharePackage is the device share exactly as the own-wallet system
157
+ * sealed it to the recipient key frozen in the operation. The provider never
158
+ * opens it; only the holder of the matching device private key can. Every
159
+ * binary member is lowercase hexadecimal.
160
+ */
161
+ export interface DeviceSharePackage {
162
+ ciphertext: string;
163
+ nonce: string;
164
+ ephemeralPublicKey: string;
165
+ encryptionAlgorithm: string;
166
+ encryptionContext: string;
167
+ }
168
+ export interface WalletResult {
169
+ requestId: string;
170
+ result: {
171
+ operationId: string;
172
+ kind: WalletResultKind;
173
+ receiptId: string;
174
+ outputBase64url?: string;
175
+ /** Present exactly for a wallet_created result. */
176
+ wallet?: CreatedWallet;
177
+ /** Present exactly for a wallet_created and a device_share_recovered result. */
178
+ deviceShare?: DeviceSharePackage;
179
+ /** Present exactly for a device_share_recovered result: the linked wallet whose share was recovered. */
180
+ linkedWalletId?: string;
181
+ /** Present for a wallet_created result whose creation named a grant (walletCreateGrant.ts). */
182
+ grant?: WalletCreateGrantOutcome;
183
+ /** Present when the grant's service holds wallet:debit:user: the debit permission the creation's passkey also created (consentDebit.ts). */
184
+ debit?: ConsentDebitOutcome;
185
+ };
186
+ }
187
+ /** MaxWalletMessageBytes is the decoded wallet.sign payload bound of source §2.8. */
188
+ export declare const MaxWalletMessageBytes = 4096;
189
+ /**
190
+ * validateUserWalletCreate rejects every locally detectable fault before any
191
+ * request leaves the client: a foreign owner, a wallet ID present or absent
192
+ * against its kind, a non-compressed recipient key, a noncanonical or
193
+ * out-of-range quantity, a non-native asset, and a sign message outside
194
+ * 1..4096 decoded bytes. It returns the exact value it was given so the
195
+ * caller sends the same closed body it validated.
196
+ */
197
+ export declare function validateUserWalletCreate(value: UserWalletOperationCreate, expectedOwnerId: string): UserWalletOperationCreate;
198
+ export declare function parseUserWalletOperationEnvelope(payload: unknown): {
199
+ requestId: string;
200
+ operation: UserWalletOperation;
201
+ };
202
+ export declare function parseUserWalletOperation(payload: unknown): UserWalletOperation;
203
+ /**
204
+ * parseWalletResult accepts the protected wallet output of source §2.2:
205
+ * output_base64url is present for a signature and forbidden for a transfer
206
+ * receipt. A wallet_created result is its own closed shape: the created
207
+ * wallet and its sealed device share, with no output member.
208
+ */
209
+ export declare function parseWalletResult(payload: unknown, expectedOperationId: string): WalletResult;
@@ -0,0 +1,317 @@
1
+ // Mode B user wallet operations (AUTH-009-A03, source §1.3). Closed types,
2
+ // client-side validation of the five wallet payloads, and strict parsers for
3
+ // the safe status projection and the protected wallet result. These helpers
4
+ // compose the same /v1 operation routes as the local operations; they add no
5
+ // authority. Local faults raise ClientFlowError; server denials stay
6
+ // AuthorizationError and are produced by the caller's transport.
7
+ import { ClientFlowError, canonicalDecimal, deadlineAfter, isNextAction, opaqueId, record, rfc3339UTC, } from "./auth.js";
8
+ import { bindingRevision, displayCommon, lowerHexBytes, lowerUUID, rawQuantity, summaryOf, walletRecipient } from "./walletDisplayParts.js";
9
+ import { parseLinkedRecoverDisplay, parseRecoveredShareResult, validLinkedRecoverCreate } from "./linkedRecovery.js";
10
+ import { parseWalletCreateGrant, parseWalletCreateGrantOutcome, validWalletCreateGrant } from "./walletCreateGrant.js";
11
+ import { parseConsentDebitOutcome, parseDebitTerms } from "./consentDebit.js";
12
+ import { parseLinkedSendDisplay } from "./linkedSendDisplay.js";
13
+ import { parseMediatedOperation } from "./mediatedOperation.js";
14
+ export { LinkedSendAssets, MaxTransferCommentBytes, ReplaceActions, linkedSendAsset, replaceAction, transferComment } from "./linkedSendDisplay.js";
15
+ /** walletMaterialPurged separates the purged display from every material display. */
16
+ export function walletMaterialPurged(display) {
17
+ return "materialPurged" in display;
18
+ }
19
+ /** MaxWalletMessageBytes is the decoded wallet.sign payload bound of source §2.8. */
20
+ export const MaxWalletMessageBytes = 4096;
21
+ const maxWalletGeneration = (1n << 64n) - 1n;
22
+ const compressedP256Key = /^(02|03)[0-9a-f]{64}$/;
23
+ const walletNetwork = /^[A-Za-z0-9_.-]{1,64}$/;
24
+ const base64urlValue = /^[A-Za-z0-9_-]+$/;
25
+ /**
26
+ * validateUserWalletCreate rejects every locally detectable fault before any
27
+ * request leaves the client: a foreign owner, a wallet ID present or absent
28
+ * against its kind, a non-compressed recipient key, a noncanonical or
29
+ * out-of-range quantity, a non-native asset, and a sign message outside
30
+ * 1..4096 decoded bytes. It returns the exact value it was given so the
31
+ * caller sends the same closed body it validated.
32
+ */
33
+ export function validateUserWalletCreate(value, expectedOwnerId) {
34
+ const common = record(value, ["kind", "owner_type", "owner_id", "payload"], ["wallet_id", "linked_wallet_id", "grant"]);
35
+ // A grant is named beside a creation alone.
36
+ if (common.owner_type !== "account" || !opaqueId(common.owner_id) || common.owner_id !== expectedOwnerId ||
37
+ (common.grant !== undefined && (common.kind !== "wallet.create" || !validWalletCreateGrant(common.grant)))) {
38
+ throw new ClientFlowError("invalid_operation_create");
39
+ }
40
+ // A linked wallet is named by a recovery of its device share alone (linkedRecovery.ts).
41
+ if (common.linked_wallet_id !== undefined) {
42
+ if (!validLinkedRecoverCreate(common))
43
+ throw new ClientFlowError("invalid_operation_create");
44
+ return value;
45
+ }
46
+ const walletRequired = common.kind !== "wallet.create";
47
+ if (walletRequired !== opaqueId(common.wallet_id))
48
+ throw new ClientFlowError("invalid_operation_create");
49
+ switch (common.kind) {
50
+ case "wallet.create": {
51
+ const payload = record(common.payload, ["network", "recipient_public_key"]);
52
+ if (!network(payload.network) || !recipientPublicKey(payload.recipient_public_key))
53
+ throw new ClientFlowError("invalid_operation_create");
54
+ break;
55
+ }
56
+ case "wallet.sign": {
57
+ const payload = record(common.payload, ["network", "message_base64url"]);
58
+ if (!network(payload.network) || !boundedMessage(payload.message_base64url))
59
+ throw new ClientFlowError("invalid_operation_create");
60
+ break;
61
+ }
62
+ case "wallet.send": {
63
+ const payload = record(common.payload, ["network", "asset", "recipient", "amount_raw", "max_fee_raw"]);
64
+ if (!network(payload.network) || payload.asset !== "native" || typeof payload.recipient !== "string" || !walletRecipient.test(payload.recipient) ||
65
+ !rawQuantity(payload.amount_raw, true) || !rawQuantity(payload.max_fee_raw, false)) {
66
+ throw new ClientFlowError("invalid_operation_create");
67
+ }
68
+ break;
69
+ }
70
+ case "wallet.export": {
71
+ const payload = record(common.payload, ["recipient_public_key"]);
72
+ if (!recipientPublicKey(payload.recipient_public_key))
73
+ throw new ClientFlowError("invalid_operation_create");
74
+ break;
75
+ }
76
+ case "wallet.recover": {
77
+ const payload = record(common.payload, ["expected_generation", "recipient_public_key"]);
78
+ if (!recipientPublicKey(payload.recipient_public_key) || !walletGeneration(payload.expected_generation))
79
+ throw new ClientFlowError("invalid_operation_create");
80
+ break;
81
+ }
82
+ default:
83
+ throw new ClientFlowError("invalid_operation_create");
84
+ }
85
+ return value;
86
+ }
87
+ export function parseUserWalletOperationEnvelope(payload) {
88
+ const root = record(payload, ["request_id", "operation"]);
89
+ if (!opaqueId(root.request_id))
90
+ throw new ClientFlowError("invalid_server_response");
91
+ return { requestId: root.request_id, operation: parseUserWalletOperation(root.operation) };
92
+ }
93
+ export function parseUserWalletOperation(payload) {
94
+ // A Mode C send is read in the mediated projection the provider serves for it.
95
+ if (payload !== null && typeof payload === "object" && payload.authorization_basis === "user_request_signed")
96
+ return parseMediatedOperation(payload);
97
+ const root = record(payload, [
98
+ "id", "version", "kind", "state", "effect_state", "authorization_basis", "owner_type", "owner_id", "requester_id",
99
+ "request_fingerprint", "operation_fingerprint", "display", "created_at", "expires_at", "next_action",
100
+ ], ["wallet_id", "confirmation_location", "result_kind", "binding_source", "connection_id", "linked_wallet_id", "binding_revision"]);
101
+ if (!opaqueId(root.id) || !canonicalDecimal(root.version) || !userWalletKind(root.kind) || !userWalletState(root.state) ||
102
+ !userWalletEffectState(root.effect_state) || !userWalletBasis(root.authorization_basis) ||
103
+ root.owner_type !== "account" || !opaqueId(root.owner_id) || !opaqueId(root.requester_id) ||
104
+ (root.wallet_id !== undefined && !opaqueId(root.wallet_id)) || !lowerHexDigest(root.request_fingerprint) ||
105
+ !lowerHexDigest(root.operation_fingerprint) || !rfc3339UTC(root.created_at) || !rfc3339UTC(root.expires_at) ||
106
+ !deadlineAfter(root.created_at, root.expires_at) || (root.confirmation_location !== undefined && !httpsURL(root.confirmation_location)) ||
107
+ (root.result_kind !== undefined && !walletResultKind(root.result_kind)) || !isNextAction(root.next_action)) {
108
+ throw new ClientFlowError("invalid_server_response");
109
+ }
110
+ // A linked wallet is named by all three of its members, only for a send or
111
+ // the recovery of its device share, never beside an own wallet; only a send
112
+ // is authorized by the session alone.
113
+ const linked = root.binding_source !== undefined;
114
+ if (linked && (root.binding_source !== "linked_wallet" || (root.kind !== "wallet.send" && root.kind !== "wallet.recover") || root.wallet_id !== undefined ||
115
+ !lowerUUID(root.connection_id) || !lowerUUID(root.linked_wallet_id) || !bindingRevision(root.binding_revision) ||
116
+ (root.kind === "wallet.recover" && root.authorization_basis === "session_authorized"))) {
117
+ throw new ClientFlowError("invalid_server_response");
118
+ }
119
+ if (!linked && (root.connection_id !== undefined || root.linked_wallet_id !== undefined || root.binding_revision !== undefined || root.authorization_basis === "session_authorized")) {
120
+ throw new ClientFlowError("invalid_server_response");
121
+ }
122
+ const display = linked && !purgedDisplay(root.display) ? (root.kind === "wallet.recover" ? parseLinkedRecoverDisplay(root.display) : parseLinkedSendDisplay(root.display)) : parseUserWalletDisplay(root.display, root.kind);
123
+ if (display.ownerId !== root.owner_id || display.requesterId !== root.requester_id || display.walletId !== root.wallet_id) {
124
+ throw new ClientFlowError("invalid_server_response");
125
+ }
126
+ if ("linkedWalletId" in display && (display.linkedWalletId !== root.linked_wallet_id || display.connectionId !== root.connection_id || display.bindingRevision !== root.binding_revision)) {
127
+ throw new ClientFlowError("invalid_server_response");
128
+ }
129
+ const operation = {
130
+ id: root.id,
131
+ version: root.version,
132
+ kind: root.kind,
133
+ state: root.state,
134
+ effectState: root.effect_state,
135
+ authorizationBasis: root.authorization_basis,
136
+ ownerType: root.owner_type,
137
+ ownerId: root.owner_id,
138
+ requesterId: root.requester_id,
139
+ requestFingerprint: root.request_fingerprint,
140
+ operationFingerprint: root.operation_fingerprint,
141
+ display,
142
+ createdAt: root.created_at,
143
+ expiresAt: root.expires_at,
144
+ nextAction: root.next_action,
145
+ };
146
+ if (root.wallet_id !== undefined)
147
+ operation.walletId = root.wallet_id;
148
+ if (linked)
149
+ operation.linkedWallet = { connectionId: root.connection_id, linkedWalletId: root.linked_wallet_id, bindingRevision: root.binding_revision };
150
+ if (root.confirmation_location !== undefined)
151
+ operation.confirmationLocation = root.confirmation_location;
152
+ if (root.result_kind !== undefined)
153
+ operation.resultKind = root.result_kind;
154
+ return operation;
155
+ }
156
+ /**
157
+ * parseWalletResult accepts the protected wallet output of source §2.2:
158
+ * output_base64url is present for a signature and forbidden for a transfer
159
+ * receipt. A wallet_created result is its own closed shape: the created
160
+ * wallet and its sealed device share, with no output member.
161
+ */
162
+ export function parseWalletResult(payload, expectedOperationId) {
163
+ const root = record(payload, ["request_id", "result"]);
164
+ const resultKind = root.result !== null && typeof root.result === "object" ? root.result.kind : undefined;
165
+ if (resultKind === "wallet_created")
166
+ return parseCreatedWalletResult(root.request_id, root.result, expectedOperationId);
167
+ if (resultKind === "device_share_recovered")
168
+ return parseRecoveredShareResult(root.request_id, root.result, expectedOperationId);
169
+ const result = record(root.result, ["operation_id", "kind", "receipt_id"], ["output_base64url"]);
170
+ if (!opaqueId(root.request_id) || !opaqueId(result.operation_id) || result.operation_id !== expectedOperationId ||
171
+ !walletResultKind(result.kind) || result.kind === "wallet_created" || result.kind === "device_share_recovered" || !opaqueId(result.receipt_id)) {
172
+ throw new ClientFlowError("invalid_server_response");
173
+ }
174
+ if (result.kind === "signature") {
175
+ if (typeof result.output_base64url !== "string" || !base64urlValue.test(result.output_base64url))
176
+ throw new ClientFlowError("invalid_server_response");
177
+ return { requestId: root.request_id, result: { operationId: result.operation_id, kind: result.kind, receiptId: result.receipt_id, outputBase64url: result.output_base64url } };
178
+ }
179
+ if (result.output_base64url !== undefined)
180
+ throw new ClientFlowError("invalid_server_response");
181
+ return { requestId: root.request_id, result: { operationId: result.operation_id, kind: result.kind, receiptId: result.receipt_id } };
182
+ }
183
+ function parseCreatedWalletResult(requestId, value, expectedOperationId) {
184
+ const result = record(value, ["operation_id", "kind", "receipt_id", "wallet", "device_share"], ["grant", "debit"]);
185
+ const wallet = record(result.wallet, ["wallet_id", "key_id", "network", "address", "algorithm", "public_key"]);
186
+ const share = record(result.device_share, ["ciphertext", "nonce", "ephemeral_public_key", "encryption_algorithm", "encryption_context"]);
187
+ // The receipt of a creation is the created wallet ID; a result naming two
188
+ // different wallets is not one creation.
189
+ if (!opaqueId(requestId) || result.operation_id !== expectedOperationId || !lowerUUID(wallet.wallet_id) || result.receipt_id !== wallet.wallet_id ||
190
+ !lowerUUID(wallet.key_id) || typeof wallet.network !== "string" || !/^[a-z0-9_.-]{1,64}$/.test(wallet.network) ||
191
+ typeof wallet.address !== "string" || !walletRecipient.test(wallet.address) || !algorithmName(wallet.algorithm) || !lowerHexBytes(wallet.public_key) ||
192
+ !lowerHexBytes(share.ciphertext) || !lowerHexBytes(share.nonce) || !lowerHexBytes(share.ephemeral_public_key) ||
193
+ !algorithmName(share.encryption_algorithm) || !lowerHexBytes(share.encryption_context)) {
194
+ throw new ClientFlowError("invalid_server_response");
195
+ }
196
+ return {
197
+ requestId,
198
+ result: {
199
+ operationId: expectedOperationId, kind: "wallet_created", receiptId: wallet.wallet_id,
200
+ wallet: { walletId: wallet.wallet_id, keyId: wallet.key_id, network: wallet.network, address: wallet.address, algorithm: wallet.algorithm, publicKey: wallet.public_key },
201
+ deviceShare: { ciphertext: share.ciphertext, nonce: share.nonce, ephemeralPublicKey: share.ephemeral_public_key, encryptionAlgorithm: share.encryption_algorithm, encryptionContext: share.encryption_context },
202
+ ...(result.grant === undefined ? {} : { grant: parseWalletCreateGrantOutcome(result.grant) }),
203
+ ...(result.debit === undefined ? {} : { debit: parseConsentDebitOutcome(result.debit) }),
204
+ },
205
+ };
206
+ }
207
+ function purgedDisplay(value) {
208
+ return value !== null && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, "material_purged");
209
+ }
210
+ function parseUserWalletDisplay(value, kind) {
211
+ if (value === null || typeof value !== "object" || Array.isArray(value))
212
+ throw new ClientFlowError("invalid_server_response");
213
+ if (Object.prototype.hasOwnProperty.call(value, "material_purged")) {
214
+ const purged = record(value, ["kind", "owner_id", "requester_id", "result_recipient", "material_purged"], ["wallet_id"]);
215
+ if (purged.kind !== kind || purged.material_purged !== true)
216
+ throw new ClientFlowError("invalid_server_response");
217
+ return { ...displayCommon(purged), kind, materialPurged: true };
218
+ }
219
+ switch (kind) {
220
+ case "wallet.create": {
221
+ const display = record(value, ["kind", "owner_id", "requester_id", "result_recipient", "network", "recipient_public_key"], ["wallet_id", "prepared_summary", "grant", "debit"]);
222
+ if (display.kind !== kind || !network(display.network) || !recipientPublicKey(display.recipient_public_key))
223
+ throw new ClientFlowError("invalid_server_response");
224
+ return { ...displayCommon(display), kind, network: display.network, recipientPublicKey: display.recipient_public_key, ...summaryOf(display.prepared_summary),
225
+ ...(display.grant === undefined ? {} : { grant: parseWalletCreateGrant(display.grant) }), ...(display.debit === undefined ? {} : { debit: parseDebitTerms(display.debit) }) };
226
+ }
227
+ case "wallet.sign": {
228
+ const display = record(value, ["kind", "owner_id", "requester_id", "result_recipient", "network", "message_base64url"], ["wallet_id", "prepared_summary"]);
229
+ if (display.kind !== kind || !network(display.network) || !boundedMessage(display.message_base64url))
230
+ throw new ClientFlowError("invalid_server_response");
231
+ return { ...displayCommon(display), kind, network: display.network, messageBase64url: display.message_base64url, ...summaryOf(display.prepared_summary) };
232
+ }
233
+ case "wallet.send": {
234
+ const display = record(value, ["kind", "owner_id", "requester_id", "result_recipient", "network", "asset", "recipient", "amount_raw", "max_fee_raw"], ["wallet_id", "prepared_summary"]);
235
+ if (display.kind !== kind || !network(display.network) || display.asset !== "native" || typeof display.recipient !== "string" ||
236
+ !walletRecipient.test(display.recipient) || !rawQuantity(display.amount_raw, true) || !rawQuantity(display.max_fee_raw, false)) {
237
+ throw new ClientFlowError("invalid_server_response");
238
+ }
239
+ return {
240
+ ...displayCommon(display), kind, network: display.network, asset: display.asset, recipient: display.recipient,
241
+ amountRaw: display.amount_raw, maxFeeRaw: display.max_fee_raw, ...summaryOf(display.prepared_summary),
242
+ };
243
+ }
244
+ case "wallet.export": {
245
+ const display = record(value, ["kind", "owner_id", "requester_id", "result_recipient", "recipient_public_key"], ["wallet_id", "prepared_summary"]);
246
+ if (display.kind !== kind || !recipientPublicKey(display.recipient_public_key))
247
+ throw new ClientFlowError("invalid_server_response");
248
+ return { ...displayCommon(display), kind, recipientPublicKey: display.recipient_public_key, ...summaryOf(display.prepared_summary) };
249
+ }
250
+ default: {
251
+ const display = record(value, ["kind", "owner_id", "requester_id", "result_recipient", "recipient_public_key", "expected_generation"], ["wallet_id", "prepared_summary"]);
252
+ if (display.kind !== "wallet.recover" || !recipientPublicKey(display.recipient_public_key) || !walletGeneration(display.expected_generation)) {
253
+ throw new ClientFlowError("invalid_server_response");
254
+ }
255
+ return {
256
+ ...displayCommon(display), kind: "wallet.recover", recipientPublicKey: display.recipient_public_key,
257
+ expectedGeneration: display.expected_generation, ...summaryOf(display.prepared_summary),
258
+ };
259
+ }
260
+ }
261
+ }
262
+ function network(value) {
263
+ return typeof value === "string" && walletNetwork.test(value);
264
+ }
265
+ function recipientPublicKey(value) {
266
+ return typeof value === "string" && compressedP256Key.test(value);
267
+ }
268
+ /**
269
+ * boundedMessage accepts unpadded base64url whose decoded length is 1..4096
270
+ * bytes. A base64url group of exactly one residual character encodes no byte,
271
+ * so that length is rejected outright.
272
+ */
273
+ function boundedMessage(value) {
274
+ if (typeof value !== "string" || !base64urlValue.test(value) || value.length % 4 === 1)
275
+ return false;
276
+ const decoded = Math.floor((value.length * 3) / 4);
277
+ return decoded >= 1 && decoded <= MaxWalletMessageBytes;
278
+ }
279
+ function walletGeneration(value) {
280
+ if (!canonicalDecimal(value))
281
+ return false;
282
+ const generation = BigInt(value);
283
+ return generation > 0n && generation <= maxWalletGeneration;
284
+ }
285
+ function userWalletKind(value) {
286
+ return value === "wallet.create" || value === "wallet.sign" || value === "wallet.send" || value === "wallet.export" || value === "wallet.recover";
287
+ }
288
+ function walletResultKind(value) {
289
+ return value === "signature" || value === "transfer_receipt" || value === "wallet_created" || value === "device_share_recovered";
290
+ }
291
+ function algorithmName(value) {
292
+ return typeof value === "string" && /^[a-z0-9_-]{1,128}$/.test(value);
293
+ }
294
+ function userWalletState(value) {
295
+ return value === "awaiting_confirmation" || value === "authorized" || value === "dispatching" || value === "outcome_unknown" ||
296
+ value === "succeeded" || value === "failed" || value === "denied" || value === "canceled" || value === "expired";
297
+ }
298
+ function userWalletEffectState(value) {
299
+ return value === "no_effect" || value === "possible" || value === "known";
300
+ }
301
+ function userWalletBasis(value) {
302
+ return value === "" || value === "user_confirmed" || value === "session_authorized";
303
+ }
304
+ function lowerHexDigest(value) {
305
+ return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
306
+ }
307
+ function httpsURL(value) {
308
+ if (typeof value !== "string")
309
+ return false;
310
+ try {
311
+ const parsed = new URL(value);
312
+ return parsed.protocol === "https:" && parsed.username === "" && parsed.password === "";
313
+ }
314
+ catch {
315
+ return false;
316
+ }
317
+ }
@@ -0,0 +1,92 @@
1
+ import type { FetchLike } from "./client.ts";
2
+ /** The code of the typed error that asks the application to start the top-level restore. */
3
+ export declare const webRenewalRequiresNavigation = "web_renewal_requires_navigation";
4
+ interface WebSessionTarget {
5
+ authorizationOrigin: string;
6
+ clientId: string;
7
+ /** The name the tabs of one application take turns under. */
8
+ lockName: string;
9
+ signal?: AbortSignal;
10
+ /** Throws once the renewal or restore was superseded; asked under the lock right before each step that asks the provider. */
11
+ current?: () => void;
12
+ }
13
+ export interface WebRenewalRequest extends WebSessionTarget {
14
+ fetch: FetchLike;
15
+ /** The session's CSRF token, held in memory only; without one the renewal starts from the cookie. */
16
+ csrfToken: string | undefined;
17
+ }
18
+ export interface WebRestoreRequest extends WebSessionTarget {
19
+ redirectUri: string;
20
+ crypto: Crypto;
21
+ /** The host's top-level launcher; it answers the registered redirect the restore came back to. */
22
+ launch: (url: string, signal?: AbortSignal) => Promise<string | void>;
23
+ /** POST /v1/auth/restore with this body, answering the session payload. */
24
+ exchange: (body: string) => Promise<unknown>;
25
+ }
26
+ /**
27
+ * The tabs of one application share the cookie, and the provider ends a
28
+ * session whose refresh value it is shown twice. Every renewal and every
29
+ * restore therefore runs under one Web Lock per client: each sends the value
30
+ * the previous one left. A restore holds it until the host's launcher
31
+ * settles or the restore's signal aborts it; the host gets the same signal
32
+ * and closes its window, so no abandoned navigation rotates the value later.
33
+ * A sign-in's exchange, which sets the cookie too, goes out under the same
34
+ * lock (client.ts), so a renewal's late answer never follows a newer
35
+ * sign-in's.
36
+ */
37
+ export declare function underRenewalLock<T>(target: WebSessionTarget & {
38
+ whenWaiting?: () => void;
39
+ }, work: () => Promise<T>): Promise<T>;
40
+ /**
41
+ * renewWebSession is the background call: POST /v1/auth/renew with the cookie
42
+ * (credentials: "include"). A page that holds the session's CSRF token sends
43
+ * it; a page that holds none (a reload, a new tab), or whose token a renewal
44
+ * or restore in another tab replaced, renews from the cookie alone with the
45
+ * fixed X-Authorization-Renew header and gets a new token. It answers the
46
+ * session payload.
47
+ */
48
+ export declare function renewWebSession(request: WebRenewalRequest): Promise<unknown>;
49
+ /**
50
+ * staleCsrfRefusal is the answer a web lifecycle call gets when a renewal or
51
+ * restore in another tab replaced the session's CSRF token since this page
52
+ * last renewed.
53
+ */
54
+ export declare function staleCsrfRefusal(error: unknown): boolean;
55
+ /**
56
+ * revokeWithCurrentCredentials is logout's way past a CSRF token another tab
57
+ * replaced. Under the tabs' lock, held until the revocation answers, it
58
+ * renews from the cookie alone and revokes with the tokens that answer, but
59
+ * only when they belong to the session the logout was asked for: a cookie
60
+ * another sign-in replaced is left alone and the revocation is not confirmed.
61
+ */
62
+ export declare function revokeWithCurrentCredentials(request: Omit<WebRenewalRequest, "csrfToken"> & {
63
+ sessionId: string;
64
+ whenWaiting?: () => void;
65
+ }, revoke: (accessToken: string, csrfToken: string) => Promise<unknown>): Promise<unknown>;
66
+ /** revokedAnswer says a logout answer confirms the revocation. */
67
+ export declare function revokedAnswer(payload: unknown): boolean;
68
+ /**
69
+ * revokeUnclaimedSession ends the session of a sign-in answer this page does
70
+ * not install, because a logout or a newer sign-in overtook it while it was
71
+ * asked for. The answer set the cookie, so without it a reload would take up
72
+ * a session nobody holds, one the person may have logged out of. The
73
+ * provider clears the cookie only while it holds that session. A CSRF token
74
+ * another tab replaced meanwhile (it renewed that session from the cookie) is
75
+ * followed by the logout's recovery: one renewal from the cookie under the
76
+ * tabs' lock, checked against the answer's session, and the revocation with
77
+ * its tokens. It answers true only for a confirmed revocation of exactly that
78
+ * session; a cookie that holds another session is left alone and answers
79
+ * false, as does any refusal or failure. whenWaiting is told when the
80
+ * recovery has to wait for the lock (a restore may hold it while its window
81
+ * is open): the revocation goes on, but nobody needs to wait for its verdict.
82
+ */
83
+ export declare function revokeUnclaimedSession(request: Omit<WebRenewalRequest, "csrfToken"> & {
84
+ whenWaiting?: () => void;
85
+ }, payload: unknown, revoke: (accessToken: string, csrfToken: string) => Promise<unknown>): Promise<boolean>;
86
+ /**
87
+ * restoreWebSession is the top-level restore: a fresh state and S256 verifier
88
+ * in memory, the provider restore URL through the host's launcher, and the
89
+ * one-use handoff of the redirect it returns exchanged for the session.
90
+ */
91
+ export declare function restoreWebSession(request: WebRestoreRequest): Promise<unknown>;
92
+ export {};