@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/README.md CHANGED
@@ -6,25 +6,29 @@ Offline-first authentication for Kora.js applications.
6
6
 
7
7
  `@korajs/auth` provides a complete authentication system designed for offline-first applications. It includes:
8
8
 
9
- - **Client-side auth management** -- token storage, session restoration, sign-up/sign-in/sign-out
10
- - **React hooks** -- `useAuth()`, `useCurrentUser()`, `useAuthStatus()`, `useOrg()`, `usePermission()`
11
- - **Server-side auth routes** -- email/password authentication with JWT tokens
12
- - **Device identity** -- ECDSA P-256 key pairs for proof-of-possession
13
- - **Token management** -- access/refresh token lifecycle with rotation and revocation detection
14
- - **Session management** -- server-side sessions with idle timeout, max limits, and MFA awareness
15
- - **Multi-factor authentication** -- TOTP (authenticator apps) with recovery codes
16
- - **Organizations and RBAC** -- multi-tenant orgs with role hierarchy and permission checks
17
- - **Passkeys (WebAuthn)** -- passwordless authentication with platform authenticators
18
- - **Encrypted token storage** -- AES-256-GCM encryption for sensitive environments
19
- - **End-to-end encryption** -- encrypt operation data before sync with `OperationEncryptor`
20
- - **Sync auth binding** -- `createKoraAuthSync()` wires tokens, JWT scopes, and device node ids to `createApp`
9
+ - **Client-side auth management**: token storage, session restoration, sign-up/sign-in/sign-out
10
+ - **React hooks**: `useAuth()`, `useCurrentUser()`, `useAuthStatus()`, `useOrg()`, `usePermission()`
11
+ - **Server-side auth routes**: email/password authentication with JWT tokens
12
+ - **Device identity**: ECDSA P-256 key pairs for proof-of-possession
13
+ - **Token management**: access/refresh token lifecycle with rotation and revocation detection
14
+ - **Session management**: server-side sessions with idle timeout, max limits, and MFA awareness
15
+ - **Multi-factor authentication**: TOTP (authenticator apps) with recovery codes
16
+ - **Organizations and RBAC**: multi-tenant orgs with role hierarchy and permission checks
17
+ - **Passkeys (WebAuthn)**: passwordless authentication with platform authenticators
18
+ - **Encrypted token storage**: AES-256-GCM encryption for sensitive environments
19
+ - **Local encryption helpers**: AES-256-GCM keys, PBKDF2 key derivation and auto-lock (end-to-end encryption of synced data is `sync.encryption` in `korajs`)
20
+ - **Sync auth binding**: `createKoraAuthSync()` binds sync to the signed-in user: per-user writes, token refresh, suspension while signed out, and scope hints
21
21
 
22
22
  The client APIs work in browser, Tauri desktop WebView, and mobile JavaScript environments. For desktop apps, run auth routes on your remote sync/auth server and point `AuthClient.serverUrl` at that server. Email/password auth, token refresh, sync authorization, MFA, organizations, and RBAC work across web and desktop clients. Passkeys should be feature-detected because WebAuthn support depends on the operating system WebView.
23
23
 
24
24
  For production desktop and mobile apps, pass a custom token storage adapter backed by the platform credential store and attach a stable device identity:
25
25
 
26
+ <!-- docs-check: standalone -->
26
27
  ```typescript
27
- import { createKoraAuth } from '@korajs/auth'
28
+ import { createKoraAuth, type AuthKeyValueStorage, type DeviceKeyStore } from '@korajs/auth'
29
+
30
+ declare const secureStore: AuthKeyValueStorage // Keychain, Keystore, a Tauri secure-storage plugin
31
+ declare const deviceKeyStore: DeviceKeyStore
28
32
 
29
33
  const authClient = createKoraAuth({
30
34
  serverUrl: 'https://acme.example.com',
@@ -38,20 +42,21 @@ const authClient = createKoraAuth({
38
42
  ## Installation
39
43
 
40
44
  ```bash
