@shuttle.technology/authorization 0.0.0-stage → 0.1.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 (56) hide show
  1. package/README.md +708 -2
  2. package/dist/aesGcmSiv.d.ts +14 -0
  3. package/dist/aesGcmSiv.js +160 -0
  4. package/dist/auth.d.ts +68 -0
  5. package/dist/auth.js +215 -0
  6. package/dist/balances.d.ts +31 -0
  7. package/dist/balances.js +44 -0
  8. package/dist/canonicalize.d.ts +19 -0
  9. package/dist/canonicalize.js +107 -0
  10. package/dist/client.d.ts +264 -0
  11. package/dist/client.js +754 -0
  12. package/dist/deviceKey.d.ts +13 -0
  13. package/dist/deviceKey.js +28 -0
  14. package/dist/deviceShare.d.ts +9 -0
  15. package/dist/deviceShare.js +24 -0
  16. package/dist/deviceShareEnvelope.d.ts +13 -0
  17. package/dist/deviceShareEnvelope.js +139 -0
  18. package/dist/linkedChecks.d.ts +17 -0
  19. package/dist/linkedChecks.js +31 -0
  20. package/dist/linkedSendDisplay.d.ts +73 -0
  21. package/dist/linkedSendDisplay.js +84 -0
  22. package/dist/linkedWalletSignals.d.ts +22 -0
  23. package/dist/linkedWalletSignals.js +101 -0
  24. package/dist/linkedWallets.d.ts +373 -0
  25. package/dist/linkedWallets.js +471 -0
  26. package/dist/mediatedOperation.d.ts +27 -0
  27. package/dist/mediatedOperation.js +68 -0
  28. package/dist/modeC.d.ts +131 -0
  29. package/dist/modeC.js +248 -0
  30. package/dist/modeCLinked.d.ts +207 -0
  31. package/dist/modeCLinked.js +270 -0
  32. package/dist/pages.d.ts +22 -0
  33. package/dist/pages.js +45 -0
  34. package/dist/poll.d.ts +15 -0
  35. package/dist/poll.js +76 -0
  36. package/dist/requestGrant.d.ts +169 -0
  37. package/dist/requestGrant.js +243 -0
  38. package/dist/sends.d.ts +89 -0
  39. package/dist/sends.js +127 -0
  40. package/dist/slice4.d.ts +173 -0
  41. package/dist/slice4.js +285 -0
  42. package/dist/streams.d.ts +212 -0
  43. package/dist/streams.js +282 -0
  44. package/dist/trackedWallets.d.ts +53 -0
  45. package/dist/trackedWallets.js +104 -0
  46. package/dist/transferStatus.d.ts +45 -0
  47. package/dist/transferStatus.js +43 -0
  48. package/dist/walletDisplayParts.d.ts +15 -0
  49. package/dist/walletDisplayParts.js +43 -0
  50. package/dist/walletEvents.d.ts +43 -0
  51. package/dist/walletEvents.js +97 -0
  52. package/dist/walletLimits.d.ts +65 -0
  53. package/dist/walletLimits.js +102 -0
  54. package/dist/walletOperations.d.ts +192 -0
  55. package/dist/walletOperations.js +308 -0
  56. package/package.json +64 -4
