@korajs/auth 1.0.0-beta.12 → 1.0.0-beta.14

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 (101) hide show
  1. package/README.md +52 -47
  2. package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
  3. package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
  4. package/dist/index.cjs +645 -150
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +27 -9
  7. package/dist/index.d.ts +27 -9
  8. package/dist/index.js +644 -150
  9. package/dist/index.js.map +1 -1
  10. package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
  11. package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
  12. package/dist/react.d.cts +2 -2
  13. package/dist/react.d.ts +2 -2
  14. package/dist/server.cjs +2880 -1667
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +810 -169
  17. package/dist/server.d.ts +810 -169
  18. package/dist/server.js +2848 -1646
  19. package/dist/server.js.map +1 -1
  20. package/dist/svelte.cjs +2 -2
  21. package/dist/svelte.cjs.map +1 -1
  22. package/dist/svelte.d.cts +2 -2
  23. package/dist/svelte.d.ts +2 -2
  24. package/dist/svelte.js +2 -2
  25. package/dist/svelte.js.map +1 -1
  26. package/dist/vue.d.cts +1 -1
  27. package/dist/vue.d.ts +1 -1
  28. package/package.json +7 -7
  29. package/src/admin/admin-api.ts +327 -0
  30. package/src/admin/audit-log.ts +324 -0
  31. package/src/admin/webhooks.ts +576 -0
  32. package/src/bindings/create-auth-session.ts +184 -0
  33. package/src/bindings/create-org-session.ts +130 -0
  34. package/src/client/auth-client.ts +1592 -0
  35. package/src/client/auth-sync.ts +213 -0
  36. package/src/client/device-session.ts +104 -0
  37. package/src/client/org-client.ts +399 -0
  38. package/src/client/quickstart.ts +108 -0
  39. package/src/client/storage.ts +94 -0
  40. package/src/device/device-identity.ts +330 -0
  41. package/src/device/device-store.ts +379 -0
  42. package/src/encryption/auto-lock.ts +170 -0
  43. package/src/encryption/database-encryption.ts +265 -0
  44. package/src/encryption/key-derivation.ts +149 -0
  45. package/src/encryption/operation-encryptor.ts +361 -0
  46. package/src/index.ts +132 -0
  47. package/src/mfa/totp.ts +826 -0
  48. package/src/org/org-routes.ts +758 -0
  49. package/src/org/org-store.ts +490 -0
  50. package/src/org/org-types.ts +230 -0
  51. package/src/passkey/passkey-client.ts +597 -0
  52. package/src/passkey/passkey-server.ts +779 -0
  53. package/src/postgres/ensure-schema.ts +65 -0
  54. package/src/provider/adapter.ts +246 -0
  55. package/src/provider/built-in/auth-routes.ts +1313 -0
  56. package/src/provider/built-in/email-verification.ts +303 -0
  57. package/src/provider/built-in/password-hash.ts +118 -0
  58. package/src/provider/built-in/password-reset.ts +416 -0
  59. package/src/provider/built-in/postgres-user-store.ts +365 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +335 -0
  62. package/src/provider/built-in/sync-scopes.ts +85 -0
  63. package/src/provider/built-in/user-store.ts +465 -0
  64. package/src/provider/external/clerk-adapter.ts +157 -0
  65. package/src/provider/external/external-jwt-provider.ts +491 -0
  66. package/src/provider/external/supabase-adapter.ts +163 -0
  67. package/src/provider/oauth/linked-identity-store.ts +108 -0
  68. package/src/provider/oauth/oauth-flow.ts +550 -0
  69. package/src/provider/oauth/oauth-types.ts +184 -0
  70. package/src/provider/oauth/postgres-oauth-store.ts +296 -0
  71. package/src/provider/oauth/sqlite-oauth-store.ts +285 -0
  72. package/src/rbac/rbac-engine.ts +323 -0
  73. package/src/rbac/rbac-types.ts +210 -0
  74. package/src/rbac/scope-resolver.ts +140 -0
  75. package/src/react/AuthProvider.tsx +97 -0
  76. package/src/react/OrgProvider.tsx +41 -0
  77. package/src/react/auth-context.ts +26 -0
  78. package/src/react/hooks.ts +110 -0
  79. package/src/react/org-hooks.ts +214 -0
  80. package/src/react.ts +26 -0
  81. package/src/server.ts +338 -0
  82. package/src/session/session.ts +401 -0
  83. package/src/svelte/auth-context.ts +50 -0
  84. package/src/svelte/org-context.ts +32 -0
  85. package/src/svelte/org-hooks.ts +201 -0
  86. package/src/svelte/use-auth.ts +115 -0
  87. package/src/svelte.ts +25 -0
  88. package/src/tokens/encrypted-token-store.ts +360 -0
  89. package/src/tokens/jwt.ts +236 -0
  90. package/src/tokens/postgres-token-revocation-store.ts +140 -0
  91. package/src/tokens/sqlite-token-revocation-store.ts +121 -0
  92. package/src/tokens/token-manager.ts +821 -0
  93. package/src/tokens/token-store.ts +192 -0
  94. package/src/types.ts +394 -0
  95. package/src/vue/auth-context.ts +10 -0
  96. package/src/vue/auth-provider-types.ts +5 -0
  97. package/src/vue/auth-provider.ts +76 -0
  98. package/src/vue/org-hooks.ts +193 -0
  99. package/src/vue/org-provider.ts +49 -0
  100. package/src/vue/use-auth.ts +139 -0
  101. package/src/vue.ts +10 -0
package/dist/index.js CHANGED
@@ -38,7 +38,19 @@ var AuthError = class extends KoraError {
38
38
  this.name = "AuthError";
39
39
  }
40
40
  };
41
+ var MfaRequiredError = class extends AuthError {
42
+ constructor(mfaToken) {
43
+ super("A second factor is required to finish signing in.", "AUTH_MFA_REQUIRED");
44
+ this.mfaToken = mfaToken;
45
+ this.name = "MfaRequiredError";
46
+ }
47
+ mfaToken;
48
+ };
41
49
  var EXPIRY_BUFFER_SECONDS = 30;
