@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.
- package/README.md +52 -47
- package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
- package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
- package/dist/index.cjs +645 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -9
- package/dist/index.d.ts +27 -9
- package/dist/index.js +644 -150
- package/dist/index.js.map +1 -1
- package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
- package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/server.cjs +2880 -1667
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +810 -169
- package/dist/server.d.ts +810 -169
- package/dist/server.js +2848 -1646
- package/dist/server.js.map +1 -1
- package/dist/svelte.cjs +2 -2
- package/dist/svelte.cjs.map +1 -1
- package/dist/svelte.d.cts +2 -2
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +2 -2
- package/dist/svelte.js.map +1 -1
- package/dist/vue.d.cts +1 -1
- package/dist/vue.d.ts +1 -1
- package/package.json +7 -7
- package/src/admin/admin-api.ts +327 -0
- package/src/admin/audit-log.ts +324 -0
- package/src/admin/webhooks.ts +576 -0
- package/src/bindings/create-auth-session.ts +184 -0
- package/src/bindings/create-org-session.ts +130 -0
- package/src/client/auth-client.ts +1592 -0
- package/src/client/auth-sync.ts +213 -0
- package/src/client/device-session.ts +104 -0
- package/src/client/org-client.ts +399 -0
- package/src/client/quickstart.ts +108 -0
- package/src/client/storage.ts +94 -0
- package/src/device/device-identity.ts +330 -0
- package/src/device/device-store.ts +379 -0
- package/src/encryption/auto-lock.ts +170 -0
- package/src/encryption/database-encryption.ts +265 -0
- package/src/encryption/key-derivation.ts +149 -0
- package/src/encryption/operation-encryptor.ts +361 -0
- package/src/index.ts +132 -0
- package/src/mfa/totp.ts +826 -0
- package/src/org/org-routes.ts +758 -0
- package/src/org/org-store.ts +490 -0
- package/src/org/org-types.ts +230 -0
- package/src/passkey/passkey-client.ts +597 -0
- package/src/passkey/passkey-server.ts +779 -0
- package/src/postgres/ensure-schema.ts +65 -0
- package/src/provider/adapter.ts +246 -0
- package/src/provider/built-in/auth-routes.ts +1313 -0
- package/src/provider/built-in/email-verification.ts +303 -0
- package/src/provider/built-in/password-hash.ts +118 -0
- package/src/provider/built-in/password-reset.ts +416 -0
- package/src/provider/built-in/postgres-user-store.ts +365 -0
- package/src/provider/built-in/quickstart-server.ts +760 -0
- package/src/provider/built-in/sqlite-user-store.ts +335 -0
- package/src/provider/built-in/sync-scopes.ts +85 -0
- package/src/provider/built-in/user-store.ts +465 -0
- package/src/provider/external/clerk-adapter.ts +157 -0
- package/src/provider/external/external-jwt-provider.ts +491 -0
- package/src/provider/external/supabase-adapter.ts +163 -0
- package/src/provider/oauth/linked-identity-store.ts +108 -0
- package/src/provider/oauth/oauth-flow.ts +550 -0
- package/src/provider/oauth/oauth-types.ts +184 -0
- package/src/provider/oauth/postgres-oauth-store.ts +296 -0
- package/src/provider/oauth/sqlite-oauth-store.ts +285 -0
- package/src/rbac/rbac-engine.ts +323 -0
- package/src/rbac/rbac-types.ts +210 -0
- package/src/rbac/scope-resolver.ts +140 -0
- package/src/react/AuthProvider.tsx +97 -0
- package/src/react/OrgProvider.tsx +41 -0
- package/src/react/auth-context.ts +26 -0
- package/src/react/hooks.ts +110 -0
- package/src/react/org-hooks.ts +214 -0
- package/src/react.ts +26 -0
- package/src/server.ts +338 -0
- package/src/session/session.ts +401 -0
- package/src/svelte/auth-context.ts +50 -0
- package/src/svelte/org-context.ts +32 -0
- package/src/svelte/org-hooks.ts +201 -0
- package/src/svelte/use-auth.ts +115 -0
- package/src/svelte.ts +25 -0
- package/src/tokens/encrypted-token-store.ts +360 -0
- package/src/tokens/jwt.ts +236 -0
- package/src/tokens/postgres-token-revocation-store.ts +140 -0
- package/src/tokens/sqlite-token-revocation-store.ts +121 -0
- package/src/tokens/token-manager.ts +821 -0
- package/src/tokens/token-store.ts +192 -0
- package/src/types.ts +394 -0
- package/src/vue/auth-context.ts +10 -0
- package/src/vue/auth-provider-types.ts +5 -0
- package/src/vue/auth-provider.ts +76 -0
- package/src/vue/org-hooks.ts +193 -0
- package/src/vue/org-provider.ts +49 -0
- package/src/vue/use-auth.ts +139 -0
- 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
|
|
10
|
-
- **React hooks
|
|
11
|
-
- **Server-side auth routes
|
|
12
|
-
- **Device identity
|
|
13
|
-
- **Token management
|
|
14
|
-
- **Session management
|
|
15
|
-
- **Multi-factor authentication
|
|
16
|
-
- **Organizations and RBAC
|
|
17
|
-
- **Passkeys (WebAuthn)
|
|
18
|
-
- **Encrypted token storage
|
|
19
|
-
- **
|
|
20
|
-
- **Sync auth binding
|
|
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
|
|
114
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
| `
|
|
173
|
-
| `AutoLockManager` |
|
|
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
|
-
-
|
|
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
|
|
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
|
-
-
|
|
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://
|
|
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':
|
|
225
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
428
|
-
*
|
|
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
|
-
*
|
|
463
|
-
*
|
|
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
|
|
474
|
-
*
|
|
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
|
|
477
|
-
private
|
|
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
|
|
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(
|
|
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,
|
|
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 };
|