package/dist/client.js ADDED
@@ -0,0 +1,754 @@
1
+ import { AuthorizationError, ClientFlowError, canonicalDecimal, identityMethods, integer, newHandoffBinding, opaqueId, parseAuthorizationError, record, rfc3339Millis, } from "./auth.js";
2
+ import { parseConfirmationChallenge, parseHostedIdentityFlow, parseIdentityFlowCallback, parseIdentityList, parseLocalOperationResult, parseOperationConfirmationCallback, parseOperationEnvelope, parsePasskeyBootstrapResult, parseVerifiedIdentityResult, validateLocalOperationCreate, } from "./slice4.js";
3
+ import { StreamsReads } from "./streams.js";
4
+ import { LinkedWallets } from "./linkedWallets.js";
5
+ export { LinkedWallets, eventOfTransaction, isWalletLimitRefusal, resumeCursor, sendEvents, transferFinal, WalletLimitRefusals } from "./linkedWallets.js";
6
+ import { parseUserWalletOperationEnvelope, parseWalletResult, validateUserWalletCreate, } from "./walletOperations.js";
7
+ import { pageEnvelope, pageProjection, pageRequest } from "./pages.js";
8
+ export { AuthorizationError, ClientFlowError } from "./auth.js";
9
+ export { pollAuthoritative } from "./poll.js";
10
+ export { MaxTransferCommentBytes, MaxWalletMessageBytes, ReplaceActions, transferComment, walletMaterialPurged } from "./walletOperations.js";
11
+ export async function initializeAuthorizationClient(configuration, dependencies) {
12
+ validateConfiguration(configuration);
13
+ const crypto = dependencies.crypto ?? globalThis.crypto;
14
+ // The platform fetch must be called on its global: stored on the dependencies
15
+ // object and called from there, a browser rejects it as an illegal invocation.
16
+ const platformFetch = globalThis.fetch;
17
+ const fetch = dependencies.fetch ?? (typeof platformFetch === "function" ? ((url, init) => platformFetch.call(globalThis, url, init)) : undefined);
18
+ if (!crypto?.subtle || typeof crypto.getRandomValues !== "function" || typeof fetch !== "function")
19
+ throw new ClientFlowError("runtime_capability_unavailable");
20
+ let publicConfiguration;
21
+ if (configuration.channel === "web") {
22
+ const web = dependencies;
23
+ if (typeof web.currentOrigin !== "function" || web.currentOrigin() !== configuration.applicationOrigin || typeof web.openHosted !== "function") {
24
+ throw new ClientFlowError("origin_mismatch");
25
+ }
26
+ publicConfiguration = await readPublicConfiguration(configuration, fetch);
27
+ }
28
+ else {
29
+ const mobile = dependencies;
30
+ const bridge = mobile.browserBridge;
31
+ if (!mobile.secureStorage || !bridge || typeof bridge.verifyAssociatedRedirect !== "function" || typeof bridge.openSystemBrowser !== "function") {
32
+ throw new ClientFlowError("mobile_association_unverified");
33
+ }
34
+ let verification;
35
+ try {
36
+ verification = await bridge.verifyAssociatedRedirect({
37
+ redirectUri: configuration.redirectUri,
38
+ applicationIdentifier: configuration.mobileApplicationIdentifier,
39
+ });
40
+ }
41
+ catch {
42
+ throw new ClientFlowError("mobile_association_unverified");
43
+ }
44
+ if (!exactMobileAssociation(verification, configuration.redirectUri, configuration.mobileApplicationIdentifier)) {
45
+ throw new ClientFlowError("mobile_association_unverified");
46
+ }
47
+ }
48
+ const normalized = {
49
+ fetch,
50
+ crypto,
51
+ now: dependencies.now ?? Date.now,
52
+ launch: configuration.channel === "web"
53
+ ? async (url) => dependencies.openHosted(url)
54
+ : async (url) => { await dependencies.browserBridge.openSystemBrowser(url); },
55
+ secureStorage: configuration.channel === "mobile" ? dependencies.secureStorage : undefined,
56
+ };
57
+ return new AuthorizationClient(configuration, normalized, publicConfiguration);
58
+ }
59
+ export class AuthorizationClient {
60
+ capabilities;
61
+ publicConfiguration;
62
+ #configuration;
63
+ #dependencies;
64
+ #pending = new Map();
65
+ #identityFlows = new Map();
66
+ #confirmations = new Map();
67
+ #refreshKey;
68
+ #accessToken;
69
+ #csrfToken;
70
+ #session;
71
+ #refreshInFlight;
72
+ constructor(configuration, dependencies, publicConfiguration) {
73
+ this.#configuration = Object.freeze({ ...configuration, methods: Object.freeze([...configuration.methods]) });
74
+ this.#dependencies = dependencies;
75
+ this.publicConfiguration = publicConfiguration;
76
+ this.capabilities = Object.freeze({ mode: "mode_b", channel: configuration.channel, methods: Object.freeze([...configuration.methods]) });
77
+ this.#refreshKey = `authorization:${configuration.tenantId}:${configuration.applicationId}:${configuration.environmentId}:${configuration.publishableClientId}:refresh`;
78
+ }
79
+ get hasSession() { return this.#accessToken !== undefined; }
80
+ signup(method) { return this.#begin("signup", method); }
81
+ login(method) { return this.#begin("login", method); }
82
+ bootstrapPasskey(signal) {
83
+ return this.#beginIdentityFlow("bootstrap", "/v1/identity/passkey-bootstrap", undefined, signal);
84
+ }
85
+ verifyIdentity(method, signal) {
86
+ return this.#beginIdentityFlow("verification", "/v1/identity-verifications", method, signal);
87
+ }
88
+ async completePasskeyBootstrap(callbackUri, signal) {
89
+ const { callback, pending } = this.#identityFlowCallback(callbackUri, "bootstrap");
90
+ const authority = await this.#accessForWrite(signal);
91
+ const payload = await this.#json("/v1/identity/passkey-bootstrap/complete", {
92
+ method: "POST",
93
+ body: JSON.stringify({ client_id: this.#configuration.publishableClientId, flow_id: callback.flowId, code: callback.code, verifier: pending.verifier, redirect_uri: this.#configuration.redirectUri }),
94
+ accessToken: authority.token,
95
+ signal,
96
+ });
97
+ const result = parsePasskeyBootstrapResult(payload);
98
+ this.#identityFlows.delete(callback.flowId);
99
+ return result;
100
+ }
101
+ /** identities lists the account's own active sign-in methods; a passkey names its credential. */
102
+ async identities(signal) {
103
+ return parseIdentityList(await this.#protectedGet("/v1/identities", signal));
104
+ }
105
+ async completeIdentityVerification(callbackUri, signal) {
106
+ const { callback, pending } = this.#identityFlowCallback(callbackUri, "verification");
107
+ const authority = await this.#accessForWrite(signal);
108
+ const payload = await this.#json("/v1/identity-verifications/complete", {
109
+ method: "POST",
110
+ body: JSON.stringify({ client_id: this.#configuration.publishableClientId, flow_id: callback.flowId, code: callback.code, verifier: pending.verifier, redirect_uri: this.#configuration.redirectUri }),
111
+ accessToken: authority.token,
112
+ signal,
113
+ });
114
+ const result = parseVerifiedIdentityResult(payload, this.#dependencies.now());
115
+ this.#identityFlows.delete(callback.flowId);
116
+ return result;
117
+ }
118
+ async createOperation(create, idempotencyKey, signal) {
119
+ const authority = await this.#accessForWrite(signal);
120
+ if (!opaqueId(idempotencyKey))
121
+ throw new ClientFlowError("invalid_idempotency_key");
122
+ validateLocalOperationCreate(create, authority.session.accountId);
123
+ const payload = await this.#json("/v1/operations", {
124
+ method: "POST", body: JSON.stringify(create), accessToken: authority.token, idempotencyKey, signal,
125
+ });
126
+ return parseOperationEnvelope(payload).operation;
127
+ }
128
+ createIdentityLink(verifiedIdentityHandle, idempotencyKey, signal) {
129
+ const ownerId = this.#session?.accountId ?? "";
130
+ return this.createOperation({ kind: "identity.link", owner_type: "account", owner_id: ownerId, payload: { verified_identity_handle: verifiedIdentityHandle } }, idempotencyKey, signal);
131
+ }
132
+ createIdentityUnlink(identityId, idempotencyKey, signal) {
133
+ const ownerId = this.#session?.accountId ?? "";
134
+ return this.createOperation({ kind: "identity.unlink", owner_type: "account", owner_id: ownerId, payload: { identity_id: identityId } }, idempotencyKey, signal);
135
+ }
136
+ createSessionRevocation(sessionId, idempotencyKey, signal) {
137
+ const ownerId = this.#session?.accountId ?? "";
138
+ return this.createOperation({ kind: "session.revoke", owner_type: "account", owner_id: ownerId, payload: { session_id: sessionId } }, idempotencyKey, signal);
139
+ }
140
+ createServiceGrant(input, idempotencyKey, signal) {
141
+ const ownerId = this.#session?.accountId ?? "";
142
+ const payload = { service_principal_id: input.servicePrincipalId, enabled: input.enabled, scopes: [...input.scopes] };
143
+ if (input.externalRef !== undefined)
144
+ payload.external_ref = input.externalRef;
145
+ return this.createOperation({ kind: "service_grant.set", owner_type: "account", owner_id: ownerId, wallet_id: input.walletId, payload }, idempotencyKey, signal);
146
+ }
147
+ async operation(operationId, signal) {
148
+ if (!opaqueId(operationId))
149
+ throw new ClientFlowError("invalid_operation_id");
150
+ return parseOperationEnvelope(await this.#protectedGet(`/v1/operations/${encodeURIComponent(operationId)}`, signal)).operation;
151
+ }
152
+ async challengeOperation(operationId, credentialId, signal) {
153
+ if (!opaqueId(operationId) || !opaqueId(credentialId))
154
+ throw new ClientFlowError("invalid_confirmation_request");
155
+ const authority = await this.#accessForWrite(signal);
156
+ const binding = await newHandoffBinding(this.#dependencies.crypto);
157
+ const payload = await this.#json(`/v1/operations/${encodeURIComponent(operationId)}/challenge`, {
158
+ method: "POST",
159
+ body: JSON.stringify({ credential_id: credentialId, redirect_uri: this.#configuration.redirectUri, state: binding.state }),
160
+ accessToken: authority.token,
161
+ signal,
162
+ });
163
+ const challenge = parseConfirmationChallenge(payload, this.#dependencies.now());
164
+ const expiresAtMs = rfc3339Millis(challenge.expiresAt);
165
+ if (expiresAtMs === undefined)
166
+ throw new ClientFlowError("invalid_server_response");
167
+ this.#confirmations.set(operationId, { state: binding.state, operationId, expiresAtMs });
168
+ try {
169
+ await this.#dependencies.launch(challenge.confirmationLocation);
170
+ }
171
+ catch (error) {
172
+ this.#confirmations.delete(operationId);
173
+ throw error;
174
+ }
175
+ return challenge;
176
+ }
177
+ async completeOperationConfirmation(callbackUri, signal) {
178
+ const operationId = this.#acceptConfirmationCallback(callbackUri);
179
+ const operation = await this.operation(operationId, signal);
180
+ this.#confirmations.delete(operationId);
181
+ return operation;
182
+ }
183
+ /**
184
+ * createWalletOperation admits one Mode B wallet intent on POST /v1/operations.
185
+ * The closed body is validated locally against the current session account
186
+ * before the request leaves the client (source §1.3, §2.2).
187
+ */
188
+ async createWalletOperation(create, idempotencyKey, signal) {
189
+ const authority = await this.#accessForWrite(signal);
190
+ if (!opaqueId(idempotencyKey))
191
+ throw new ClientFlowError("invalid_idempotency_key");
192
+ validateUserWalletCreate(create, authority.session.accountId);
193
+ const payload = await this.#json("/v1/operations", {
194
+ method: "POST", body: JSON.stringify(create), accessToken: authority.token, idempotencyKey, signal,
195
+ });
196
+ return parseUserWalletOperationEnvelope(payload).operation;
197
+ }
198
+ createWallet(input, idempotencyKey, signal) {
199
+ return this.createWalletOperation({
200
+ kind: "wallet.create", owner_type: "account", owner_id: this.#session?.accountId ?? "",
201
+ payload: { network: input.network, recipient_public_key: input.recipientPublicKey },
202
+ }, idempotencyKey, signal);
203
+ }
204
+ signWithWallet(input, idempotencyKey, signal) {
205
+ return this.createWalletOperation({
206
+ kind: "wallet.sign", owner_type: "account", owner_id: this.#session?.accountId ?? "", wallet_id: input.walletId,
207
+ payload: { network: input.network, message_base64url: input.messageBase64url },
208
+ }, idempotencyKey, signal);
209
+ }
210
+ sendFromWallet(input, idempotencyKey, signal) {
211
+ return this.createWalletOperation({
212
+ kind: "wallet.send", owner_type: "account", owner_id: this.#session?.accountId ?? "", wallet_id: input.walletId,
213
+ payload: { network: input.network, asset: "native", recipient: input.recipient, amount_raw: input.amountRaw, max_fee_raw: input.maxFeeRaw },
214
+ }, idempotencyKey, signal);
215
+ }
216
+ exportWallet(input, idempotencyKey, signal) {
217
+ return this.createWalletOperation({
218
+ kind: "wallet.export", owner_type: "account", owner_id: this.#session?.accountId ?? "", wallet_id: input.walletId,
219
+ payload: { recipient_public_key: input.recipientPublicKey },
220
+ }, idempotencyKey, signal);
221
+ }
222
+ recoverWallet(input, idempotencyKey, signal) {
223
+ return this.createWalletOperation({
224
+ kind: "wallet.recover", owner_type: "account", owner_id: this.#session?.accountId ?? "", wallet_id: input.walletId,
225
+ payload: { expected_generation: input.expectedGeneration, recipient_public_key: input.recipientPublicKey },
226
+ }, idempotencyKey, signal);
227
+ }
228
+ /** walletOperation reads the safe wallet status on GET /v1/operations/{operation_id}. */
229
+ async walletOperation(operationId, signal) {
230
+ if (!opaqueId(operationId))
231
+ throw new ClientFlowError("invalid_operation_id");
232
+ return parseUserWalletOperationEnvelope(await this.#protectedGet(`/v1/operations/${encodeURIComponent(operationId)}`, signal)).operation;
233
+ }
234
+ /**
235
+ * challengeWalletOperation reuses the shared confirmation challenge route and
236
+ * pending-state bookkeeping; the wallet and local challenge contracts are the
237
+ * same closed response.
238
+ */
239
+ challengeWalletOperation(operationId, credentialId, signal) {
240
+ return this.challengeOperation(operationId, credentialId, signal);
241
+ }
242
+ async completeWalletOperationConfirmation(callbackUri, signal) {
243
+ const operationId = this.#acceptConfirmationCallback(callbackUri);
244
+ const operation = await this.walletOperation(operationId, signal);
245
+ this.#confirmations.delete(operationId);
246
+ return operation;
247
+ }
248
+ /**
249
+ * executeWalletOperation claims one confirmed wallet effect. The first claim
250
+ * answers 202 and an exact replay answers 200; both carry the same operation.
251
+ */
252
+ async executeWalletOperation(operationId, version, idempotencyKey, signal) {
253
+ if (!opaqueId(operationId) || !canonicalDecimal(version) || !opaqueId(idempotencyKey))
254
+ throw new ClientFlowError("invalid_execute_request");
255
+ const authority = await this.#accessForWrite(signal);
256
+ const payload = await this.#json(`/v1/operations/${encodeURIComponent(operationId)}/execute`, {
257
+ method: "POST", accessToken: authority.token, idempotencyKey, ifMatch: version, signal,
258
+ });
259
+ return parseUserWalletOperationEnvelope(payload).operation;
260
+ }
261
+ /** walletOperationResult reads the protected wallet output for its frozen recipient. */
262
+ async walletOperationResult(operationId, signal) {
263
+ if (!opaqueId(operationId))
264
+ throw new ClientFlowError("invalid_operation_id");
265
+ return parseWalletResult(await this.#protectedGet(`/v1/operations/${encodeURIComponent(operationId)}/result`, signal), operationId);
266
+ }
267
+ async executeOperation(operationId, version, idempotencyKey, signal) {
268
+ if (!opaqueId(operationId) || !canonicalDecimal(version) || !opaqueId(idempotencyKey))
269
+ throw new ClientFlowError("invalid_execute_request");
270
+ const authority = await this.#accessForWrite(signal);
271
+ const payload = await this.#json(`/v1/operations/${encodeURIComponent(operationId)}/execute`, {
272
+ method: "POST", accessToken: authority.token, idempotencyKey, ifMatch: version, signal,
273
+ });
274
+ return parseOperationEnvelope(payload).operation;
275
+ }
276
+ /** HTTP-only Auth-owned links and session-authorized Streams registration. */
277
+ linkedWallets() {
278
+ return new LinkedWallets({
279
+ get: (path, signal) => this.#protectedGet(path, signal),
280
+ stream: (path, signal) => this.#protectedStream(path, signal),
281
+ write: async (path, method, body, signal, idempotencyKey, version) => {
282
+ const authority = await this.#accessForWrite(signal);
283
+ const payload = await this.#json(path, { method, body: body === undefined ? undefined : JSON.stringify(body), accessToken: authority.token, signal, idempotencyKey, ifMatch: version });
284
+ return { payload, accountId: authority.session.accountId };
285
+ },
286
+ accountId: () => this.#session?.accountId ?? "",
287
+ now: () => this.#dependencies.now(),
288
+ });
289
+ }
290
+ /**
291
+ * streams binds the two authorized Streams reads of source §2.6 to this
292
+ * session's protected GET, so the account caller reuses the one access-token
293
+ * and refresh path. The read contract, its types and its bounds live in the
294
+ * `./streams` entry point, which this file deliberately does not re-export; a
295
+ * service caller constructs `StreamsReads` there with its own credentialed
296
+ * GET. The caller supplies the stable `idempotencyKey` in each read's options;
297
+ * it is forwarded unchanged on the original GET and any protected 401 retry.
298
+ */
299
+ streams() {
300
+ return new StreamsReads({ scope: "account", get: (path, signal, idempotencyKey) => this.#protectedGet(path, signal, idempotencyKey) });
301
+ }
302
+ async operationResult(operationId, signal) {
303
+ if (!opaqueId(operationId))
304
+ throw new ClientFlowError("invalid_operation_id");
305
+ return parseLocalOperationResult(await this.#protectedGet(`/v1/operations/${encodeURIComponent(operationId)}/result`, signal));
306
+ }
307
+ cancel(attemptId) {
308
+ this.#pending.delete(attemptId);
309
+ return "local_canceled";
310
+ }
311
+ async exchangeHandoff(callbackUri, signal) {
312
+ const callback = exactCallback(callbackUri, this.#configuration.redirectUri);
313
+ const attempt = this.#pending.get(callback.attemptId);
314
+ if (!attempt)
315
+ throw new ClientFlowError("unknown_attempt");
316
+ if (attempt.state !== callback.state)
317
+ throw new ClientFlowError("state_mismatch");
318
+ if (this.#dependencies.now() / 1_000 >= attempt.expiresAt) {
319
+ this.#pending.delete(callback.attemptId);
320
+ throw new ClientFlowError("attempt_expired");
321
+ }
322
+ const payload = await this.#json("/v1/auth/exchange", {
323
+ method: "POST",
324
+ body: JSON.stringify({
325
+ client_id: this.#configuration.publishableClientId,
326
+ attempt_id: callback.attemptId,
327
+ code: callback.code,
328
+ verifier: attempt.verifier,
329
+ redirect_uri: this.#configuration.redirectUri,
330
+ }),
331
+ signal,
332
+ });
333
+ const session = await this.#acceptSession(payload, true);
334
+ this.#pending.delete(callback.attemptId);
335
+ return session;
336
+ }
337
+ restoreSession(signal) { return this.#refresh(signal); }
338
+ refreshSession(signal) { return this.#refresh(signal); }
339
+ async logout(signal) {
340
+ const token = this.#accessToken;
341
+ const csrf = this.#csrfToken;
342
+ this.#accessToken = undefined;
343
+ this.#csrfToken = undefined;
344
+ this.#session = undefined;
345
+ try {
346
+ if (this.#dependencies.secureStorage)
347
+ await this.#dependencies.secureStorage.delete(this.#refreshKey);
348
+ if (!token || (this.#configuration.channel === "web" && !csrf))
349
+ return { localCleared: true, serverDisposition: "server_unconfirmed" };
350
+ const payload = await this.#json("/v1/auth/logout", {
351
+ method: "POST",
352
+ body: JSON.stringify({ client_id: this.#configuration.publishableClientId }),
353
+ accessToken: token,
354
+ csrfToken: csrf,
355
+ signal,
356
+ });
357
+ const result = record(payload, ["request_id", "server_disposition"]);
358
+ if (!opaqueId(result.request_id) || result.server_disposition !== "server_revoked")
359
+ throw new ClientFlowError("invalid_server_response");
360
+ return { localCleared: true, serverDisposition: "server_revoked" };
361
+ }
362
+ catch {
363
+ return { localCleared: true, serverDisposition: "server_unconfirmed" };
364
+ }
365
+ }
366
+ async me(signal) {
367
+ const payload = await this.#protectedGet("/v1/me", signal);
368
+ const root = record(payload, ["request_id", "account", "session_id", "server_time"]);
369
+ const account = record(root.account, ["account_id", "version", "setup_required"]);
370
+ 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)) {
371
+ throw new ClientFlowError("invalid_server_response");
372
+ }
373
+ 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 };
374
+ }
375
+ async sessions(input) {
376
+ const page = pageRequest("/v1/sessions", input);
377
+ const payload = await this.#protectedGet(page.path, page.signal);
378
+ const root = pageEnvelope(payload);
379
+ const items = root.items.map((value) => {
380
+ const item = record(value, ["session_id", "client_id", "state", "created_at", "expires_at", "current"]);
381
+ if (!opaqueId(item.session_id) || !opaqueId(item.client_id) || typeof item.state !== "string" || typeof item.created_at !== "string" || typeof item.expires_at !== "string" || typeof item.current !== "boolean")
382
+ throw new ClientFlowError("invalid_server_response");
383
+ return { sessionId: item.session_id, clientId: item.client_id, state: item.state, createdAt: item.created_at, expiresAt: item.expires_at, current: item.current };
384
+ });
385
+ return pageProjection(root.request_id, items, root.has_more, root.next_cursor);
386
+ }
387
+ async wallets(input) {
388
+ const page = pageRequest("/v1/wallets", input);
389
+ const payload = await this.#protectedGet(page.path, page.signal);
390
+ const root = pageEnvelope(payload);
391
+ const items = root.items.map((value) => {
392
+ const item = record(value, ["wallet_id", "network", "address", "state", "generation", "mapping_version"]);
393
+ if (!opaqueId(item.wallet_id) || typeof item.network !== "string" || typeof item.address !== "string" || typeof item.state !== "string" || !canonicalDecimal(item.generation) || !canonicalDecimal(item.mapping_version))
394
+ throw new ClientFlowError("invalid_server_response");
395
+ return { walletId: item.wallet_id, network: item.network, address: item.address, state: item.state, generation: item.generation, mappingVersion: item.mapping_version };
396
+ });
397
+ return pageProjection(root.request_id, items, root.has_more, root.next_cursor);
398
+ }
399
+ async #begin(intent, method) {
400
+ if (!identityMethods.includes(method) || !this.#configuration.methods.includes(method))
401
+ throw new ClientFlowError("method_unavailable");
402
+ const binding = await newHandoffBinding(this.#dependencies.crypto);
403
+ const payload = await this.#json("/v1/auth/attempts", {
404
+ method: "POST",
405
+ body: JSON.stringify({
406
+ client_id: this.#configuration.publishableClientId,
407
+ intent,
408
+ method,
409
+ redirect_uri: this.#configuration.redirectUri,
410
+ state: binding.state,
411
+ code_challenge: binding.challenge,
412
+ code_challenge_method: "S256",
413
+ }),
414
+ });
415
+ const root = record(payload, ["request_id", "attempt_id", "launch_url", "expires_at"]);
416
+ if (!opaqueId(root.request_id) || !opaqueId(root.attempt_id) || !httpsURL(root.launch_url) || !integer(root.expires_at) || root.expires_at <= this.#dependencies.now() / 1_000) {
417
+ throw new ClientFlowError("invalid_server_response");
418
+ }
419
+ const attempt = { requestId: root.request_id, attemptId: root.attempt_id, launchUrl: root.launch_url, expiresAt: root.expires_at };
420
+ this.#pending.set(attempt.attemptId, { state: binding.state, verifier: binding.verifier, expiresAt: attempt.expiresAt });
421
+ await this.#dependencies.launch(attempt.launchUrl);
422
+ return attempt;
423
+ }
424
+ async #beginIdentityFlow(kind, path, method, signal) {
425
+ if ((kind === "bootstrap" && !this.#configuration.methods.includes("passkey")) || (method !== undefined && (!identityMethods.includes(method) || !this.#configuration.methods.includes(method)))) {
426
+ throw new ClientFlowError("method_unavailable");
427
+ }
428
+ const authority = await this.#accessForWrite(signal);
429
+ const binding = await newHandoffBinding(this.#dependencies.crypto);
430
+ const body = {
431
+ client_id: this.#configuration.publishableClientId,
432
+ redirect_uri: this.#configuration.redirectUri,
433
+ state: binding.state,
434
+ code_challenge: binding.challenge,
435
+ code_challenge_method: "S256",
436
+ };
437
+ if (method !== undefined)
438
+ body.method = method;
439
+ const payload = await this.#json(path, { method: "POST", body: JSON.stringify(body), accessToken: authority.token, signal });
440
+ const flow = parseHostedIdentityFlow(payload, this.#dependencies.now());
441
+ this.#identityFlows.set(flow.flowId, { kind, state: binding.state, verifier: binding.verifier, expiresAt: flow.expiresAt });
442
+ try {
443
+ await this.#dependencies.launch(flow.launchUrl);
444
+ }
445
+ catch (error) {
446
+ this.#identityFlows.delete(flow.flowId);
447
+ throw error;
448
+ }
449
+ return flow;
450
+ }
451
+ #identityFlowCallback(callbackUri, expectedKind) {
452
+ const callback = parseIdentityFlowCallback(callbackUri, this.#configuration.redirectUri);
453
+ const pending = this.#identityFlows.get(callback.flowId);
454
+ if (!pending || pending.kind !== expectedKind)
455
+ throw new ClientFlowError("unknown_identity_flow");
456
+ if (pending.state !== callback.state)
457
+ throw new ClientFlowError("state_mismatch");
458
+ if (this.#dependencies.now() / 1_000 >= pending.expiresAt) {
459
+ this.#identityFlows.delete(callback.flowId);
460
+ throw new ClientFlowError("identity_flow_expired");
461
+ }
462
+ return { callback, pending };
463
+ }
464
+ /**
465
+ * #acceptConfirmationCallback applies the shared local confirmation checks:
466
+ * exact redirect, the in-memory state for that operation, and the challenge
467
+ * deadline. It never asserts a server disposition.
468
+ */
469
+ #acceptConfirmationCallback(callbackUri) {
470
+ const callback = parseOperationConfirmationCallback(callbackUri, this.#configuration.redirectUri);
471
+ const pending = this.#confirmations.get(callback.operationId);
472
+ if (!pending)
473
+ throw new ClientFlowError("unknown_confirmation");
474
+ if (pending.state !== callback.state)
475
+ throw new ClientFlowError("state_mismatch");
476
+ if (this.#dependencies.now() >= pending.expiresAtMs) {
477
+ this.#confirmations.delete(callback.operationId);
478
+ throw new ClientFlowError("confirmation_expired");
479
+ }
480
+ return callback.operationId;
481
+ }
482
+ async #accessForWrite(signal) {
483
+ if (!this.#accessToken || !this.#session)
484
+ throw new ClientFlowError("session_unavailable");
485
+ if (this.#dependencies.now() / 1_000 >= this.#session.accessExpiresAt)
486
+ await this.#refresh(signal);
487
+ if (!this.#accessToken || !this.#session)
488
+ throw new ClientFlowError("session_unavailable");
489
+ return { token: this.#accessToken, session: this.#session };
490
+ }
491
+ #refresh(signal) {
492
+ if (this.#refreshInFlight)
493
+ return this.#refreshInFlight;
494
+ const operation = this.#performRefresh(signal).finally(() => { this.#refreshInFlight = undefined; });
495
+ this.#refreshInFlight = operation;
496
+ return operation;
497
+ }
498
+ async #performRefresh(signal) {
499
+ if (this.#configuration.channel === "web") {
500
+ const binding = await newHandoffBinding(this.#dependencies.crypto);
501
+ const start = new URL("/v1/auth/restore", this.#configuration.authorizationOrigin);
502
+ start.searchParams.set("client_id", this.#configuration.publishableClientId);
503
+ start.searchParams.set("redirect_uri", this.#configuration.redirectUri);
504
+ start.searchParams.set("state", binding.state);
505
+ start.searchParams.set("code_challenge", binding.challenge);
506
+ start.searchParams.set("code_challenge_method", "S256");
507
+ const returned = await this.#dependencies.launch(start.toString());
508
+ if (typeof returned !== "string")
509
+ throw new ClientFlowError("restore_redirect_unavailable");
510
+ const handoff = exactRestoreCallback(returned, this.#configuration.redirectUri);
511
+ if (handoff.state !== binding.state)
512
+ throw new ClientFlowError("state_mismatch");
513
+ const payload = await this.#json("/v1/auth/restore", {
514
+ method: "POST",
515
+ body: JSON.stringify({
516
+ client_id: this.#configuration.publishableClientId,
517
+ restore_id: handoff.restoreId,
518
+ code: handoff.code,
519
+ verifier: binding.verifier,
520
+ redirect_uri: this.#configuration.redirectUri,
521
+ }),
522
+ signal,
523
+ });
524
+ return this.#acceptSession(payload, false);
525
+ }
526
+ const refreshToken = await this.#dependencies.secureStorage?.get(this.#refreshKey);
527
+ if (!refreshToken)
528
+ throw new ClientFlowError("mobile_refresh_unavailable");
529
+ const payload = await this.#json("/v1/auth/refresh", {
530
+ method: "POST",
531
+ body: JSON.stringify({ client_id: this.#configuration.publishableClientId, refresh_token: refreshToken }),
532
+ signal,
533
+ });
534
+ return this.#acceptSession(payload, false);
535
+ }
536
+ async #acceptSession(payload, exchange) {
537
+ const channelField = this.#configuration.channel === "web" ? "csrf_token" : "refresh_token";
538
+ const required = ["request_id", "access_token", "expires_at", "account_id", "session_id", channelField];
539
+ if (exchange)
540
+ required.push("setup_required");
541
+ const root = record(payload, required);
542
+ if (!opaqueId(root.request_id) || typeof root.access_token !== "string" || root.access_token.length === 0 || !integer(root.expires_at) || !opaqueId(root.account_id) || !opaqueId(root.session_id) || (exchange && typeof root.setup_required !== "boolean") || typeof root[channelField] !== "string" || root[channelField].length === 0) {
543
+ throw new ClientFlowError("invalid_server_response");
544
+ }
545
+ const session = {
546
+ requestId: root.request_id,
547
+ accessExpiresAt: root.expires_at,
548
+ accountId: root.account_id,
549
+ sessionId: root.session_id,
550
+ setupRequired: exchange ? root.setup_required : this.#session?.setupRequired,
551
+ };
552
+ if (session.setupRequired === undefined)
553
+ delete session.setupRequired;
554
+ if (this.#configuration.channel === "web") {
555
+ this.#csrfToken = root.csrf_token;
556
+ }
557
+ else {
558
+ try {
559
+ await this.#dependencies.secureStorage?.set(this.#refreshKey, root.refresh_token);
560
+ }
561
+ catch {
562
+ try {
563
+ await this.#dependencies.secureStorage?.delete(this.#refreshKey);
564
+ }
565
+ catch { /* fail closed locally */ }
566
+ this.#accessToken = undefined;
567
+ this.#session = undefined;
568
+ throw new ClientFlowError("secure_storage_unavailable");
569
+ }
570
+ }
571
+ this.#accessToken = root.access_token;
572
+ this.#session = session;
573
+ return { ...session };
574
+ }
575
+ async #protectedGet(path, signal, idempotencyKey) {
576
+ if (!this.#accessToken || !this.#session)
577
+ throw new ClientFlowError("session_unavailable");
578
+ if (this.#dependencies.now() / 1_000 >= this.#session.accessExpiresAt)
579
+ await this.#refresh(signal);
580
+ const attemptedToken = this.#accessToken;
581
+ try {
582
+ return await this.#json(path, { method: "GET", accessToken: attemptedToken, idempotencyKey, signal });
583
+ }
584
+ catch (error) {
585
+ if (!(error instanceof AuthorizationError) || error.status !== 401 || error.code !== "UNAUTHORIZED")
586
+ throw error;
587
+ if (this.#accessToken === attemptedToken)
588
+ await this.#refresh(signal);
589
+ return this.#json(path, { method: "GET", accessToken: this.#accessToken, idempotencyKey, signal });
590
+ }
591
+ }
592
+ /** Opens a long-lived answer with the session's access token, refreshed once if the server no longer takes it. */
593
+ async #protectedStream(path, signal) {
594
+ const open = (token) => this.#dependencies.fetch(`${this.#configuration.authorizationOrigin}${path}`, {
595
+ method: "GET", headers: { Accept: "text/event-stream", Authorization: `Bearer ${token}` },
596
+ credentials: this.#configuration.channel === "web" ? "include" : "omit", cache: "no-store", signal,
597
+ });
598
+ const first = await this.#accessForWrite(signal);
599
+ let response = await open(first.token);
600
+ if (response.status === 401) {
601
+ if (this.#accessToken === first.token)
602
+ await this.#refresh(signal);
603
+ response = await open((await this.#accessForWrite(signal)).token);
604
+ }
605
+ if (!response.ok)
606
+ throw parseAuthorizationError(await response.json(), response.status);
607
+ if (!response.body || !(response.headers?.get("content-type") ?? "").startsWith("text/event-stream"))
608
+ throw new ClientFlowError("invalid_server_response");
609
+ return response.body;
610
+ }
611
+ async #json(path, options) {
612
+ const headers = { Accept: "application/json" };
613
+ if (options.body !== undefined)
614
+ headers["Content-Type"] = "application/json";
615
+ if (options.accessToken)
616
+ headers.Authorization = `Bearer ${options.accessToken}`;
617
+ if (options.csrfToken)
618
+ headers["X-CSRF-Token"] = options.csrfToken;
619
+ if (options.idempotencyKey)
620
+ headers["Idempotency-Key"] = options.idempotencyKey;
621
+ if (options.ifMatch)
622
+ headers["If-Match"] = options.ifMatch;
623
+ const response = await this.#dependencies.fetch(`${this.#configuration.authorizationOrigin}${path}`, {
624
+ method: options.method,
625
+ headers,
626
+ body: options.body,
627
+ credentials: this.#configuration.channel === "web" ? "include" : "omit",
628
+ cache: "no-store",
629
+ signal: options.signal,
630
+ });
631
+ const payload = await response.json();
632
+ if (!response.ok)
633
+ throw parseAuthorizationError(payload, response.status);
634
+ return payload;
635
+ }
636
+ }
637
+ async function readPublicConfiguration(configuration, fetch) {
638
+ const response = await fetch(`${configuration.authorizationOrigin}/v1/public/clients/${encodeURIComponent(configuration.publishableClientId)}`, {
639
+ method: "GET",
640
+ headers: { Accept: "application/json" },
641
+ credentials: "omit",
642
+ cache: "no-store",
643
+ });
644
+ const payload = await response.json();
645
+ if (!response.ok)
646
+ throw parseAuthorizationError(payload, response.status);
647
+ const root = record(payload, ["request_id", "client"]);
648
+ const client = record(root.client, ["id", "name", "environment_id", "methods", "provider_origin", "rp_id", "redirects", "platform"]);
649
+ if (!opaqueId(root.request_id) || client.id !== configuration.publishableClientId || typeof client.name !== "string" || client.name.length < 1 || client.name.length > 128 || client.environment_id !== configuration.environmentId || !Array.isArray(client.methods) || client.methods.length < 1 || client.methods.length > identityMethods.length || !client.methods.every((method) => typeof method === "string" && identityMethods.includes(method)) || new Set(client.methods).size !== client.methods.length || !sameStrings(client.methods, configuration.methods) || client.provider_origin !== configuration.authorizationOrigin || typeof client.rp_id !== "string" || client.rp_id.length === 0 || !Array.isArray(client.redirects) || client.redirects.length < 1 || client.redirects.length > 20 || !client.redirects.every((redirect) => typeof redirect === "string" && exactHttpsRedirect(redirect)) || new Set(client.redirects).size !== client.redirects.length || !client.redirects.includes(configuration.redirectUri) || client.platform !== "web") {
650
+ throw new ClientFlowError("invalid_public_configuration");
651
+ }
652
+ return Object.freeze({
653
+ requestId: root.request_id,
654
+ id: client.id,
655
+ name: client.name,
656
+ environmentId: client.environment_id,
657
+ methods: Object.freeze([...client.methods]),
658
+ providerOrigin: client.provider_origin,
659
+ rpId: client.rp_id,
660
+ redirects: Object.freeze([...client.redirects]),
661
+ platform: client.platform,
662
+ });
663
+ }
664
+ function validateConfiguration(configuration) {
665
+ const allowedFields = ["applicationId", "applicationOrigin", "authorizationOrigin", "channel", "environmentId", "methods", "mobileApplicationIdentifier", "mode", "publishableClientId", "redirectUri", "tenantId"];
666
+ if (Object.keys(configuration).some((field) => !allowedFields.includes(field)))
667
+ throw new ClientFlowError("invalid_public_configuration");
668
+ if (configuration.mode !== "mode_b" || (configuration.channel !== "web" && configuration.channel !== "mobile") || !exactHttpsOrigin(configuration.authorizationOrigin) || !exactHttpsRedirect(configuration.redirectUri) || !opaqueId(configuration.publishableClientId) || !opaqueId(configuration.tenantId) || !opaqueId(configuration.applicationId) || !opaqueId(configuration.environmentId) || configuration.methods.length === 0 || new Set(configuration.methods).size !== configuration.methods.length || configuration.methods.some((method) => !identityMethods.includes(method))) {
669
+ throw new ClientFlowError("invalid_public_configuration");
670
+ }
671
+ if (configuration.channel === "web" && (!configuration.applicationOrigin || !exactHttpsOrigin(configuration.applicationOrigin) || configuration.mobileApplicationIdentifier !== undefined))
672
+ throw new ClientFlowError("invalid_public_configuration");
673
+ if (configuration.channel === "mobile" && (configuration.applicationOrigin !== undefined || !mobileApplicationIdentifier(configuration.mobileApplicationIdentifier)))
674
+ throw new ClientFlowError("invalid_public_configuration");
675
+ }
676
+ function mobileApplicationIdentifier(value) {
677
+ return typeof value === "string" && /^[A-Za-z0-9._-]{1,255}$/.test(value);
678
+ }
679
+ function exactMobileAssociation(value, redirectUri, applicationIdentifier) {
680
+ if (value === null || typeof value !== "object" || Array.isArray(value))
681
+ return false;
682
+ const result = value;
683
+ return Object.keys(result).length === 3 && result.source === "os_verified_https_association" && result.redirectUri === redirectUri && result.applicationIdentifier === applicationIdentifier;
684
+ }
685
+ function exactHttpsOrigin(value) {
686
+ try {
687
+ const parsed = new URL(value);
688
+ return parsed.protocol === "https:" && parsed.username === "" && parsed.password === "" && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "" && parsed.origin === value;
689
+ }
690
+ catch {
691
+ return false;
692
+ }
693
+ }
694
+ function exactHttpsRedirect(value) {
695
+ try {
696
+ const parsed = new URL(value);
697
+ return parsed.protocol === "https:" && parsed.username === "" && parsed.password === "" && parsed.search === "" && parsed.hash === "";
698
+ }
699
+ catch {
700
+ return false;
701
+ }
702
+ }
703
+ function httpsURL(value) {
704
+ if (typeof value !== "string")
705
+ return false;
706
+ try {
707
+ return new URL(value).protocol === "https:";
708
+ }
709
+ catch {
710
+ return false;
711
+ }
712
+ }
713
+ function exactCallback(value, expected) {
714
+ let callback;
715
+ try {
716
+ callback = new URL(value);
717
+ }
718
+ catch {
719
+ throw new ClientFlowError("invalid_callback");
720
+ }
721
+ const target = new URL(expected);
722
+ if (`${callback.origin}${callback.pathname}` !== `${target.origin}${target.pathname}` || callback.hash !== "" || callback.searchParams.size !== 3)
723
+ throw new ClientFlowError("invalid_callback");
724
+ const code = callback.searchParams.getAll("code");
725
+ const state = callback.searchParams.getAll("state");
726
+ const attemptId = callback.searchParams.getAll("attempt_id");
727
+ if (code.length !== 1 || state.length !== 1 || attemptId.length !== 1 || code[0] === "" || state[0] === "" || !opaqueId(attemptId[0]))
728
+ throw new ClientFlowError("invalid_callback");
729
+ return { code: code[0], state: state[0], attemptId: attemptId[0] };
730
+ }
731
+ function exactRestoreCallback(value, expected) {
732
+ let callback;
733
+ try {
734
+ callback = new URL(value);
735
+ }
736
+ catch {
737
+ throw new ClientFlowError("invalid_callback");
738
+ }
739
+ const target = new URL(expected);
740
+ if (`${callback.origin}${callback.pathname}` !== `${target.origin}${target.pathname}` || callback.hash !== "" || callback.searchParams.size !== 3)
741
+ throw new ClientFlowError("invalid_callback");
742
+ const code = callback.searchParams.getAll("code");
743
+ const state = callback.searchParams.getAll("state");
744
+ const restoreId = callback.searchParams.getAll("restore_id");
745
+ if (code.length !== 1 || state.length !== 1 || restoreId.length !== 1 || code[0] === "" || state[0] === "" || !opaqueId(restoreId[0]))
746
+ throw new ClientFlowError("invalid_callback");
747
+ return { code: code[0], state: state[0], restoreId: restoreId[0] };
748
+ }
749
+ function sameStrings(left, right) {
750
+ if (left.length !== right.length)
751
+ return false;
752
+ const expected = new Set(right);
753
+ return left.every((value) => typeof value === "string" && expected.has(value));
754
+ }