50
+ var DEFAULT_REQUEST_TIMEOUT_MS = 2e4;
51
+ var DEFAULT_BACKOFF_BASE_MS = 2e3;
52
+ var DEFAULT_BACKOFF_MAX_MS = 5 * 6e4;
53
+ var CLOCK_ROLLBACK_TOLERANCE_MS = 5 * 6e4;
42
54
  function decodeJwtPayload(token) {
43
55
  const parts = token.split(".");
44
56
  if (parts.length !== 3) {
@@ -47,25 +59,28 @@ function decodeJwtPayload(token) {
47
59
  try {
48
60
  const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
49
61
  const json = atob(base64);
50
- return JSON.parse(json);
62
+ const parsed = JSON.parse(json);
63
+ return isRecord(parsed) ? parsed : null;
51
64
  } catch {
52
65
  return null;
53
66
  }
54
67
  }
55
- function isTokenExpired(token) {
68
+ function isTokenExpired(token, bufferSeconds = EXPIRY_BUFFER_SECONDS) {
56
69
  const payload = decodeJwtPayload(token);
57
70
  if (!payload || typeof payload.exp !== "number") {
58
71
  return true;
59
72
  }
60
73
  const nowSeconds = Math.floor(Date.now() / 1e3);
61
- return payload.exp <= nowSeconds + EXPIRY_BUFFER_SECONDS;
74
+ return payload.exp <= nowSeconds + bufferSeconds;
62
75
  }
63
- function getUserIdFromToken(token) {
76
+ function tokenIssuedAtMs(token) {
64
77
  const payload = decodeJwtPayload(token);
65
- if (!payload || typeof payload.sub !== "string") {
66
- return null;
67
- }
68
- return payload.sub;
78
+ if (!payload) return null;
79
+ if (typeof payload.iatMs === "number") return payload.iatMs;
80
+ return typeof payload.iat === "number" ? payload.iat * 1e3 : null;
81
+ }
82
+ function isRecord(value) {
83
+ return typeof value === "object" && value !== null && !Array.isArray(value);
69
84
  }
70
85
  function getDefaultFetch() {
71
86
  if (typeof globalThis.fetch !== "function") {
@@ -97,6 +112,46 @@ function redirectCurrentWindow(url) {
97
112
  }
98
113
  globalThis.window.location.assign(url);
99
114
  }
115
+ var OAUTH_BINDING_PREFIX = "kora_oauth_binding:";
116
+ function getSessionStorage() {
117
+ try {
118
+ const storage = globalThis.sessionStorage;
119
+ return storage && typeof storage.getItem === "function" ? storage : null;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+ var DEFINITIVE_REFRESH_CODES = /* @__PURE__ */ new Set(["REFRESH_TOKEN_INVALID", "invalid_grant"]);
125
+ function isDefinitiveRejection(response) {
126
+ if (!isRecord(response.json)) return false;
127
+ const code = typeof response.json.code === "string" ? response.json.code : void 0;
128
+ const error = typeof response.json.error === "string" ? response.json.error : void 0;
129
+ if (response.status === 401) return code !== void 0 || error !== void 0;
130
+ if (response.status === 400) {
131
+ return code !== void 0 && DEFINITIVE_REFRESH_CODES.has(code) || error !== void 0 && DEFINITIVE_REFRESH_CODES.has(error);
132
+ }
133
+ return false;
134
+ }
135
+ function parseRetryAfter(value) {
136
+ if (!value) return void 0;
137
+ const seconds = Number(value);
138
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
139
+ const date = Date.parse(value);
140
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
141
+ }
142
+ function readTokenPair(json) {
143
+ if (!isRecord(json)) return null;
144
+ const data = json.data !== void 0 ? json.data : json;
145
+ if (!isRecord(data)) return null;
146
+ return typeof data.accessToken === "string" && typeof data.refreshToken === "string" ? { accessToken: data.accessToken, refreshToken: data.refreshToken } : null;
147
+ }
148
+ var inProcessLocks = /* @__PURE__ */ new WeakMap();
149
+ function getWebLocks() {
150
+ const nav = globalThis.navigator;
151
+ const locks = nav?.locks;
152
+ if (typeof locks !== "object" || locks === null) return null;
153
+ return typeof locks.request === "function" ? locks : null;
154
+ }
100
155
  function createTokenStorage(prefix) {
101
156
  let useLocalStorage = false;
102
157
  try {
@@ -153,10 +208,22 @@ var AuthClient = class {
153
208
  fetchFn;
154
209
  deviceIdentity;
155
210
  listeners = /* @__PURE__ */ new Set();
211
+ sessionListeners = /* @__PURE__ */ new Set();
212
+ requestTimeoutMs;
213
+ maxOfflineGraceMs;
214
+ backoffBaseMs;
215
+ backoffMaxMs;
216
+ lockName;
156
217
  _state = "loading";
157
218
  _user = null;
158
219
  _refreshPromise = null;
159
220
  _initialized = false;
221
+ sessionStatus = "fresh";
222
+ lastServerContactAt = 0;
223
+ failureCount = 0;
224
+ nextAttemptAt = 0;
225
+ retryTimer = null;
226
+ detachEnvironment;
160
227
  /**
161
228
  * Creates a new AuthClient.
162
229
  *
@@ -168,6 +235,12 @@ var AuthClient = class {
168
235
  this.storage = config.storage ?? createTokenStorage(prefix);
169
236
  this.fetchFn = config.fetch ?? getDefaultFetch();
170
237
  this.deviceIdentity = config.deviceIdentity;
238
+ this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
239
+ this.maxOfflineGraceMs = config.maxOfflineGraceMs;
240
+ this.backoffBaseMs = config.refreshBackoff?.baseDelayMs ?? DEFAULT_BACKOFF_BASE_MS;
241
+ this.backoffMaxMs = config.refreshBackoff?.maxDelayMs ?? DEFAULT_BACKOFF_MAX_MS;
242
+ this.lockName = `kora-auth-refresh:${prefix}`;
243
+ this.detachEnvironment = this.attachEnvironmentListeners();
171
244
  }
172
245
  // -----------------------------------------------------------------------
173
246
  // Public getters
@@ -180,10 +253,54 @@ var AuthClient = class {
180
253
  get currentUser() {
181
254
  return this._user;
182
255
  }
183
- /** Whether the user is currently authenticated. */
256
+ /** Whether the user is currently authenticated (fresh or offline). */
184
257
  get isAuthenticated() {
185
258
  return this._state === "authenticated";
186
259
  }
260
+ /**
261
+ * The stored session identity and its freshness, or null when signed out.
262
+ * Unlike {@link getAccessToken}, this is available offline: the identity is
263
+ * decoupled from whether a fresh access token can be minted right now.
264
+ */
265
+ get session() {
266
+ if (this._state !== "authenticated" || !this._user) return null;
267
+ return {
268
+ userId: this._user.id,
269
+ deviceId: this.cachedDeviceId,
270
+ status: this.sessionStatus,
271
+ lastServerContactAt: this.lastServerContactAt
272
+ };
273
+ }
274
+ cachedDeviceId = null;
275
+ /**
276
+ * Read the stored session identity (user id and device id) from storage,
277
+ * without any network request. Returns null when no usable session is stored.
278
+ */
279
+ async getStoredIdentity() {
280
+ const claims = await this.getStoredClaims();
281
+ if (!claims || typeof claims.sub !== "string" || claims.sub.length === 0) return null;
282
+ return {
283
+ userId: claims.sub,
284
+ deviceId: typeof claims.dev === "string" && claims.dev.length > 0 ? claims.dev : null
285
+ };
286
+ }
287
+ /**
288
+ * Decoded (unverified) claims of the stored credentials, preferring the access
289
+ * token even when it has expired. For client-side hints only (local database
290
+ * name, sync node id, handshake scope narrowing); the server re-derives
291
+ * everything it authorizes from a verified token.
292
+ */
293
+ async getStoredClaims() {
294
+ const access = await this.storage.getAccessToken();
295
+ const refresh = await this.storage.getRefreshToken();
296
+ if (!refresh) return null;
297
+ for (const token of [access, refresh]) {
298
+ if (!token) continue;
299
+ const claims = decodeJwtPayload(token);
300
+ if (claims && typeof claims.sub === "string" && claims.sub.length > 0) return claims;
301
+ }
302
+ return null;
303
+ }
187
304
  // -----------------------------------------------------------------------
188
305
  // Initialization
189
306
  // -----------------------------------------------------------------------
@@ -192,6 +309,8 @@ var AuthClient = class {
192
309
  *
193
310
  * Loads tokens from storage, validates the access token, and attempts a
194
311
  * refresh if the access token is expired but a refresh token is available.
312
+ * When the auth server cannot be reached, the stored session is restored as
313
+ * authenticated-offline instead of being discarded.
195
314
  * Safe to call multiple times -- subsequent calls are no-ops once initialized.
196
315
  */
197
316
  async initialize() {
@@ -205,20 +324,21 @@ var AuthClient = class {
205
324
  this.setState("unauthenticated", null);
206
325
  return;
207
326
  }
327
+ this.noteIssuedCredential(refreshToken);
208
328
  if (!isTokenExpired(accessToken)) {
209
329
  await this.restoreSession(accessToken);
210
330
  return;
211
331
  }
212
- try {
213
- const newAccessToken = await this.refreshAccessToken(refreshToken);
214
- if (newAccessToken) {
215
- await this.restoreSession(newAccessToken);
216
- return;
217
- }
218
- } catch {
332
+ const outcome = await this.refresh();
333
+ if (outcome.kind === "ok") {
334
+ await this.restoreSession(outcome.accessToken);
335
+ return;
219
336
  }
220
- await this.storage.clear();
221
- this.setState("unauthenticated", null);
337
+ if (outcome.kind === "rejected") {
338
+ this.setState("unauthenticated", null);
339
+ return;
340
+ }
341
+ await this.enterOfflineSession();
222
342
  }
223
343
  // -----------------------------------------------------------------------
224
344
  // Sign up / Sign in / Sign out
@@ -236,11 +356,7 @@ var AuthClient = class {
236
356
  method: "POST",
237
357
  body
238
358
  });
239
- const tokens = "tokens" in response ? response.tokens : response;
240
- await this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
241
- const user = await this.fetchUserProfile(tokens.accessToken);
242
- this.setState("authenticated", user);
243
- return user;
359
+ return this.completeSignIn(response);
244
360
  }
245
361
  /**
246
362
  * Sign in with email and password.
@@ -255,11 +371,22 @@ var AuthClient = class {
255
371
  method: "POST",
256
372
  body
257
373
  });
258
- const tokens = "tokens" in response ? response.tokens : response;
259
- await this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
260
- const user = await this.fetchUserProfile(tokens.accessToken);
261
- this.setState("authenticated", user);
262
- return user;
374
+ return this.completeSignIn(response);
375
+ }
376
+ /**
377
+ * Finish a sign-in that required a second factor.
378
+ *
379
+ * @param mfaToken - From the {@link MfaRequiredError} thrown by sign-in
380
+ * @param proof - A current TOTP code, or a recovery code
381
+ * @returns The authenticated AuthUser
382
+ * @throws {AuthError} If the code or the MFA session is invalid
383
+ */
384
+ async verifyMfa(mfaToken, proof) {
385
+ const response = await this.request("/auth/mfa/verify", {
386
+ method: "POST",
387
+ body: { mfaToken, ...proof }
388
+ });
389
+ return this.completeSignIn(response);
263
390
  }
264
391
  /**
265
392
  * Create an OAuth authorization URL and optionally redirect the current window.
@@ -280,14 +407,19 @@ var AuthClient = class {
280
407
  */
281
408
  async completeOAuthSignIn(provider, params) {
282
409
  const body = await this.withDeviceIdentity(params);
410
+ const binding = params.binding ?? this.takeOAuthBinding(params.state);
283
411
  const response = await this.request(
284
412
  `/auth/oauth/${encodeURIComponent(provider)}/callback`,
285
413
  {
286
414
  method: "POST",
287
- body: { ...body }
415
+ body: { ...body, ...binding ? { binding } : {} }
288
416
  }
289
417
  );
418
+ if ("mfaRequired" in response) {
419
+ throw new MfaRequiredError(response.mfaToken);
420
+ }
290
421
  await this.storage.setTokens(response.tokens.accessToken, response.tokens.refreshToken);
422
+ this.markFresh(response.tokens.refreshToken);
291
423
  const user = normalizeAuthUser(response.user);
292
424
  this.setState("authenticated", user);
293
425
  return user;
@@ -295,19 +427,27 @@ var AuthClient = class {
295
427
  /**
296
428
  * Create an OAuth authorization URL for linking another provider to the current user.
297
429
  */
298
- async getOAuthAuthorizationUrl(provider, options = {}) {
299
- return this.createOAuthAuthorization(provider, { ...options, redirect: false });
430
+ async getOAuthAuthorizationUrl(provider, _options = {}) {
431
+ const token = await this.requireAccessToken();
432
+ const result = await this.request(
433
+ `/auth/oauth/${encodeURIComponent(provider)}/link/start`,
434
+ { method: "POST", body: {}, token }
435
+ );
436
+ this.rememberOAuthBinding(result);
437
+ return result;
300
438
  }
301
439
  /**
302
440
  * Link an OAuth provider to the current authenticated user.
303
441
  */
304
442
  async linkOAuth(provider, params) {
305
443
  const token = await this.requireAccessToken();
444
+ const binding = params.binding ?? this.takeOAuthBinding(params.state);
306
445
  return this.request(`/auth/oauth/${encodeURIComponent(provider)}/link`, {
307
446
  method: "POST",
308
447
  body: {
309
448
  code: params.code,
310
- state: params.state
449
+ state: params.state,
450
+ ...binding ? { binding } : {}
311
451
  },
312
452
  token
313
453
  });
@@ -344,6 +484,9 @@ var AuthClient = class {
344
484
  const refreshToken = await this.storage.getRefreshToken();
345
485
  await this.storage.clear();
346
486
  this._refreshPromise = null;
487
+ this.resetBackoff();
488
+ this.sessionStatus = "fresh";
489
+ this.cachedDeviceId = null;
347
490
  this.setState("unauthenticated", null);
348
491
  if (accessToken) {
349
492
  try {
@@ -362,24 +505,46 @@ var AuthClient = class {
362
505
  /**
363
506
  * Get a valid access token, automatically refreshing if expired.
364
507
  *
365
- * @returns A valid access token string, or null if the user is not
366
- * authenticated and refresh is not possible
508
+ * Returns null when no fresh token can be obtained right now. That does NOT
509
+ * mean the user is signed out: check {@link state} / {@link session}. A
510
+ * transient failure keeps the session (authenticated-offline) and later calls
511
+ * retry with backoff.
512
+ *
513
+ * @returns A valid access token string, or null if none is available now
367
514
  */
368
515
  async getAccessToken() {
369
516
  const accessToken = await this.storage.getAccessToken();
370
517
  if (accessToken && !isTokenExpired(accessToken)) {
518
+ if (this._state === "authenticated" && this.sessionStatus !== "fresh") {
519
+ this.markFresh(await this.storage.getRefreshToken());
520
+ }
371
521
  return accessToken;
372
522
  }
373
523
  const refreshToken = await this.storage.getRefreshToken();
374
524
  if (!refreshToken) {
375
525
  return null;
376
526
  }
377
- try {
378
- const newAccessToken = await this.refreshAccessToken(refreshToken);
379
- return newAccessToken;
380
- } catch {
527
+ const outcome = await this.refresh();
528
+ return outcome.kind === "ok" ? outcome.accessToken : null;
529
+ }
530
+ /**
531
+ * Refresh the session now, even when the cached access token has not expired
532
+ * locally. Used after the sync server ended a session with `AUTH_EXPIRED` or
533
+ * `AUTH_REVOKED`: the server's clock or a revocation says the cached token is
534
+ * no longer good. Concurrent calls (and other tabs) share one refresh.
535
+ *
536
+ * A transient failure keeps the session (authenticated-offline) and returns
537
+ * null; only a definitive server rejection signs the user out.
538
+ *
539
+ * @returns The refreshed access token, or null if none could be obtained now
540
+ */
541
+ async refreshAccessToken() {
542
+ const refreshToken = await this.storage.getRefreshToken();
543
+ if (!refreshToken) {
381
544
  return null;
382
545
  }
546
+ const outcome = await this.refresh();
547
+ return outcome.kind === "ok" ? outcome.accessToken : null;
383
548
  }
384
549
  /**
385
550
  * Get a valid token for the sync engine handshake.
@@ -390,6 +555,23 @@ var AuthClient = class {
390
555
  async getSyncToken() {
391
556
  return this.getAccessToken();
392
557
  }
558
+ /**
559
+ * Retry immediately: clears the refresh backoff and attempts a refresh if the
560
+ * session is offline. Call it when connectivity is known to be back (for
561
+ * example when a sync transport opens). Also wired to the browser `online`
562
+ * and `visibilitychange` events automatically.
563
+ */
564
+ async retryNow() {
565
+ this.resetBackoff();
566
+ if (this._state === "authenticated" && this.sessionStatus !== "fresh") {
567
+ await this.getAccessToken();
568
+ }
569
+ }
570
+ /** Remove environment listeners and timers (for tests and teardown). */
571
+ destroy() {
572
+ this.detachEnvironment();
573
+ this.clearRetryTimer();
574
+ }
393
575
  // -----------------------------------------------------------------------
394
576
  // State change subscriptions
395
577
  // -----------------------------------------------------------------------
@@ -416,6 +598,20 @@ var AuthClient = class {
416
598
  this.listeners.delete(callback);
417
599
  };
418
600
  }
601
+ /**
602
+ * Subscribe to session freshness changes (fresh, authenticated-offline,
603
+ * locked). Fires in addition to {@link onAuthChange}, including when the
604
+ * state stays 'authenticated' but connectivity to the auth server changes.
605
+ *
606
+ * @param callback - Called with the current session (null when signed out)
607
+ * @returns An unsubscribe function
608
+ */
609
+ onSessionChange(callback) {
610
+ this.sessionListeners.add(callback);
611
+ return () => {
612
+ this.sessionListeners.delete(callback);
613
+ };
614
+ }
419
615
  // -----------------------------------------------------------------------
420
616
  // Internal helpers
421
617
  // -----------------------------------------------------------------------
@@ -433,30 +629,115 @@ var AuthClient = class {
433
629
  } catch {
434
630
  }
435
631
  }
632
+ this.notifySession();
436
633
  }
437
634
  }
635
+ setSessionStatus(status) {
636
+ if (this.sessionStatus === status) return;
637
+ this.sessionStatus = status;
638
+ this.notifySession();
639
+ }
640
+ notifySession() {
641
+ const session = this.session;
642
+ for (const listener of this.sessionListeners) {
643
+ try {
644
+ listener(session);
645
+ } catch {
646
+ }
647
+ }
648
+ }
649
+ async completeSignIn(response) {
650
+ if ("mfaRequired" in response) {
651
+ throw new MfaRequiredError(response.mfaToken);
652
+ }
653
+ const tokens = "tokens" in response ? response.tokens : response;
654
+ await this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
655
+ this.markFresh(tokens.refreshToken);
656
+ const user = "user" in response && response.user ? normalizeAuthUser(response.user) : await this.fetchUserProfile(tokens.accessToken);
657
+ this.setState("authenticated", user);
658
+ return user;
659
+ }
438
660
  /**
439
661
  * Restore a session from a valid access token by fetching the user profile.
440
- * Falls back to extracting the user ID from the token payload if the
441
- * /auth/me request fails (offline scenario).
662
+ * A definitive 401 from `/auth/me` ends the session (NEW-AUTH-4); any other
663
+ * failure restores it as authenticated-offline from the stored identity.
442
664
  */
443
665
  async restoreSession(accessToken) {
666
+ let response;
444
667
  try {
445
- const user = await this.fetchUserProfile(accessToken);
446
- this.setState("authenticated", user);
668
+ response = await this.rawRequest("/auth/me", { method: "GET", token: accessToken });
447
669
  } catch {
448
- const userId = getUserIdFromToken(accessToken);
449
- if (userId) {
450
- this.setState("authenticated", {
451
- id: userId,
452
- email: "",
453
- name: null
454
- });
455
- } else {
456
- await this.storage.clear();
457
- this.setState("unauthenticated", null);
670
+ await this.enterOfflineSession();
671
+ return;
672
+ }
673
+ if (response.ok && isRecord(response.json)) {
674
+ const profile = response.json.data !== void 0 ? response.json.data : response.json;
675
+ if (isRecord(profile) && typeof profile.id === "string") {
676
+ this.markFresh(null);
677
+ this.setState("authenticated", normalizeAuthUser(profile));
678
+ return;
458
679
  }
459
680
  }
681
+ if (response.status === 401 && isDefinitiveRejection(response)) {
682
+ await this.endSession();
683
+ return;
684
+ }
685
+ await this.enterOfflineSession();
686
+ }
687
+ /**
688
+ * Restore the session from stored credentials while the auth server is
689
+ * unreachable. Signs out only when the stored refresh token is unusable.
690
+ */
691
+ async enterOfflineSession() {
692
+ const refreshToken = await this.storage.getRefreshToken();
693
+ const identity = await this.getStoredIdentity();
694
+ if (!refreshToken || !identity || isTokenExpired(refreshToken, 0)) {
695
+ await this.endSession();
696
+ return;
697
+ }
698
+ this.cachedDeviceId = identity.deviceId;
699
+ this.noteIssuedCredential(refreshToken);
700
+ const user = this._user && this._user.id === identity.userId ? this._user : { id: identity.userId, email: "", name: null };
701
+ this.sessionStatus = this.offlineStatus();
702
+ this.setState("authenticated", user);
703
+ this.notifySession();
704
+ }
705
+ /** Offline, or locked when the grace period ran out or the clock went backwards. */
706
+ offlineStatus() {
707
+ const now = Date.now();
708
+ if (this.lastServerContactAt > 0 && now + CLOCK_ROLLBACK_TOLERANCE_MS < this.lastServerContactAt) {
709
+ return "locked";
710
+ }
711
+ if (this.maxOfflineGraceMs !== void 0 && this.lastServerContactAt > 0 && now - this.lastServerContactAt > this.maxOfflineGraceMs) {
712
+ return "locked";
713
+ }
714
+ return "offline";
715
+ }
716
+ async endSession() {
717
+ await this.storage.clear();
718
+ this.resetBackoff();
719
+ this.sessionStatus = "fresh";
720
+ this.cachedDeviceId = null;
721
+ this.setState("unauthenticated", null);
722
+ }
723
+ /** The refresh token's issue time is the server's clock at the last rotation. */
724
+ noteIssuedCredential(refreshToken) {
725
+ if (!refreshToken) return;
726
+ const issued = tokenIssuedAtMs(refreshToken);
727
+ if (issued !== null) this.lastServerContactAt = Math.max(this.lastServerContactAt, issued);
728
+ const claims = decodeJwtPayload(refreshToken);
729
+ if (claims && typeof claims.dev === "string") this.cachedDeviceId = claims.dev;
730
+ }
731
+ markFresh(refreshToken) {
732
+ this.resetBackoff();
733
+ this.lastServerContactAt = Math.max(this.lastServerContactAt, Date.now());
734
+ this.noteIssuedCredential(refreshToken);
735
+ this.setSessionStatus("fresh");
736
+ }
737
+ markOffline() {
738
+ if (this._state === "authenticated") {
739
+ this.setSessionStatus(this.offlineStatus());
740
+ }
460
741
  }
461
742
  /**
462
743
  * Fetch the current user profile from the server.
@@ -470,19 +751,9 @@ var AuthClient = class {
470
751
  }
471
752
  async createOAuthAuthorization(provider, options) {
472
753
  const params = new URLSearchParams();
473
- const withIdentity = await this.withDeviceIdentity({
474
- deviceId: options.deviceId,
475
- devicePublicKey: options.devicePublicKey
476
- });
477
754
  if (options.returnTo) {
478
755
  params.set("returnTo", options.returnTo);
479
756
  }
480
- if (withIdentity.deviceId) {
481
- params.set("deviceId", withIdentity.deviceId);
482
- }
483
- if (withIdentity.devicePublicKey) {
484
- params.set("devicePublicKey", withIdentity.devicePublicKey);
485
- }
486
757
  if (options.metadata) {
487
758
  for (const [key, value] of Object.entries(options.metadata)) {
488
759
  if (value !== void 0 && value !== null) {
@@ -491,12 +762,38 @@ var AuthClient = class {
491
762
  }
492
763
  }
493
764
  const query = params.toString();
494
- return this.request(
765
+ const result = await this.request(
495
766
  `/auth/oauth/${encodeURIComponent(provider)}${query ? `?${query}` : ""}`,
496
767
  {
497
768
  method: "GET"
498
769
  }
499
770
  );
771
+ this.rememberOAuthBinding(result);
772
+ return result;
773
+ }
774
+ /** Bindings of flows this client started, by state (survives the redirect on web). */
775
+ oauthBindings = /* @__PURE__ */ new Map();
776
+ rememberOAuthBinding(result) {
777
+ if (!result.binding || !result.state) return;
778
+ this.oauthBindings.set(result.state, result.binding);
779
+ const session = getSessionStorage();
780
+ try {
781
+ session?.setItem(`${OAUTH_BINDING_PREFIX}${result.state}`, result.binding);
782
+ } catch {
783
+ }
784
+ }
785
+ takeOAuthBinding(state) {
786
+ const inMemory = this.oauthBindings.get(state);
787
+ this.oauthBindings.delete(state);
788
+ const session = getSessionStorage();
789
+ let stored = null;
790
+ try {
791
+ stored = session?.getItem(`${OAUTH_BINDING_PREFIX}${state}`) ?? null;
792
+ session?.removeItem(`${OAUTH_BINDING_PREFIX}${state}`);
793
+ } catch {
794
+ stored = null;
795
+ }
796
+ return inMemory ?? stored ?? void 0;
500
797
  }
501
798
  async withDeviceIdentity(params) {
502
799
  if (!this.deviceIdentity || params.deviceId && params.devicePublicKey) {
@@ -510,54 +807,187 @@ var AuthClient = class {
510
807
  };
511
808
  }
512
809
  /**
513
- * Refresh the access token using a refresh token.
514
- * De-duplicates concurrent refresh calls so only one network request is made.
810
+ * Refresh the session's tokens. De-duplicates concurrent calls in this
811
+ * client, serializes refreshes across tabs, and applies backoff after
812
+ * transient failures.
515
813
  */
516
- async refreshAccessToken(refreshToken) {
814
+ refresh() {
517
815
  if (this._refreshPromise) {
518
816
  return this._refreshPromise;
519
817
  }
520
- this._refreshPromise = this.performRefresh(refreshToken);
521
- try {
522
- const result = await this._refreshPromise;
523
- return result;
524
- } finally {
525
- this._refreshPromise = null;
526
- }
818
+ const promise = this.refreshOnce().finally(() => {
819
+ if (this._refreshPromise === promise) this._refreshPromise = null;
820
+ });
821
+ this._refreshPromise = promise;
822
+ return promise;
527
823
  }
528
- async requireAccessToken() {
529
- const token = await this.getAccessToken();
530
- if (!token) {
531
- throw new AuthError("You must be signed in to perform this action.", "AUTH_REQUIRED");
824
+ async refreshOnce() {
825
+ const refreshToken = await this.storage.getRefreshToken();
826
+ if (!refreshToken) return { kind: "rejected" };
827
+ if (isTokenExpired(refreshToken, 0)) {
828
+ await this.endSession();
829
+ return { kind: "rejected" };
532
830
  }
533
- return token;
831
+ if (Date.now() < this.nextAttemptAt) {
832
+ this.markOffline();
833
+ return { kind: "transient" };
834
+ }
835
+ let outcome;
836
+ try {
837
+ outcome = await this.withRefreshLock(async () => {
838
+ const current = await this.storage.getRefreshToken();
839
+ if (!current) return { kind: "rejected" };
840
+ const currentAccess = await this.storage.getAccessToken();
841
+ if (current !== refreshToken && currentAccess && !isTokenExpired(currentAccess)) {
842
+ return { kind: "ok", accessToken: currentAccess };
843
+ }
844
+ return this.performRefresh(current);
845
+ });
846
+ } catch {
847
+ outcome = { kind: "transient" };
848
+ }
849
+ if (outcome.kind === "ok") {
850
+ this.markFresh(await this.storage.getRefreshToken());
851
+ } else if (outcome.kind === "rejected") {
852
+ this.resetBackoff();
853
+ this.cachedDeviceId = null;
854
+ this.sessionStatus = "fresh";
855
+ this.setState("unauthenticated", null);
856
+ } else {
857
+ this.registerFailure(outcome.retryAfterMs);
858
+ this.markOffline();
859
+ }
860
+ return outcome;
534
861
  }
535
862
  /**
536
- * Execute the token refresh network request.
863
+ * Execute the token refresh network request and classify the answer.
537
864
  */
538
865
  async performRefresh(refreshToken) {
866
+ let response;
539
867
  try {
540
- const response = await this.request("/auth/refresh", {
868
+ response = await this.rawRequest("/auth/refresh", {
541
869
  method: "POST",
542
870
  body: { refreshToken }
543
871
  });
544
- await this.storage.setTokens(response.accessToken, response.refreshToken);
545
- return response.accessToken;
546
872
  } catch {
547
- await this.storage.clear();
548
- this.setState("unauthenticated", null);
549
- return null;
873
+ return { kind: "transient" };
874
+ }
875
+ if (response.ok) {
876
+ const tokens = readTokenPair(response.json);
877
+ if (!tokens) {
878
+ return { kind: "transient" };
879
+ }
880
+ await this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
881
+ return { kind: "ok", accessToken: tokens.accessToken };
882
+ }
883
+ if (isDefinitiveRejection(response)) {
884
+ if (await this.storage.getRefreshToken() === refreshToken) {
885
+ await this.storage.clear();
886
+ return { kind: "rejected" };
887
+ }
888
+ const adopted = await this.storage.getAccessToken();
889
+ return adopted && !isTokenExpired(adopted) ? { kind: "ok", accessToken: adopted } : { kind: "transient" };
550
890
  }
891
+ return { kind: "transient", retryAfterMs: response.retryAfterMs };
892
+ }
893
+ async withRefreshLock(fn) {
894
+ const locks = getWebLocks();
895
+ if (locks) {
896
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
897
+ const timer = controller ? setTimeout(() => controller.abort(), this.requestTimeoutMs * 2) : null;
898
+ try {
899
+ return await locks.request(
900
+ this.lockName,
901
+ controller ? { signal: controller.signal } : {},
902
+ async () => {
903
+ if (timer) clearTimeout(timer);
904
+ return fn();
905
+ }
906
+ );
907
+ } finally {
908
+ if (timer) clearTimeout(timer);
909
+ }
910
+ }
911
+ const key = this.storage;
912
+ const previous = inProcessLocks.get(key) ?? Promise.resolve();
913
+ const run = previous.then(fn, fn);
914
+ inProcessLocks.set(
915
+ key,
916
+ run.then(
917
+ () => void 0,
918
+ () => void 0
919
+ )
920
+ );
921
+ return run;
551
922
  }
552
923
  /**
553
- * Make an HTTP request to the auth server.
554
- *
555
- * @param path - URL path relative to serverUrl (e.g. '/auth/signin')
556
- * @param options - Request options
557
- * @returns Parsed JSON response body
558
- * @throws {AuthError} On network failure or non-2xx response
924
+ * Schedule the next allowed refresh attempt. The first retry after a failure
925
+ * is immediate (it recovers a rotation response lost on the wire); after
926
+ * that, exponential backoff with equal jitter, never earlier than Retry-After.
559
927
  */
560
- async request(path, options) {
928
+ registerFailure(retryAfterMs) {
929
+ this.failureCount++;
930
+ let delay = 0;
931
+ if (this.failureCount > 1) {
932
+ const ceiling = Math.min(this.backoffBaseMs * 2 ** (this.failureCount - 2), this.backoffMaxMs);
933
+ delay = ceiling / 2 + Math.random() * (ceiling / 2);
934
+ }
935
+ if (retryAfterMs !== void 0) delay = Math.max(delay, retryAfterMs);
936
+ this.nextAttemptAt = Date.now() + delay;
937
+ this.scheduleRetry(Math.max(delay, this.backoffBaseMs));
938
+ }
939
+ resetBackoff() {
940
+ this.failureCount = 0;
941
+ this.nextAttemptAt = 0;
942
+ this.clearRetryTimer();
943
+ }
944
+ /** Background retry so an offline session recovers without the app calling in. */
945
+ scheduleRetry(delayMs) {
946
+ this.clearRetryTimer();
947
+ if (typeof setTimeout !== "function") return;
948
+ const timer = setTimeout(() => {
949
+ this.retryTimer = null;
950
+ if (this._state === "authenticated" && this.sessionStatus !== "fresh") {
951
+ void this.getAccessToken().catch(() => void 0);
952
+ }
953
+ }, delayMs);
954
+ timer.unref?.();
955
+ this.retryTimer = timer;
956
+ }
957
+ clearRetryTimer() {
958
+ if (this.retryTimer !== null) {
959
+ clearTimeout(this.retryTimer);
960
+ this.retryTimer = null;
961
+ }
962
+ }
963
+ attachEnvironmentListeners() {
964
+ const target = globalThis.window;
965
+ if (!target || typeof target.addEventListener !== "function") return () => {
966
+ };
967
+ const wake = () => {
968
+ const doc = globalThis.document;
969
+ if (doc?.visibilityState === "hidden") return;
970
+ void this.retryNow().catch(() => void 0);
971
+ };
972
+ target.addEventListener("online", wake);
973
+ target.addEventListener("visibilitychange", wake);
974
+ return () => {
975
+ target.removeEventListener?.("online", wake);
976
+ target.removeEventListener?.("visibilitychange", wake);
977
+ };
978
+ }
979
+ async requireAccessToken() {
980
+ const token = await this.getAccessToken();
981
+ if (!token) {
982
+ throw new AuthError("You must be signed in to perform this action.", "AUTH_REQUIRED");
983
+ }
984
+ return token;
985
+ }
986
+ /**
987
+ * Send one request with a timeout and return status plus parsed JSON (if any).
988
+ * Throws only for transport failures (network, abort, timeout).
989
+ */
990
+ async rawRequest(path, options) {
561
991
  const url = `${this.serverUrl}${path}`;
562
992
  const headers = {};
563
993
  if (options.body) {
@@ -566,14 +996,61 @@ var AuthClient = class {
566
996
  if (options.token) {
567
997
  headers.Authorization = `Bearer ${options.token}`;
568
998
  }
999
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
1000
+ let timer = null;
1001
+ const timeout = new Promise((_, reject) => {
1002
+ timer = setTimeout(() => {
1003
+ controller?.abort();
1004
+ reject(
1005
+ new AuthError(
1006
+ `Request to ${path} timed out after ${this.requestTimeoutMs}ms.`,
1007
+ "AUTH_TIMEOUT",
1008
+ { path }
1009
+ )
1010
+ );
1011
+ }, this.requestTimeoutMs);
1012
+ });
1013
+ try {
1014
+ const response = await Promise.race([
1015
+ this.fetchFn(url, {
1016
+ method: options.method,
1017
+ headers,
1018
+ body: options.body ? JSON.stringify(options.body) : void 0,
1019
+ ...controller ? { signal: controller.signal } : {}
1020
+ }),
1021
+ timeout
1022
+ ]);
1023
+ let json;
1024
+ try {
1025
+ json = await Promise.race([response.json(), timeout]);
1026
+ } catch {
1027
+ json = void 0;
1028
+ }
1029
+ const headerGetter = response.headers;
1030
+ return {
1031
+ status: response.status,
1032
+ ok: response.ok,
1033
+ json,
1034
+ retryAfterMs: parseRetryAfter(headerGetter?.get?.("Retry-After"))
1035
+ };
1036
+ } finally {
1037
+ if (timer !== null) clearTimeout(timer);
1038
+ }
1039
+ }
1040
+ /**
1041
+ * Make an HTTP request to the auth server.
1042
+ *
1043
+ * @param path - URL path relative to serverUrl (e.g. '/auth/signin')
1044
+ * @param options - Request options
1045
+ * @returns Parsed JSON response body
1046
+ * @throws {AuthError} On network failure, timeout, or non-2xx response
1047
+ */
1048
+ async request(path, options) {
569
1049
  let response;
570
1050
  try {
571
- response = await this.fetchFn(url, {
572
- method: options.method,
573
- headers,
574
- body: options.body ? JSON.stringify(options.body) : void 0
575
- });
1051
+ response = await this.rawRequest(path, options);
576
1052
  } catch (cause) {
1053
+ if (cause instanceof AuthError) throw cause;
577
1054
  throw new AuthError(
578
1055
  `Network request to ${path} failed. The auth server at ${this.serverUrl} may be unreachable. Check your network connection and serverUrl configuration.`,
579
1056
  "AUTH_NETWORK_ERROR",
@@ -583,26 +1060,33 @@ var AuthClient = class {
583
1060
  if (!response.ok) {
584
1061
  let errorMessage = `Auth server returned HTTP ${response.status}`;
585
1062
  let serverError;
586
- try {
587
- const body = await response.json();
588
- if (typeof body.error === "string") {
589
- errorMessage = body.error;
1063
+ let serverCode;
1064
+ if (isRecord(response.json)) {
1065
+ if (typeof response.json.error === "string") {
1066
+ errorMessage = response.json.error;
590
1067
  serverError = errorMessage;
591
- } else if (typeof body.message === "string") {
592
- errorMessage = body.message;
1068
+ } else if (typeof response.json.message === "string") {
1069
+ errorMessage = response.json.message;
593
1070
  serverError = errorMessage;
594
1071
  }
595
- } catch {
1072
+ if (typeof response.json.code === "string") serverCode = response.json.code;
596
1073
  }
597
1074
  throw new AuthError(errorMessage, "AUTH_SERVER_ERROR", {
598
1075
  path,
599
1076
  status: response.status,
600
- serverError
1077
+ serverError,
1078
+ serverCode
601
1079
  });
602
1080
  }
603
- const json = await response.json();
604
- const data = json.data !== void 0 ? json.data : json;
605
- return data;
1081
+ if (!isRecord(response.json) && !Array.isArray(response.json)) {
1082
+ throw new AuthError(
1083
+ `Auth server returned a non-JSON response for ${path}. A captive portal or proxy may be intercepting requests.`,
1084
+ "AUTH_INVALID_RESPONSE",
1085
+ { path, status: response.status }
1086
+ );
1087
+ }
1088
+ const json = response.json;
1089
+ return json.data !== void 0 ? json.data : json;
606
1090
  }
607
1091
  };
608
1092
 
@@ -940,7 +1424,10 @@ function createKoraAuth(options) {
940
1424
  storageKey: options.storageKey,
941
1425
  storage,
942
1426
  fetch: options.fetch,
943
- deviceIdentity
1427
+ deviceIdentity,
1428
+ requestTimeoutMs: options.requestTimeoutMs,
1429
+ maxOfflineGraceMs: options.maxOfflineGraceMs,
1430
+ refreshBackoff: options.refreshBackoff
944
1431
  });
945
1432
  }
946
1433
  function tryCreateDefaultTokenStorage(storageKey) {
@@ -1002,38 +1489,49 @@ function decodeJwtPayload2(token) {
1002
1489
  return null;
1003
1490
  }
1004
1491
  }
1005
- function readDeviceIdFromClaims(claims) {
1006
- const dev = claims.dev;
1007
- return typeof dev === "string" && dev.length > 0 ? dev : void 0;
1492
+ function readString(claims, key) {
1493
+ const value = claims?.[key];
1494
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1008
1495
  }
1009
1496
  function createKoraAuthSync(options) {
1010
1497
  const { authClient, schema, scopeFromClaims, anonymous = "suspend" } = options;
1498
+ const resolveClaims = async () => {
1499
+ const token = await authClient.getAccessToken();
1500
+ if (token) return { claims: decodeJwtPayload2(token), token };
1501
+ if (authClient.state === "unauthenticated" || !authClient.getStoredClaims) {
1502
+ return { claims: null, token: null };
1503
+ }
1504
+ return { claims: await authClient.getStoredClaims(), token: null };
1505
+ };
1011
1506
  const binding = {
1012
- auth: async () => {
1013
- const token = await authClient.getAccessToken();
1507
+ auth: async (request) => {
1508
+ const token = request?.forceRefresh && authClient.refreshAccessToken ? await authClient.refreshAccessToken() : await authClient.getAccessToken();
1014
1509
  return { token: token ?? "" };
1015
1510
  }
1016
1511
  };
1017
1512
  binding.resolveSyncState = async () => {
1018
1513
  if (authClient.state === "loading") return { state: "loading" };
1019
- const token = await authClient.getAccessToken();
1020
- if (!token) {
1021
- return anonymous === "allow" ? { state: "anonymous", mayConnectAnonymously: true } : { state: "signed-out", mayConnectAnonymously: false };
1022
- }
1023
- const claims = decodeJwtPayload2(token);
1024
- const userId = typeof claims?.sub === "string" ? claims.sub : "";
1514
+ const { claims, token } = await resolveClaims();
1515
+ const userId = readString(claims, "sub");
1025
1516
  if (!userId) {
1026
1517
  return anonymous === "allow" ? { state: "anonymous", mayConnectAnonymously: true } : { state: "signed-out", mayConnectAnonymously: false };
1027
1518
  }
1028
- return { state: "authenticated", userId, token };
1519
+ const deviceId = readString(claims, "dev");
1520
+ if (token) {
1521
+ return { state: "authenticated", userId, token, ...deviceId ? { deviceId } : {} };
1522
+ }
1523
+ return {
1524
+ state: "authenticated",
1525
+ userId,
1526
+ token: null,
1527
+ offline: true,
1528
+ locked: authClient.session?.status === "locked",
1529
+ ...deviceId ? { deviceId } : {}
1530
+ };
1029
1531
  };
1030
1532
  if (schema) {
1031
1533
  binding.resolveScopeMap = async () => {
1032
- const token = await authClient.getAccessToken();
1033
- if (!token) {
1034
- return void 0;
1035
- }
1036
- const claims = decodeJwtPayload2(token);
1534
+ const { claims } = await resolveClaims();
1037
1535
  if (!claims) {
1038
1536
  return void 0;
1039
1537
  }
@@ -1042,30 +1540,24 @@ function createKoraAuthSync(options) {
1042
1540
  };
1043
1541
  }
1044
1542
  binding.resolveNodeId = async () => {
1045
- const token = await authClient.getAccessToken();
1046
- if (!token) {
1047
- return void 0;
1048
- }
1049
- const claims = decodeJwtPayload2(token);
1050
- if (!claims) {
1051
- return void 0;
1052
- }
1053
- return readDeviceIdFromClaims(claims);
1543
+ const { claims } = await resolveClaims();
1544
+ return readString(claims, "dev");
1054
1545
  };
1055
1546
  binding.resolveUserId = async () => {
1056
- const token = await authClient.getAccessToken();
1057
- if (!token) {
1058
- return void 0;
1059
- }
1060
- const claims = decodeJwtPayload2(token);
1061
- if (!claims || typeof claims.sub !== "string" || claims.sub.length === 0) {
1062
- return void 0;
1063
- }
1064
- return claims.sub;
1547
+ const { claims } = await resolveClaims();
1548
+ return readString(claims, "sub");
1065
1549
  };
1066
- if (authClient.onAuthChange) {
1067
- binding.subscribe = (listener) => authClient.onAuthChange?.(() => listener()) ?? (() => {
1068
- });
1550
+ if (authClient.onAuthChange || authClient.onSessionChange) {
1551
+ binding.subscribe = (listener) => {
1552
+ const offAuth = authClient.onAuthChange?.(() => listener()) ?? (() => {
1553
+ });
1554
+ const offSession = authClient.onSessionChange?.(() => listener()) ?? (() => {
1555
+ });
1556
+ return () => {
1557
+ offAuth();
1558
+ offSession();
1559
+ };
1560
+ };
1069
1561
  }
1070
1562
  return binding;
1071
1563
  }
@@ -1248,10 +1740,11 @@ var OrgClient = class {
1248
1740
  await this.request(`/orgs/${orgId}/invitations/${invitationId}`, { method: "DELETE" });
1249
1741
  }
1250
1742
  /**
1251
- * List pending invitations for the current user's email.
1743
+ * List pending invitations addressed to the signed-in user's verified email.
1744
+ * The server resolves the email from the session; tokens are never returned.
1252
1745
  */
1253
- async listMyInvitations(email) {
1254
- return this.request(`/invitations?email=${encodeURIComponent(email)}`, {
1746
+ async listMyInvitations() {
1747
+ return this.request("/invitations", {
1255
1748
  method: "GET"
1256
1749
  });
1257
1750
  }
@@ -1843,6 +2336,7 @@ export {
1843
2336
  InMemoryDeviceKeyStore,
1844
2337
  IndexedDBDeviceKeyStore,
1845
2338
  KeyDerivationError,
2339
+ MfaRequiredError,
1846
2340
  OperationEncryptionError,
1847
2341
  OperationEncryptor,
1848
2342
  OrgClient,