@synqauth/js 0.0.0-stage → 0.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,605 @@
1
+ import { a as problemError, i as isSynqError, n as secureUrl, o as toSynqError, r as SynqError, t as createApiClient } from "./api-Cf9K1Rc9.js";
2
+ import * as oauth from "oauth4webapi";
3
+ //#region src/storage.ts
4
+ /**
5
+ * Keeps everything in this process's memory: the default, and the only store
6
+ * that holds refresh tokens unless told. Clients that share one share its
7
+ * lock, so they never refresh one session twice at once.
8
+ */
9
+ function memoryStorage() {
10
+ const values = /* @__PURE__ */ new Map();
11
+ const locks = /* @__PURE__ */ new Map();
12
+ return {
13
+ holdsRefreshTokens: true,
14
+ get: (key) => values.get(key),
15
+ set: (key, value) => {
16
+ values.set(key, value);
17
+ },
18
+ remove: (key) => {
19
+ values.delete(key);
20
+ },
21
+ keys: () => [...values.keys()],
22
+ lock(name, fn) {
23
+ const run = (locks.get(name) ?? Promise.resolve()).then(fn, fn);
24
+ const settled = run.then(() => void 0, () => void 0);
25
+ locks.set(name, settled);
26
+ settled.then(() => {
27
+ if (locks.get(name) === settled) locks.delete(name);
28
+ });
29
+ return run;
30
+ }
31
+ };
32
+ }
33
+ /** A store on one of the browser's Web Storage areas. */
34
+ function webStorage(area) {
35
+ const store = globalThis[area];
36
+ if (!store) throw new SynqError("invalid_options", `${area} is only available in a browser`);
37
+ return {
38
+ holdsRefreshTokens: false,
39
+ get: (key) => store.getItem(key),
40
+ set: (key, value) => store.setItem(key, value),
41
+ remove: (key) => store.removeItem(key),
42
+ keys: () => {
43
+ const keys = [];
44
+ for (let i = 0; i < store.length; i++) {
45
+ const key = store.key(i);
46
+ if (key !== null) keys.push(key);
47
+ }
48
+ return keys;
49
+ }
50
+ };
51
+ }
52
+ /**
53
+ * The tab's `sessionStorage`: it ends with the tab, and no other tab reads
54
+ * it. As the session's store it keeps the access and ID tokens only, never
55
+ * the refresh token.
56
+ */
57
+ function browserSessionStorage() {
58
+ return webStorage("sessionStorage");
59
+ }
60
+ /**
61
+ * The browser's `localStorage`, which every tab of the app's origin shares.
62
+ * Where a sign-in's transaction goes by default in a browser: a magic link
63
+ * or an email's link opens a new tab, which finishes the sign-in and comes
64
+ * back to the app there, so the tab that started it can't be the only one
65
+ * that knows it. Each transaction is kept under its own state, for an hour
66
+ * at most, deleted when it finishes, and swept away when stale. The price:
67
+ * any script on the app's origin can read a transaction meanwhile (its PKCE
68
+ * verifier and nonce, useless without the code Synq sends to the app's own
69
+ * redirect URI), as it can read anything else the app keeps. As the
70
+ * session's store it keeps the access and ID tokens only, never the refresh
71
+ * token.
72
+ */
73
+ function browserLocalStorage() {
74
+ return webStorage("localStorage");
75
+ }
76
+ /**
77
+ * Where a sign-in's transaction is kept unless the app says: the browser's
78
+ * localStorage, else memory. Only a page has a `window`; Node's own Web
79
+ * Storage is no browser's (and warns when touched without a file).
80
+ */
81
+ function defaultTransactionStorage() {
82
+ const page = globalThis;
83
+ return typeof page.window === "object" && page.localStorage ? browserLocalStorage() : memoryStorage();
84
+ }
85
+ //#endregion
86
+ //#region src/client.ts
87
+ const SESSION_KEY = "synq.session";
88
+ const REFRESH_KEY = "synq.refresh";
89
+ const TRANSACTION_PREFIX = "synq.tx.";
90
+ /** The store's lock a refresh runs under, when the store has one. */
91
+ const REFRESH_LOCK = "synq.refresh";
92
+ /** How long a sign-in's transaction is kept: as long as Synq keeps an interaction open. */
93
+ const TRANSACTION_TTL_SECONDS = 3600;
94
+ /** Parameters that belong to the flow itself, which `params` can't replace. */
95
+ const FLOW_PARAMS = /* @__PURE__ */ new Set([
96
+ "client_id",
97
+ "response_type",
98
+ "redirect_uri",
99
+ "scope",
100
+ "state",
101
+ "nonce",
102
+ "code_challenge",
103
+ "code_challenge_method",
104
+ "resource",
105
+ "prompt",
106
+ "max_age",
107
+ "login_hint",
108
+ "synq_method",
109
+ "request",
110
+ "request_uri",
111
+ "response_mode"
112
+ ]);
113
+ /** A client for one app at one of a brand's Synq issuers. */
114
+ function createSynqClient(options) {
115
+ return new Client(options);
116
+ }
117
+ var Client = class {
118
+ #options;
119
+ #issuer;
120
+ #client;
121
+ #auth;
122
+ #http;
123
+ #storage;
124
+ #transactions;
125
+ #leeway;
126
+ #listeners = /* @__PURE__ */ new Set();
127
+ #server;
128
+ /** The session as last read or written; `undefined` until first read. */
129
+ #session;
130
+ /** The refresh token, here when the storage may not hold it. */
131
+ #refreshToken;
132
+ #refreshing;
133
+ /** Bumped by every sign-in and sign-out, so a refresh that started before one never writes over it. */
134
+ #generation = 0;
135
+ constructor(options) {
136
+ const issuer = secureUrl(options.issuer, "The issuer");
137
+ if (!options.clientId) throw new SynqError("invalid_options", "The clientId is missing");
138
+ if (!options.redirectUri) throw new SynqError("invalid_options", "The redirectUri is missing");
139
+ this.#options = options;
140
+ this.#issuer = issuer;
141
+ this.#client = { client_id: options.clientId };
142
+ this.#auth = options.clientSecret ? oauth.ClientSecretBasic(options.clientSecret) : oauth.None();
143
+ this.#http = {};
144
+ if (options.fetch) this.#http[oauth.customFetch] = options.fetch;
145
+ if (issuer.protocol === "http:") this.#http[oauth.allowInsecureRequests] = true;
146
+ this.#storage = options.storage ?? memoryStorage();
147
+ this.#transactions = options.transactionStorage ?? defaultTransactionStorage();
148
+ this.#leeway = options.refreshLeewaySeconds ?? 30;
149
+ }
150
+ async authorizationUrl(options = {}) {
151
+ const server = await this.#discover();
152
+ if (!server.authorization_endpoint) throw new SynqError("discovery_failed", "The issuer has no authorization endpoint");
153
+ await this.#sweepTransactions();
154
+ const verifier = oauth.generateRandomCodeVerifier();
155
+ const transaction = {
156
+ state: oauth.generateRandomState(),
157
+ nonce: oauth.generateRandomNonce(),
158
+ verifier,
159
+ redirectUri: this.#options.redirectUri,
160
+ resource: options.resource ?? this.#options.resource,
161
+ maxAge: options.maxAge,
162
+ createdAt: now()
163
+ };
164
+ const url = new URL(server.authorization_endpoint);
165
+ const params = url.searchParams;
166
+ for (const [name, value] of Object.entries(options.params ?? {})) {
167
+ if (FLOW_PARAMS.has(name)) throw new SynqError("invalid_options", `${name} is the flow's own parameter: use its option`);
168
+ params.set(name, value);
169
+ }
170
+ params.set("client_id", this.#options.clientId);
171
+ params.set("response_type", "code");
172
+ params.set("redirect_uri", transaction.redirectUri);
173
+ params.set("scope", options.scope ?? this.#options.scope ?? "openid");
174
+ params.set("state", transaction.state);
175
+ params.set("nonce", transaction.nonce);
176
+ params.set("code_challenge", await oauth.calculatePKCECodeChallenge(verifier));
177
+ params.set("code_challenge_method", "S256");
178
+ if (transaction.resource) params.set("resource", transaction.resource);
179
+ for (const extra of options.additionalResources ?? []) if (extra && extra !== transaction.resource) params.append("resource", extra);
180
+ if (options.prompt) params.set("prompt", options.prompt);
181
+ if (options.maxAge !== void 0) params.set("max_age", String(options.maxAge));
182
+ if (options.loginHint) params.set("login_hint", options.loginHint);
183
+ if (options.method) params.set("synq_method", options.method);
184
+ await this.#transactions.set(TRANSACTION_PREFIX + transaction.state, JSON.stringify(transaction));
185
+ if (!options.pushed) return url;
186
+ if (!server.pushed_authorization_request_endpoint) throw new SynqError("discovery_failed", "The issuer takes no pushed authorization requests");
187
+ let requestUri;
188
+ try {
189
+ const response = await oauth.pushedAuthorizationRequest(server, this.#client, this.#auth, new URLSearchParams([...params].filter(([name]) => name !== "client_id")), this.#http);
190
+ requestUri = (await oauth.processPushedAuthorizationResponse(server, this.#client, response)).request_uri;
191
+ } catch (error) {
192
+ await this.#transactions.remove(TRANSACTION_PREFIX + transaction.state);
193
+ throw toSynqError(error);
194
+ }
195
+ const pushed = new URL(server.authorization_endpoint);
196
+ pushed.searchParams.set("client_id", this.#options.clientId);
197
+ pushed.searchParams.set("request_uri", requestUri);
198
+ return pushed;
199
+ }
200
+ async signIn(options) {
201
+ const location = globalThis.location;
202
+ if (!location) throw new SynqError("invalid_options", "signIn() navigates a browser: use authorizationUrl()");
203
+ location.assign((await this.authorizationUrl(options)).href);
204
+ }
205
+ async handleCallback(url) {
206
+ const callback = new URL(url);
207
+ const state = callback.searchParams.get("state");
208
+ if (!state) throw new SynqError("unknown_state", "The callback has no state");
209
+ const key = TRANSACTION_PREFIX + state;
210
+ const stored = await this.#transactions.get(key);
211
+ await this.#transactions.remove(key);
212
+ const transaction = stored ? JSON.parse(stored) : void 0;
213
+ if (!transaction || now() - transaction.createdAt > TRANSACTION_TTL_SECONDS) throw new SynqError("unknown_state", "This sign-in wasn't started here, has already finished, or took too long");
214
+ const server = await this.#discover();
215
+ try {
216
+ const params = oauth.validateAuthResponse(server, this.#client, callback, transaction.state);
217
+ const response = await oauth.authorizationCodeGrantRequest(server, this.#client, this.#auth, params, transaction.redirectUri, transaction.verifier, {
218
+ ...this.#http,
219
+ additionalParameters: resourceParams(transaction.resource)
220
+ });
221
+ const result = await oauth.processAuthorizationCodeResponse(server, this.#client, response, {
222
+ expectedNonce: transaction.nonce,
223
+ maxAge: transaction.maxAge ?? oauth.skipAuthTimeCheck,
224
+ requireIdToken: true
225
+ });
226
+ const claims = oauth.getValidatedIdTokenClaims(result);
227
+ this.#generation += 1;
228
+ await this.#clear();
229
+ const session = await this.#save(result, claims);
230
+ this.#emit({
231
+ type: "signed-in",
232
+ session
233
+ });
234
+ return session;
235
+ } catch (error) {
236
+ throw toSynqError(error);
237
+ }
238
+ }
239
+ async getSession() {
240
+ const stored = await this.#load();
241
+ return stored ? await this.#public(stored) : null;
242
+ }
243
+ async restoreSession(options = {}) {
244
+ const stored = await this.#load();
245
+ if (!stored) return null;
246
+ const session = await this.#public(stored);
247
+ if (session.refreshable || fresh(stored, this.#leeway)) return session;
248
+ this.#generation += 1;
249
+ await this.#clear();
250
+ await this.signIn({
251
+ ...options,
252
+ prompt: "none"
253
+ });
254
+ return null;
255
+ }
256
+ async getAccessToken(options = {}) {
257
+ const stored = await this.#load();
258
+ if (!stored) throw new SynqError("not_signed_in", "Nobody is signed in");
259
+ if (fresh(stored, options.minValiditySeconds ?? this.#leeway)) return stored.accessToken;
260
+ return (await this.refresh()).accessToken;
261
+ }
262
+ refresh() {
263
+ this.#refreshing ??= this.#lockedRefresh().finally(() => {
264
+ this.#refreshing = void 0;
265
+ });
266
+ return this.#refreshing;
267
+ }
268
+ subscribe(listener) {
269
+ this.#listeners.add(listener);
270
+ return () => {
271
+ this.#listeners.delete(listener);
272
+ };
273
+ }
274
+ async signOut(options = {}) {
275
+ const stored = await this.#load();
276
+ const refreshToken = await this.#readRefreshToken();
277
+ this.#generation += 1;
278
+ await this.#clear();
279
+ if (stored) this.#emit({
280
+ type: "signed-out",
281
+ session: null
282
+ });
283
+ const server = await this.#discover();
284
+ if (refreshToken && server.revocation_endpoint) for (let attempt = 1; attempt <= 2; attempt++) try {
285
+ const response = await oauth.revocationRequest(server, this.#client, this.#auth, refreshToken, {
286
+ ...this.#http,
287
+ additionalParameters: { token_type_hint: "refresh_token" }
288
+ });
289
+ await oauth.processRevocationResponse(response);
290
+ break;
291
+ } catch (error) {
292
+ if (attempt === 2) console.warn("@synqauth/js: revoking the refresh token at sign-out failed; it stays usable until it expires", toSynqError(error).code);
293
+ }
294
+ if (!server.end_session_endpoint) return null;
295
+ const url = new URL(server.end_session_endpoint);
296
+ url.searchParams.set("client_id", this.#options.clientId);
297
+ if (stored?.idToken) url.searchParams.set("id_token_hint", stored.idToken);
298
+ const back = options.postLogoutRedirectUri ?? this.#options.postLogoutRedirectUri;
299
+ if (back) url.searchParams.set("post_logout_redirect_uri", back);
300
+ if (options.state) url.searchParams.set("state", options.state);
301
+ return url;
302
+ }
303
+ /**
304
+ * Refreshes under the store's lock, when it has one, so no other client
305
+ * sharing the store refreshes the same session at once. Inside it, the
306
+ * session is read again: when another client refreshed it meanwhile, its
307
+ * tokens are used, never the refresh token it already spent.
308
+ */
309
+ async #lockedRefresh() {
310
+ const storage = this.#storage;
311
+ if (!storage.lock) return this.#refresh();
312
+ const before = (await this.#load())?.accessToken;
313
+ return storage.lock(REFRESH_LOCK, async () => {
314
+ this.#session = void 0;
315
+ const current = await this.#load();
316
+ if (current && current.accessToken !== before && fresh(current, this.#leeway)) {
317
+ const session = await this.#public(current);
318
+ this.#emit({
319
+ type: "refreshed",
320
+ session
321
+ });
322
+ return session;
323
+ }
324
+ return this.#refresh();
325
+ });
326
+ }
327
+ /** Removes sign-ins that were started but never finished, where the store can list them. */
328
+ async #sweepTransactions() {
329
+ const store = this.#transactions;
330
+ if (!store.keys) return;
331
+ try {
332
+ for (const key of await store.keys()) {
333
+ if (!key.startsWith(TRANSACTION_PREFIX)) continue;
334
+ const raw = await store.get(key);
335
+ let createdAt;
336
+ try {
337
+ createdAt = raw ? JSON.parse(raw).createdAt : void 0;
338
+ } catch {
339
+ createdAt = void 0;
340
+ }
341
+ if (typeof createdAt !== "number" || now() - createdAt > TRANSACTION_TTL_SECONDS) await store.remove(key);
342
+ }
343
+ } catch {}
344
+ }
345
+ async #refresh() {
346
+ const generation = this.#generation;
347
+ const stored = await this.#load();
348
+ if (!stored) throw new SynqError("not_signed_in", "Nobody is signed in");
349
+ const refreshToken = await this.#readRefreshToken();
350
+ if (!refreshToken) throw new SynqError("no_refresh_token", "This session has no refresh token: sign in again, or ask for offline_access");
351
+ const server = await this.#discover();
352
+ let result;
353
+ try {
354
+ const response = await oauth.refreshTokenGrantRequest(server, this.#client, this.#auth, refreshToken, {
355
+ ...this.#http,
356
+ additionalParameters: resourceParams(this.#options.resource)
357
+ });
358
+ result = await oauth.processRefreshTokenResponse(server, this.#client, response);
359
+ } catch (error) {
360
+ const failure = toSynqError(error);
361
+ if (failure.code === "invalid_grant" && generation === this.#generation) {
362
+ this.#generation += 1;
363
+ await this.#clear();
364
+ this.#emit({
365
+ type: "signed-out",
366
+ session: null
367
+ });
368
+ }
369
+ throw failure;
370
+ }
371
+ if (generation !== this.#generation) throw new SynqError("not_signed_in", "The session ended while it was being refreshed");
372
+ const claims = oauth.getValidatedIdTokenClaims(result);
373
+ if (claims && stored.claims && claims.sub !== stored.claims.sub) throw new SynqError("invalid_response", "The refreshed ID token names someone else");
374
+ const session = await this.#save(result, claims ?? stored.claims, stored);
375
+ this.#emit({
376
+ type: "refreshed",
377
+ session
378
+ });
379
+ return session;
380
+ }
381
+ #discover() {
382
+ this.#server ??= (async () => {
383
+ try {
384
+ const response = await oauth.discoveryRequest(this.#issuer, {
385
+ ...this.#http,
386
+ algorithm: "oidc"
387
+ });
388
+ return await oauth.processDiscoveryResponse(this.#issuer, response);
389
+ } catch (error) {
390
+ this.#server = void 0;
391
+ const failure = toSynqError(error);
392
+ throw failure.code === "network_error" ? failure : new SynqError("discovery_failed", `Synq's discovery document at ${this.#issuer.href} couldn't be read`, { cause: error });
393
+ }
394
+ })();
395
+ return this.#server;
396
+ }
397
+ async #save(result, claims, previous) {
398
+ const stored = {
399
+ accessToken: result.access_token,
400
+ tokenType: result.token_type,
401
+ expiresAt: result.expires_in === void 0 ? void 0 : now() + result.expires_in,
402
+ scope: result.scope ?? previous?.scope,
403
+ idToken: result.id_token ?? previous?.idToken,
404
+ claims
405
+ };
406
+ this.#session = stored;
407
+ await this.#storage.set(SESSION_KEY, JSON.stringify(stored));
408
+ if (result.refresh_token) await this.#writeRefreshToken(result.refresh_token);
409
+ return await this.#public(stored);
410
+ }
411
+ async #load() {
412
+ if (this.#session === void 0) {
413
+ const raw = await this.#storage.get(SESSION_KEY);
414
+ this.#session = raw ? JSON.parse(raw) : null;
415
+ }
416
+ return this.#session;
417
+ }
418
+ async #readRefreshToken() {
419
+ if (this.#storage.holdsRefreshTokens) return await this.#storage.get(REFRESH_KEY) ?? void 0;
420
+ return this.#refreshToken;
421
+ }
422
+ async #writeRefreshToken(token) {
423
+ if (this.#storage.holdsRefreshTokens) await this.#storage.set(REFRESH_KEY, token);
424
+ else this.#refreshToken = token;
425
+ }
426
+ async #clear() {
427
+ this.#session = null;
428
+ this.#refreshToken = void 0;
429
+ await this.#storage.remove(SESSION_KEY);
430
+ await this.#storage.remove(REFRESH_KEY);
431
+ }
432
+ async #public(stored) {
433
+ return {
434
+ ...stored,
435
+ refreshable: await this.#readRefreshToken() !== void 0
436
+ };
437
+ }
438
+ #emit(event) {
439
+ for (const listener of this.#listeners) try {
440
+ listener(event);
441
+ } catch {}
442
+ }
443
+ };
444
+ function now() {
445
+ return Math.floor(Date.now() / 1e3);
446
+ }
447
+ /** Whether a session's access token is good for `leeway` seconds more (or Synq gave it no lifetime). */
448
+ function fresh(session, leeway) {
449
+ return session.expiresAt === void 0 || session.expiresAt - now() > leeway;
450
+ }
451
+ function resourceParams(resource) {
452
+ return resource ? { resource } : void 0;
453
+ }
454
+ //#endregion
455
+ //#region src/account.ts
456
+ /**
457
+ * The sign-in that finishes a link request, for `SynqClient.signIn` or
458
+ * `authorizationUrl`: a fresh sign-in (`prompt=login`) carrying the request.
459
+ */
460
+ function linkSignIn(request) {
461
+ const { prompt, ...params } = request.authorization_params;
462
+ return {
463
+ prompt,
464
+ params
465
+ };
466
+ }
467
+ /** The account API's URL for a brand: the resource its access tokens are for. */
468
+ function accountApiUrl(issuer) {
469
+ return `${secureUrl(String(issuer), "The issuer").href.replace(/\/+$/, "")}/me`;
470
+ }
471
+ /** A client for the brand's account API, for an app that holds its own session: everything the signed-in person can do to their own account. */
472
+ function createAccountClient(options) {
473
+ if (typeof options.accessToken !== "function") throw new SynqError("invalid_options", "Give the account client an accessToken function");
474
+ const raw = createApiClient({
475
+ baseUrl: accountApiUrl(options.issuer),
476
+ credential: options.accessToken,
477
+ fetch: options.fetch
478
+ });
479
+ async function run(send, pick) {
480
+ try {
481
+ const json = await send();
482
+ return {
483
+ data: pick ? pick(json) : json,
484
+ error: null
485
+ };
486
+ } catch (error) {
487
+ return {
488
+ data: null,
489
+ error: error instanceof SynqError ? error : new SynqError("network_error", String(error), { cause: error })
490
+ };
491
+ }
492
+ }
493
+ const read = (path, opts = {}, request = {}) => run(() => raw.get(path, {
494
+ ...request,
495
+ signal: opts.signal
496
+ }));
497
+ const list = (path, opts = {}) => run(() => raw.get(path, { signal: opts.signal }), (json) => json.data);
498
+ const change = (method, path, opts = {}, request = {}) => run(() => raw[method](path, {
499
+ ...request,
500
+ idempotencyKey: opts.idempotencyKey ?? crypto.randomUUID(),
501
+ signal: opts.signal
502
+ }), (json) => json === void 0 ? null : json);
503
+ return {
504
+ me: (opts) => read("/v1/profile", opts),
505
+ user: {
506
+ update: (changes, opts) => change("patch", "/v1/profile", opts, { body: changes }),
507
+ metadata: (opts) => read("/v1/metadata", opts),
508
+ usernameAvailable: (username, opts) => read("/v1/usernames/{username}", opts, { path: { username } })
509
+ },
510
+ wallets: {
511
+ list: (opts) => list("/v1/wallets", opts),
512
+ update: (wallet, changes, opts) => change("patch", "/v1/wallets/{wallet}", opts, {
513
+ path: { wallet },
514
+ body: changes
515
+ }),
516
+ remove: (wallet, opts) => change("delete", "/v1/identities/{identity}", opts, { path: { identity: wallet } })
517
+ },
518
+ accounts: {
519
+ list: (opts) => list("/v1/identities", opts),
520
+ link: (method, opts) => change("post", "/v1/link-requests", opts, { body: { method } }),
521
+ unlink: (identity, opts) => change("delete", "/v1/identities/{identity}", opts, { path: { identity } })
522
+ },
523
+ sessions: {
524
+ list: (opts) => list("/v1/sessions", opts),
525
+ revoke: (session, opts) => change("delete", "/v1/sessions/{session}", opts, { path: { session } })
526
+ },
527
+ passkeys: {
528
+ list: (opts) => list("/v1/passkeys", opts),
529
+ registrationOptions: (opts) => change("post", "/v1/passkeys/registration-options", opts),
530
+ register: (credential, opts) => change("post", "/v1/passkeys", opts, { body: { credential } }),
531
+ remove: (passkey, opts) => change("delete", "/v1/passkeys/{passkey}", opts, { path: { passkey } })
532
+ },
533
+ consents: {
534
+ list: (opts) => list("/v1/consents", opts),
535
+ withdraw: (consent, opts) => change("delete", "/v1/consents/{consent}", opts, { path: { consent } })
536
+ },
537
+ activity: (opts) => list("/v1/sign-ins", opts),
538
+ deletion: {
539
+ schedule: (opts) => change("post", "/v1/deletion", opts),
540
+ cancel: (opts) => change("delete", "/v1/deletion", opts)
541
+ }
542
+ };
543
+ }
544
+ //#endregion
545
+ //#region src/generated/errors.ts
546
+ /** Each `code` a Synq API's problem documents carry, with its HTTP status. */
547
+ const ERROR_CODES = {
548
+ unauthorized: 401,
549
+ invalid_token: 401,
550
+ token_expired: 401,
551
+ api_key_revoked: 401,
552
+ forbidden: 403,
553
+ insufficient_permissions: 403,
554
+ not_found: 404,
555
+ validation_failed: 400,
556
+ idempotency_key_conflict: 422,
557
+ idempotency_key_in_flight: 409,
558
+ conflict: 409,
559
+ org_slug_taken: 409,
560
+ issuer_slug_taken: 409,
561
+ issuer_slug_immutable: 422,
562
+ plan_quota_exceeded: 403,
563
+ plan_feature_unavailable: 403,
564
+ org_creation_closed: 403,
565
+ email_unverified: 403,
566
+ org_quota_exceeded: 403,
567
+ recipient_org_quota_exceeded: 403,
568
+ fresh_sign_in_required: 403,
569
+ username_taken: 409,
570
+ last_sign_in_method: 409,
571
+ passkey_exists: 409,
572
+ sign_in_method_unavailable: 409,
573
+ identity_limit_reached: 409,
574
+ connection_rejected: 422,
575
+ token_gates_unavailable: 409,
576
+ passkey_rejected: 400,
577
+ email_undeliverable: 422,
578
+ email_domain_unavailable: 409,
579
+ email_domain_invalid: 422,
580
+ email_domain_limit_reached: 409,
581
+ email_domain_in_use: 409,
582
+ plan_not_sold: 422,
583
+ subscription_exists: 409,
584
+ no_billing_account: 409,
585
+ no_subscription: 409,
586
+ payment_failed: 402,
587
+ custom_domain_exists: 409,
588
+ hostname_taken: 409,
589
+ invalid_hostname: 422,
590
+ apex_not_supported: 422,
591
+ reserved_hostname: 422,
592
+ hostname_denied: 422,
593
+ hostname_not_allowed_here: 422,
594
+ payload_too_large: 413,
595
+ unsupported_media_type: 415,
596
+ upload_incomplete: 409,
597
+ upload_expired: 410,
598
+ file_rejected: 422,
599
+ upload_limit_reached: 409,
600
+ rate_limited: 429,
601
+ internal_error: 500,
602
+ service_unavailable: 503
603
+ };
604
+ //#endregion
605
+ export { ERROR_CODES, SynqError, accountApiUrl, browserLocalStorage, browserSessionStorage, createAccountClient, createSynqClient, isSynqError, linkSignIn, memoryStorage, problemError };