41
- pnpm add @korajs/auth
45
+ pnpm add @korajs/auth@beta
42
46
  ```
43
47
 
44
48
  ## Quick Start
45
49
 
46
50
  ### Client-side (React)
47
51
 
52
+ <!-- docs-check: file auth-client.tsx -->
48
53
  ```tsx
49
54
  import { createKoraAuth } from '@korajs/auth'
50
55
  import { AuthProvider, useAuth } from '@korajs/auth/react'
51
56
 
52
- const authClient = createKoraAuth({ serverUrl: 'http://localhost:3001' })
57
+ export const authClient = createKoraAuth({ serverUrl: 'http://localhost:3001' })
53
58
 
54
- function App() {
59
+ export function App() {
55
60
  return (
56
61
  <AuthProvider client={authClient}>
57
62
  <MyApp />
@@ -90,37 +95,47 @@ function MyApp() {
90
95
 
91
96
  ```tsx
92
97
  import { createKoraAuthSync } from '@korajs/auth'
93
- import { createApp } from 'korajs'
98
+ import { createApp, defineSchema, t } from 'korajs'
99
+ import { authClient } from './auth-client'
100
+
101
+ const schema = defineSchema({ version: 1, collections: { todos: { fields: { title: t.string() } } } })
94
102
 
95
103
  const app = createApp({
96
104
  schema,
97
105
  sync: {
98
106
  url: 'ws://localhost:3001/kora-sync',
99
107
  authClient: createKoraAuthSync({ authClient, schema }),
108
+ autoConnect: true,
100
109
  },
101
110
  })
102
111
  ```
103
112
 
113
+ Sync waits while nobody is signed in (`anonymous: 'allow'` syncs anonymously instead), and every
114
+ local write belongs to the user who made it.
115
+
104
116
  ### Server-side
105
117
 
118
+ <!-- docs-check: standalone -->
106
119
  ```typescript
107
120
  import {
108
121
  createKoraAuthServer,
109
122
  createSqliteOAuthStores,
123
+ createSqliteUserStore,
110
124
  googleProvider,
111
125
  } from '@korajs/auth/server'
126
+ import { createProductionServer, createSqliteServerStore } from '@korajs/server'
112
127
 
113
- const oauthStores = await createSqliteOAuthStores({
114
- filename: './auth.db',
115
- })
128
+ const userStore = await createSqliteUserStore({ filename: './auth.db' })
129
+ const oauthStores = await createSqliteOAuthStores({ filename: './auth.db' })
116
130
 
117
131
  const auth = createKoraAuthServer({
118
- jwtSecret: process.env.KORA_AUTH_SECRET!,
132
+ jwtSecret: process.env.KORA_AUTH_SECRET, // required in production
133
+ userStore, // production refuses in-memory stores
119
134
  oauth: {
120
135
  providers: [
121
136
  googleProvider({
122
- clientId: process.env.GOOGLE_CLIENT_ID!,
123
- clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
137
+ clientId: process.env.GOOGLE_CLIENT_ID ?? '',
138
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
124
139
  redirectUri: 'https://app.example.com/auth/oauth/google/callback',
125
140
  }),
126
141
  ],
@@ -129,24 +144,12 @@ const auth = createKoraAuthServer({
129
144
  },
130
145
  })
131
146
 
132
- // Wire into your HTTP server:
133
- app.all('/auth/*', async (req, res) => {
134
- const result = await auth.handleRequest({
135
- method: req.method,
136
- path: req.path,
137
- body: req.body,
138
- headers: req.headers,
139
- query: req.query,
140
- ip: req.ip,
141
- })
142
- res.status(result.status).json(result.body)
143
- })
144
-
145
- // Bridge to Kora sync server:
146
- const syncServer = new KoraSyncServer({
147
- store,
148
- auth: auth.auth,
147
+ const server = createProductionServer({
148
+ store: createSqliteServerStore({ filename: './kora-server.db' }),
149
+ httpRoutes: [{ path: '/auth', handle: auth.handleRequest }],
150
+ syncOptions: { auth: auth.auth }, // verified identity and server-granted scopes
149
151
  })
152
+ await server.start()
150
153
  ```
151
154
 
152
155
  ## Exports
@@ -169,8 +172,8 @@ const syncServer = new KoraSyncServer({
169
172
  | `createPasskeyCredential` | Register a new passkey |
170
173
  | `authenticateWithPasskey` | Sign in with a passkey |
171
174
  | `encryptData` / `decryptData` | AES-256-GCM data encryption |
172
- | `OperationEncryptor` | E2E encryption for sync operations |
173
- | `AutoLockManager` | Auto-lock encryption keys after idle timeout |
175
+ | `deriveEncryptionKey` / `generateEncryptionKey` | Local AES-256-GCM keys |
176
+ | `AutoLockManager` | Lock after an idle timeout |
174
177
 
175
178
  ### `@korajs/auth/react`
176
179
 
@@ -191,8 +194,8 @@ const syncServer = new KoraSyncServer({
191
194
  | `createKoraAuthServer` | Quickstart server factory with auth routes and sync provider |
192
195
  | `BuiltInAuthRoutes` | HTTP route handlers for all auth operations |
193
196
  | `TokenManager` | JWT issuing, validation, refresh rotation, revocation |
197
+ | `createSqliteUserStore` / `createPostgresUserStore` | Durable users and token revocations |
194
198
  | `InMemoryUserStore` | Dev/test user store |
195
- | `InMemoryTokenRevocationStore` | Dev/test token revocation store |
196
199
  | `OAuthManager` / provider helpers | OAuth authorization code flow and provider configs |
197
200
  | `InMemoryLinkedIdentityStore` | Dev/test OAuth account-linking store |
198
201
  | `createSqliteOAuthStores` / `createPostgresOAuthStores` | Durable OAuth state and linked identity stores |
@@ -210,13 +213,15 @@ const syncServer = new KoraSyncServer({
210
213
 
211
214
  - Passwords hashed with PBKDF2-SHA512 (600,000 iterations, 32-byte salt)
212
215
  - JWT tokens signed with HMAC-SHA256 with constant-time comparison
213
- - Refresh token rotation with replay detection and device-level revocation
216
+ - Atomic refresh token rotation with reuse detection per token family
214
217
  - Device keys use ECDSA P-256 with non-extractable private keys (Web Crypto)
215
218
  - TOTP uses SHA-1 HMAC per RFC 6238 with 30-second time steps
216
- - Access tokens expire in 15 minutes (configurable), refresh tokens in 7 days
219
+ - Access tokens expire in 15 minutes and refresh tokens in 90 days (configurable)
220
+ - Revocation (sign-out, device, password change, admin) applies to every route and ends live sync sessions
221
+ - Sign-in is rate limited per account and per IP; MFA at sign-in with TOTP (replay protection and lockout)
217
222
  - Session idle timeout with sliding window and configurable max concurrent sessions
218
223
  - Passkeys use WebAuthn L2 with platform authenticator support
219
- - Encrypted token store uses AES-256-GCM with PBKDF2-derived keys
224
+ - The encrypted token store uses AES-256-GCM with a key you provide (generated or PBKDF2-derived)
220
225
 
221
226
  ## Architecture
222
227
 
@@ -241,7 +246,7 @@ Client Server
241
246
 
242
247
  ## Documentation
243
248
 
244
- See the [Authentication Guide](https://ehoneahobed.github.io/kora/guide/authentication) and [Auth API Reference](https://ehoneahobed.github.io/kora/api/auth) for complete documentation.
249
+ See the [Authentication Guide](https://korajs.dev/guide/authentication) and the [Auth API Reference](https://korajs.dev/api/auth).
245
250
 
246
251
  ## License
247
252
 
@@ -218,13 +218,46 @@ declare function createPersistentDeviceIdentity(options: PersistentDeviceIdentit
218
218
  declare class AuthError extends KoraError {
219
219
  constructor(message: string, code: string, context?: Record<string, unknown>);
220
220
  }
221
+ /**
222
+ * Thrown by sign-in when the account requires a second factor (AUTH-10).
223
+ * Complete it with {@link AuthClient.verifyMfa} using {@link mfaToken}.
224
+ */
225
+ declare class MfaRequiredError extends AuthError {
226
+ readonly mfaToken: string;
227
+ constructor(mfaToken: string);
228
+ }
221
229
  /**
222
230
  * Possible authentication states for the client.
223
231
  * - 'loading': Initial state while restoring tokens from storage
224
- * - 'authenticated': User is signed in with a valid session
225
- * - 'unauthenticated': No valid session exists
232
+ * - 'authenticated': A session exists. It may be fresh or offline; see
233
+ * {@link AuthClient.session} for the freshness of its credentials.
234
+ * - 'unauthenticated': No session exists (never signed in, signed out, or the
235
+ * auth server definitively rejected the session)
226
236
  */
227
237
  type AuthState = 'loading' | 'authenticated' | 'unauthenticated';
238
+ /**
239
+ * Freshness of an authenticated session.
240
+ * - 'fresh': the last refresh or profile request reached the auth server
241
+ * - 'offline': authenticated-offline. The identity is known from stored
242
+ * credentials, but no fresh access token can be minted right now (network,
243
+ * timeout, 5xx, captive portal...). Local data stays available; sync waits.
244
+ * - 'locked': offline for longer than `maxOfflineGraceMs`, or the device clock
245
+ * moved backwards. The UI should lock; local data is never wiped.
246
+ */
247
+ type AuthSessionStatus = 'fresh' | 'offline' | 'locked';
248
+ /**
249
+ * The identity of the stored session, independent of token freshness.
250
+ */
251
+ interface AuthClientSession {
252
+ /** User id (`sub` of the stored credentials). */
253
+ userId: string;
254
+ /** Device id (`dev` of the stored credentials), when known. */
255
+ deviceId: string | null;
256
+ /** Freshness of the credentials. */
257
+ status: AuthSessionStatus;
258
+ /** Last successful contact with the auth server (ms since epoch). */
259
+ lastServerContactAt: number;
260
+ }
228
261
  /**
229
262
  * Authenticated user information.
230
263
  */
@@ -247,6 +280,12 @@ interface LinkedOAuthAccount {
247
280
  interface OAuthAuthorizationResult {
248
281
  url: string;
249
282
  state: string;
283
+ /**
284
+ * Client binding for this flow (AUTH-3). The client keeps it (session storage
285
+ * on the web, memory on native) and presents it with the callback; a callback
286
+ * carrying someone else's code and state is then refused.
287
+ */
288
+ binding?: string;
250
289
  }
251
290
  interface OAuthAuthorizationOptions {
252
291
  /**
@@ -268,6 +307,8 @@ interface OAuthAuthorizationOptions {
268
307
  interface OAuthCallbackParams {
269
308
  code: string;
270
309
  state: string;
310
+ /** Flow binding; looked up from the started flow when omitted. */
311
+ binding?: string;
271
312
  deviceId?: string;
272
313
  devicePublicKey?: string;
273
314
  }
@@ -299,6 +340,28 @@ interface AuthClientConfig {
299
340
  * `deviceId` and `devicePublicKey` fields unless the caller provides them.
300
341
  */
301
342
  deviceIdentity?: AuthDeviceIdentityProvider;
343
+ /**
344
+ * Timeout for every auth request, in milliseconds. A request that has not
345
+ * answered by then is aborted and treated as a transient failure.
346
+ * @default 20000
347
+ */
348
+ requestTimeoutMs?: number;
349
+ /**
350
+ * How long a session may stay authenticated-offline (no successful contact
351
+ * with the auth server) before it is `locked`. Locking never wipes local data
352
+ * or tokens; it only tells the UI to ask the user to reconnect.
353
+ * Defaults to no limit beyond the refresh token's own expiry.
354
+ */
355
+ maxOfflineGraceMs?: number;
356
+ /**
357
+ * Backoff between refresh attempts after transient failures. The first retry
358
+ * after a failure is immediate (it recovers a response lost on the wire);
359
+ * later ones back off exponentially with jitter, honouring `Retry-After`.
360
+ */
361
+ refreshBackoff?: {
362
+ baseDelayMs?: number;
363
+ maxDelayMs?: number;
364
+ };
302
365
  }
303
366
  type MaybePromise<T> = T | Promise<T>;
304
367
  interface AuthTokenStorage {
@@ -314,6 +377,15 @@ interface AuthTokenStorage {
314
377
  * token refresh, and auth state change notifications. Framework-agnostic --
315
378
  * works in any JavaScript environment with `fetch` and optionally `localStorage`.
316
379
  *
380
+ * Offline-first session rules (AUTH-13):
381
+ * - Only the auth server ends a session: tokens are cleared only on a 401 (or a
382
+ * 400 `invalid_grant`) carrying a Kora JSON error, on an explicit sign-out, or
383
+ * when the refresh token itself has expired.
384
+ * - Every other failure (no network, timeout, abort, 5xx, 429, 511, HTML from a
385
+ * captive portal) keeps the tokens, keeps the user signed in as
386
+ * authenticated-offline and retries with jittered backoff.
387
+ * - One tab refreshes at a time (Web Locks); the others adopt its result.
388
+ *
317
389
  * @example
318
390
  * ```typescript
319
391
  * const auth = new AuthClient({ serverUrl: 'http://localhost:3001' })
@@ -334,10 +406,22 @@ declare class AuthClient {
334
406
  private readonly fetchFn;
335
407
  private readonly deviceIdentity;
336
408
  private readonly listeners;
409
+ private readonly sessionListeners;
410
+ private readonly requestTimeoutMs;
411
+ private readonly maxOfflineGraceMs;
412
+ private readonly backoffBaseMs;
413
+ private readonly backoffMaxMs;
414
+ private readonly lockName;
337
415
  private _state;
338
416
  private _user;
339
417
  private _refreshPromise;
340
418
  private _initialized;
419
+ private sessionStatus;
420
+ private lastServerContactAt;
421
+ private failureCount;
422
+ private nextAttemptAt;
423
+ private retryTimer;
424
+ private readonly detachEnvironment;
341
425
  /**
342
426
  * Creates a new AuthClient.
343
427
  *
@@ -348,13 +432,37 @@ declare class AuthClient {
348
432
  get state(): AuthState;
349
433
  /** Current authenticated user, or null if not signed in. */
350
434
  get currentUser(): AuthUser | null;
351
- /** Whether the user is currently authenticated. */
435
+ /** Whether the user is currently authenticated (fresh or offline). */
352
436
  get isAuthenticated(): boolean;
437
+ /**
438
+ * The stored session identity and its freshness, or null when signed out.
439
+ * Unlike {@link getAccessToken}, this is available offline: the identity is
440
+ * decoupled from whether a fresh access token can be minted right now.
441
+ */
442
+ get session(): AuthClientSession | null;
443
+ private cachedDeviceId;
444
+ /**
445
+ * Read the stored session identity (user id and device id) from storage,
446
+ * without any network request. Returns null when no usable session is stored.
447
+ */
448
+ getStoredIdentity(): Promise<{
449
+ userId: string;
450
+ deviceId: string | null;
451
+ } | null>;
452
+ /**
453
+ * Decoded (unverified) claims of the stored credentials, preferring the access
454
+ * token even when it has expired. For client-side hints only (local database
455
+ * name, sync node id, handshake scope narrowing); the server re-derives
456
+ * everything it authorizes from a verified token.
457
+ */
458
+ getStoredClaims(): Promise<Record<string, unknown> | null>;
353
459
  /**
354
460
  * Initialize the auth client by restoring a session from stored tokens.
355
461
  *
356
462
  * Loads tokens from storage, validates the access token, and attempts a
357
463
  * refresh if the access token is expired but a refresh token is available.
464
+ * When the auth server cannot be reached, the stored session is restored as
465
+ * authenticated-offline instead of being discarded.
358
466
  * Safe to call multiple times -- subsequent calls are no-ops once initialized.
359
467
  */
360
468
  initialize(): Promise<void>;
@@ -385,6 +493,19 @@ declare class AuthClient {
385
493
  deviceId?: string;
386
494
  devicePublicKey?: string;
387
495
  }): Promise<AuthUser>;
496
+ /**
497
+ * Finish a sign-in that required a second factor.
498
+ *
499
+ * @param mfaToken - From the {@link MfaRequiredError} thrown by sign-in
500
+ * @param proof - A current TOTP code, or a recovery code
501
+ * @returns The authenticated AuthUser
502
+ * @throws {AuthError} If the code or the MFA session is invalid
503
+ */
504
+ verifyMfa(mfaToken: string, proof: {
505
+ code: string;
506
+ } | {
507
+ recoveryCode: string;
508
+ }): Promise<AuthUser>;
388
509
  /**
389
510
  * Create an OAuth authorization URL and optionally redirect the current window.
390
511
  *
@@ -400,7 +521,7 @@ declare class AuthClient {
400
521
  /**
401
522
  * Create an OAuth authorization URL for linking another provider to the current user.
402
523
  */
403
- getOAuthAuthorizationUrl(provider: string, options?: OAuthAuthorizationOptions): Promise<OAuthAuthorizationResult>;
524
+ getOAuthAuthorizationUrl(provider: string, _options?: OAuthAuthorizationOptions): Promise<OAuthAuthorizationResult>;
404
525
  /**
405
526
  * Link an OAuth provider to the current authenticated user.
406
527
  */
@@ -424,10 +545,26 @@ declare class AuthClient {
424
545
  /**
425
546
  * Get a valid access token, automatically refreshing if expired.
426
547
  *
427
- * @returns A valid access token string, or null if the user is not
428
- * authenticated and refresh is not possible
548
+ * Returns null when no fresh token can be obtained right now. That does NOT
549
+ * mean the user is signed out: check {@link state} / {@link session}. A
550
+ * transient failure keeps the session (authenticated-offline) and later calls
551
+ * retry with backoff.
552
+ *
553
+ * @returns A valid access token string, or null if none is available now
429
554
  */
430
555
  getAccessToken(): Promise<string | null>;
556
+ /**
557
+ * Refresh the session now, even when the cached access token has not expired
558
+ * locally. Used after the sync server ended a session with `AUTH_EXPIRED` or
559
+ * `AUTH_REVOKED`: the server's clock or a revocation says the cached token is
560
+ * no longer good. Concurrent calls (and other tabs) share one refresh.
561
+ *
562
+ * A transient failure keeps the session (authenticated-offline) and returns
563
+ * null; only a definitive server rejection signs the user out.
564
+ *
565
+ * @returns The refreshed access token, or null if none could be obtained now
566
+ */
567
+ refreshAccessToken(): Promise<string | null>;
431
568
  /**
432
569
  * Get a valid token for the sync engine handshake.
433
570
  * Alias for {@link getAccessToken}.
@@ -435,6 +572,15 @@ declare class AuthClient {
435
572
  * @returns A valid access token string, or null if unavailable
436
573
  */
437
574
  getSyncToken(): Promise<string | null>;
575
+ /**
576
+ * Retry immediately: clears the refresh backoff and attempts a refresh if the
577
+ * session is offline. Call it when connectivity is known to be back (for
578
+ * example when a sync transport opens). Also wired to the browser `online`
579
+ * and `visibilitychange` events automatically.
580
+ */
581
+ retryNow(): Promise<void>;
582
+ /** Remove environment listeners and timers (for tests and teardown). */
583
+ destroy(): void;
438
584
  /**
439
585
  * Subscribe to authentication state changes.
440
586
  *
@@ -453,39 +599,86 @@ declare class AuthClient {
453
599
  * ```
454
600
  */
455
601
  onAuthChange(callback: (state: AuthState) => void): () => void;
602
+ /**
603
+ * Subscribe to session freshness changes (fresh, authenticated-offline,
604
+ * locked). Fires in addition to {@link onAuthChange}, including when the
605
+ * state stays 'authenticated' but connectivity to the auth server changes.
606
+ *
607
+ * @param callback - Called with the current session (null when signed out)
608
+ * @returns An unsubscribe function
609
+ */
610
+ onSessionChange(callback: (session: AuthClientSession | null) => void): () => void;
456
611
  /**
457
612
  * Update internal state and notify all listeners.
458
613
  */
459
614
  private setState;
615
+ private setSessionStatus;
616
+ private notifySession;
617
+ private completeSignIn;
460
618
  /**
461
619
  * Restore a session from a valid access token by fetching the user profile.
462
- * Falls back to extracting the user ID from the token payload if the
463
- * /auth/me request fails (offline scenario).
620
+ * A definitive 401 from `/auth/me` ends the session (NEW-AUTH-4); any other
621
+ * failure restores it as authenticated-offline from the stored identity.
464
622
  */
465
623
  private restoreSession;
624
+ /**
625
+ * Restore the session from stored credentials while the auth server is
626
+ * unreachable. Signs out only when the stored refresh token is unusable.
627
+ */
628
+ private enterOfflineSession;
629
+ /** Offline, or locked when the grace period ran out or the clock went backwards. */
630
+ private offlineStatus;
631
+ private endSession;
632
+ /** The refresh token's issue time is the server's clock at the last rotation. */
633
+ private noteIssuedCredential;
634
+ private markFresh;
635
+ private markOffline;
466
636
  /**
467
637
  * Fetch the current user profile from the server.
468
638
  */
469
639
  private fetchUserProfile;
470
640
  private createOAuthAuthorization;
641
+ /** Bindings of flows this client started, by state (survives the redirect on web). */
642
+ private readonly oauthBindings;
643
+ private rememberOAuthBinding;
644
+ private takeOAuthBinding;
471
645
  private withDeviceIdentity;
472
646
  /**
473
- * Refresh the access token using a refresh token.
474
- * De-duplicates concurrent refresh calls so only one network request is made.
647
+ * Refresh the session's tokens. De-duplicates concurrent calls in this
648
+ * client, serializes refreshes across tabs, and applies backoff after
649
+ * transient failures.
475
650
  */
476
- private refreshAccessToken;
477
- private requireAccessToken;
651
+ private refresh;
652
+ private refreshOnce;
478
653
  /**
479
- * Execute the token refresh network request.
654
+ * Execute the token refresh network request and classify the answer.
480
655
  */
481
656
  private performRefresh;
657
+ private withRefreshLock;
658
+ /**
659
+ * Schedule the next allowed refresh attempt. The first retry after a failure
660
+ * is immediate (it recovers a rotation response lost on the wire); after
661
+ * that, exponential backoff with equal jitter, never earlier than Retry-After.
662
+ */
663
+ private registerFailure;
664
+ private resetBackoff;
665
+ /** Background retry so an offline session recovers without the app calling in. */
666
+ private scheduleRetry;
667
+ private clearRetryTimer;
668
+ private attachEnvironmentListeners;
669
+ private requireAccessToken;
670
+ /**
671
+ * Send one request with a timeout and return status plus parsed JSON (if any).
672
+ * Throws only for transport failures (network, abort, timeout).
673
+ */
674
+ private rawRequest;
482
675
  /**
483
676
  * Make an HTTP request to the auth server.
484
677
  *
485
678
  * @param path - URL path relative to serverUrl (e.g. '/auth/signin')
486
679
  * @param options - Request options
487
680
  * @returns Parsed JSON response body
488
- * @throws {AuthError} On network failure or non-2xx response
681
+ * @throws {AuthError} On network failure, timeout, or non-2xx response
489
682
  */
490
683
  private request;
491
684
  }
@@ -689,9 +882,10 @@ declare class OrgClient {
689
882
  */
690
883
  revokeInvitation(orgId: string, invitationId: string): Promise<void>;
691
884
  /**
692
- * List pending invitations for the current user's email.
885
+ * List pending invitations addressed to the signed-in user's verified email.
886
+ * The server resolves the email from the session; tokens are never returned.
693
887
  */
694
- listMyInvitations(email: string): Promise<ClientInvitation[]>;
888
+ listMyInvitations(): Promise<Array<Omit<ClientInvitation, 'token'>>>;
695
889
  /**
696
890
  * Subscribe to active org changes.
697
891
  * @returns Unsubscribe function
@@ -719,4 +913,4 @@ declare function checkOrgPermission(currentRole: string | null, requiredRole: st
719
913
  */
720
914
  declare function createOrgSession(client: OrgClient): OrgSession;
721
915
 
722
- export { type AuthClientConfig as A, createDeviceKeyStore as B, type ClientInvitation as C, type DeviceKeyStore as D, createMemoryAuthTokenStorage as E, createOrgSession as F, createPersistentDeviceIdentity as G, createWebStorageAuthTokenStorage as H, InMemoryDeviceKeyStore as I, type LinkedOAuthAccount as L, type OAuthAuthorizationOptions as O, type PersistentDeviceIdentityOptions as P, type AuthTokenStorage as a, type AuthKeyValueStorage as b, type AuthDeviceIdentityProvider as c, AuthClient as d, type AuthState as e, type AuthDeviceIdentity as f, AuthDeviceIdentityError as g, AuthError as h, type AuthSession as i, type AuthSessionSnapshot as j, type AuthTokenStorageOptions as k, type AuthUser as l, type ClientMembership as m, type ClientOrganization as n, DeviceKeyStoreError as o, IndexedDBDeviceKeyStore as p, type OAuthAuthorizationResult as q, type OAuthCallbackParams as r, OrgClient as s, type OrgClientConfig as t, OrgClientError as u, type OrgSession as v, type OrgSnapshot as w, checkOrgPermission as x, createAuthSession as y, createAuthTokenStorage as z };
916
+ export { type AuthClientConfig as A, createAuthSession as B, type ClientInvitation as C, type DeviceKeyStore as D, createAuthTokenStorage as E, createDeviceKeyStore as F, createMemoryAuthTokenStorage as G, createOrgSession as H, InMemoryDeviceKeyStore as I, createPersistentDeviceIdentity as J, createWebStorageAuthTokenStorage as K, type LinkedOAuthAccount as L, MfaRequiredError as M, type OAuthAuthorizationOptions as O, type PersistentDeviceIdentityOptions as P, type AuthTokenStorage as a, type AuthKeyValueStorage as b, type AuthDeviceIdentityProvider as c, AuthClient as d, type AuthState as e, type AuthClientSession as f, type AuthDeviceIdentity as g, AuthDeviceIdentityError as h, AuthError as i, type AuthSession as j, type AuthSessionSnapshot as k, type AuthSessionStatus as l, type AuthTokenStorageOptions as m, type AuthUser as n, type ClientMembership as o, type ClientOrganization as p, DeviceKeyStoreError as q, IndexedDBDeviceKeyStore as r, type OAuthAuthorizationResult as s, type OAuthCallbackParams as t, OrgClient as u, type OrgClientConfig as v, OrgClientError as w, type OrgSession as x, type OrgSnapshot as y, checkOrgPermission as z };