@korajs/auth 1.0.0-beta.11 → 1.0.0-beta.13
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 +2852 -1675
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +779 -168
- package/dist/server.d.ts +779 -168
- package/dist/server.js +2831 -1665
- 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 +328 -0
- package/src/provider/built-in/quickstart-server.ts +760 -0
- package/src/provider/built-in/sqlite-user-store.ts +322 -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 +272 -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 +334 -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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import {
|
|
3
|
+
type SqliteRevocationDatabase,
|
|
4
|
+
SqliteTokenRevocationStore,
|
|
5
|
+
} from '../../tokens/sqlite-token-revocation-store'
|
|
6
|
+
import type { TokenRevocationStore } from '../../tokens/token-manager'
|
|
7
|
+
import type { AuthDevice, AuthUser, StoredUser, UserStore } from './user-store'
|
|
8
|
+
import { DeviceOwnershipError, DuplicateEmailError } from './user-store'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Row shape returned by better-sqlite3 for the auth_users table.
|
|
12
|
+
*/
|
|
13
|
+
interface UserRow {
|
|
14
|
+
id: string
|
|
15
|
+
email: string
|
|
16
|
+
name: string
|
|
17
|
+
email_verified: number
|
|
18
|
+
created_at: number
|
|
19
|
+
password_hash: string
|
|
20
|
+
salt: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Row shape returned by better-sqlite3 for the auth_devices table.
|
|
25
|
+
*/
|
|
26
|
+
interface DeviceRow {
|
|
27
|
+
id: string
|
|
28
|
+
user_id: string
|
|
29
|
+
public_key: string
|
|
30
|
+
name: string
|
|
31
|
+
revoked: number
|
|
32
|
+
created_at: number
|
|
33
|
+
last_seen_at: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Minimal better-sqlite3 subset to avoid a hard dependency on the package.
|
|
38
|
+
* The real Database instance satisfies this at runtime.
|
|
39
|
+
*/
|
|
40
|
+
interface SqliteDatabase {
|
|
41
|
+
pragma(source: string): unknown
|
|
42
|
+
exec(source: string): void
|
|
43
|
+
prepare(source: string): {
|
|
44
|
+
run(...params: unknown[]): unknown
|
|
45
|
+
get(...params: unknown[]): unknown
|
|
46
|
+
all(...params: unknown[]): unknown[]
|
|
47
|
+
}
|
|
48
|
+
transaction<T>(fn: () => T): () => T
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* SQLite-backed user and device store using better-sqlite3.
|
|
53
|
+
*
|
|
54
|
+
* Provides persistent user storage suitable for single-server deployments,
|
|
55
|
+
* Electron apps, and development environments. Uses WAL mode for concurrent
|
|
56
|
+
* read/write performance.
|
|
57
|
+
*
|
|
58
|
+
* This implementation uses dynamic imports for better-sqlite3 so projects
|
|
59
|
+
* that do not use SQLite server-side do not need to install it.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* import { createSqliteUserStore } from '@korajs/auth/server'
|
|
64
|
+
*
|
|
65
|
+
* const userStore = await createSqliteUserStore({ filename: './auth.db' })
|
|
66
|
+
* const routes = new BuiltInAuthRoutes({ userStore, tokenManager })
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export class SqliteUserStore implements UserStore {
|
|
70
|
+
private readonly db: SqliteDatabase
|
|
71
|
+
private revocationStore: SqliteTokenRevocationStore | null = null
|
|
72
|
+
|
|
73
|
+
constructor(db: SqliteDatabase) {
|
|
74
|
+
this.db = db
|
|
75
|
+
this.db.pragma('journal_mode = WAL')
|
|
76
|
+
this.ensureTables()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Token revocations stored in the same SQLite database as the users. */
|
|
80
|
+
getTokenRevocationStore(): TokenRevocationStore {
|
|
81
|
+
if (!this.revocationStore) {
|
|
82
|
+
this.revocationStore = new SqliteTokenRevocationStore(
|
|
83
|
+
this.db as unknown as SqliteRevocationDatabase,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
return this.revocationStore
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private ensureTables(): void {
|
|
90
|
+
this.db.exec(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS auth_users (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
email TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
94
|
+
name TEXT NOT NULL,
|
|
95
|
+
email_verified INTEGER NOT NULL DEFAULT 0,
|
|
96
|
+
created_at INTEGER NOT NULL,
|
|
97
|
+
password_hash TEXT NOT NULL,
|
|
98
|
+
salt TEXT NOT NULL
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS auth_devices (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
user_id TEXT NOT NULL,
|
|
104
|
+
public_key TEXT NOT NULL,
|
|
105
|
+
name TEXT NOT NULL,
|
|
106
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
created_at INTEGER NOT NULL,
|
|
108
|
+
last_seen_at INTEGER NOT NULL,
|
|
109
|
+
FOREIGN KEY (user_id) REFERENCES auth_users(id) ON DELETE CASCADE
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_auth_devices_user_id ON auth_devices(user_id);
|
|
113
|
+
`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async createUser(params: {
|
|
117
|
+
email: string
|
|
118
|
+
passwordHash: string
|
|
119
|
+
salt: string
|
|
120
|
+
name: string
|
|
121
|
+
}): Promise<AuthUser> {
|
|
122
|
+
const normalizedEmail = params.email.toLowerCase()
|
|
123
|
+
const now = Date.now()
|
|
124
|
+
const id = randomUUID()
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
this.db
|
|
128
|
+
.prepare(`
|
|
129
|
+
INSERT INTO auth_users (id, email, name, email_verified, created_at, password_hash, salt)
|
|
130
|
+
VALUES (?, ?, ?, 0, ?, ?, ?)
|
|
131
|
+
`)
|
|
132
|
+
.run(id, normalizedEmail, params.name, now, params.passwordHash, params.salt)
|
|
133
|
+
} catch (err: unknown) {
|
|
134
|
+
if (err instanceof Error && err.message.includes('UNIQUE constraint failed')) {
|
|
135
|
+
throw new DuplicateEmailError()
|
|
136
|
+
}
|
|
137
|
+
throw err
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { id, email: normalizedEmail, name: params.name, emailVerified: false, createdAt: now }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async findByEmail(email: string): Promise<StoredUser | null> {
|
|
144
|
+
const row = this.db
|
|
145
|
+
.prepare('SELECT * FROM auth_users WHERE email = ?')
|
|
146
|
+
.get(email.toLowerCase()) as UserRow | undefined
|
|
147
|
+
|
|
148
|
+
return row ? rowToStoredUser(row) : null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async findById(id: string): Promise<StoredUser | null> {
|
|
152
|
+
const row = this.db.prepare('SELECT * FROM auth_users WHERE id = ?').get(id) as
|
|
153
|
+
| UserRow
|
|
154
|
+
| undefined
|
|
155
|
+
|
|
156
|
+
return row ? rowToStoredUser(row) : null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async registerDevice(params: {
|
|
160
|
+
id: string
|
|
161
|
+
userId: string
|
|
162
|
+
publicKey: string
|
|
163
|
+
name: string
|
|
164
|
+
}): Promise<AuthDevice> {
|
|
165
|
+
const now = Date.now()
|
|
166
|
+
// INSERT-or-ignore first, then read: the owner check sees whichever
|
|
167
|
+
// registration won, even when two users race for the same id.
|
|
168
|
+
this.db
|
|
169
|
+
.prepare(`
|
|
170
|
+
INSERT INTO auth_devices (id, user_id, public_key, name, revoked, created_at, last_seen_at)
|
|
171
|
+
VALUES (?, ?, ?, ?, 0, ?, ?)
|
|
172
|
+
ON CONFLICT(id) DO NOTHING
|
|
173
|
+
`)
|
|
174
|
+
.run(params.id, params.userId, params.publicKey, params.name, now, now)
|
|
175
|
+
const existing = this.db.prepare('SELECT * FROM auth_devices WHERE id = ?').get(params.id) as
|
|
176
|
+
| DeviceRow
|
|
177
|
+
| undefined
|
|
178
|
+
|
|
179
|
+
if (existing && existing.user_id !== params.userId) {
|
|
180
|
+
throw new DeviceOwnershipError(params.id)
|
|
181
|
+
}
|
|
182
|
+
if (existing && !existing.revoked) {
|
|
183
|
+
return rowToDevice(existing)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (existing) {
|
|
187
|
+
// Re-activate the owner's previously revoked device
|
|
188
|
+
this.db
|
|
189
|
+
.prepare(`
|
|
190
|
+
UPDATE auth_devices SET revoked = 0, public_key = ?, name = ?, last_seen_at = ?
|
|
191
|
+
WHERE id = ? AND user_id = ?
|
|
192
|
+
`)
|
|
193
|
+
.run(params.publicKey, params.name, now, params.id, params.userId)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
id: params.id,
|
|
198
|
+
userId: params.userId,
|
|
199
|
+
publicKey: params.publicKey,
|
|
200
|
+
name: params.name,
|
|
201
|
+
revoked: false,
|
|
202
|
+
createdAt: existing ? existing.created_at : now,
|
|
203
|
+
lastSeenAt: now,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async findDevice(deviceId: string): Promise<AuthDevice | null> {
|
|
208
|
+
const row = this.db.prepare('SELECT * FROM auth_devices WHERE id = ?').get(deviceId) as
|
|
209
|
+
| DeviceRow
|
|
210
|
+
| undefined
|
|
211
|
+
|
|
212
|
+
return row ? rowToDevice(row) : null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async listDevices(userId: string): Promise<AuthDevice[]> {
|
|
216
|
+
const rows = this.db
|
|
217
|
+
.prepare('SELECT * FROM auth_devices WHERE user_id = ?')
|
|
218
|
+
.all(userId) as DeviceRow[]
|
|
219
|
+
|
|
220
|
+
return rows.map(rowToDevice)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async revokeDevice(deviceId: string): Promise<void> {
|
|
224
|
+
this.db.prepare('UPDATE auth_devices SET revoked = 1 WHERE id = ?').run(deviceId)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async setEmailVerified(userId: string, verified: boolean): Promise<void> {
|
|
228
|
+
this.db
|
|
229
|
+
.prepare('UPDATE auth_users SET email_verified = ? WHERE id = ?')
|
|
230
|
+
.run(verified ? 1 : 0, userId)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async updatePassword(userId: string, passwordHash: string, salt: string): Promise<void> {
|
|
234
|
+
this.db
|
|
235
|
+
.prepare('UPDATE auth_users SET password_hash = ?, salt = ? WHERE id = ?')
|
|
236
|
+
.run(passwordHash, salt, userId)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async listAll(): Promise<StoredUser[]> {
|
|
240
|
+
const rows = this.db.prepare('SELECT * FROM auth_users').all() as UserRow[]
|
|
241
|
+
return rows.map(rowToStoredUser)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async update(user: StoredUser): Promise<void> {
|
|
245
|
+
this.db
|
|
246
|
+
.prepare(`
|
|
247
|
+
UPDATE auth_users
|
|
248
|
+
SET email = ?, name = ?, email_verified = ?, password_hash = ?, salt = ?
|
|
249
|
+
WHERE id = ?
|
|
250
|
+
`)
|
|
251
|
+
.run(user.email, user.name, user.emailVerified ? 1 : 0, user.passwordHash, user.salt, user.id)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async delete(userId: string): Promise<void> {
|
|
255
|
+
const deleteInTransaction = this.db.transaction(() => {
|
|
256
|
+
this.db.prepare('DELETE FROM auth_devices WHERE user_id = ?').run(userId)
|
|
257
|
+
this.db.prepare('DELETE FROM auth_users WHERE id = ?').run(userId)
|
|
258
|
+
})
|
|
259
|
+
deleteInTransaction()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async touchDevice(deviceId: string): Promise<void> {
|
|
263
|
+
this.db
|
|
264
|
+
.prepare('UPDATE auth_devices SET last_seen_at = ? WHERE id = ?')
|
|
265
|
+
.run(Date.now(), deviceId)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Creates a SqliteUserStore from a file path.
|
|
271
|
+
*
|
|
272
|
+
* Uses runtime dynamic imports so projects that do not use SQLite server-side
|
|
273
|
+
* do not need to install `better-sqlite3`.
|
|
274
|
+
*
|
|
275
|
+
* @param options.filename - Path to the SQLite database file, or `:memory:` for in-memory
|
|
276
|
+
*/
|
|
277
|
+
export async function createSqliteUserStore(options: {
|
|
278
|
+
filename: string
|
|
279
|
+
}): Promise<SqliteUserStore> {
|
|
280
|
+
const Database = await loadBetterSqlite3()
|
|
281
|
+
const db = new Database(options.filename)
|
|
282
|
+
return new SqliteUserStore(db as unknown as SqliteDatabase)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function loadBetterSqlite3(): Promise<new (filename: string) => unknown> {
|
|
286
|
+
try {
|
|
287
|
+
// Use createRequire for CJS compatibility with better-sqlite3
|
|
288
|
+
const { createRequire } = await import('node:module')
|
|
289
|
+
const require = createRequire(import.meta.url)
|
|
290
|
+
return require('better-sqlite3') as new (
|
|
291
|
+
filename: string,
|
|
292
|
+
) => unknown
|
|
293
|
+
} catch {
|
|
294
|
+
throw new Error(
|
|
295
|
+
'SQLite backend requires the "better-sqlite3" package. Install it in your project dependencies.',
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function rowToStoredUser(row: UserRow): StoredUser {
|
|
301
|
+
return {
|
|
302
|
+
id: row.id,
|
|
303
|
+
email: row.email,
|
|
304
|
+
name: row.name,
|
|
305
|
+
emailVerified: Boolean(row.email_verified),
|
|
306
|
+
createdAt: row.created_at,
|
|
307
|
+
passwordHash: row.password_hash,
|
|
308
|
+
salt: row.salt,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function rowToDevice(row: DeviceRow): AuthDevice {
|
|
313
|
+
return {
|
|
314
|
+
id: row.id,
|
|
315
|
+
userId: row.user_id,
|
|
316
|
+
publicKey: row.public_key,
|
|
317
|
+
name: row.name,
|
|
318
|
+
revoked: Boolean(row.revoked),
|
|
319
|
+
createdAt: row.created_at,
|
|
320
|
+
lastSeenAt: row.last_seen_at,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type ScopeMap, claimScopes } from '@korajs/core'
|
|
2
|
+
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Verified identity of a sync session, derived on the server from a validated
|
|
7
|
+
* access token. Never contains client-supplied values.
|
|
8
|
+
*/
|
|
9
|
+
export interface VerifiedSyncClaims {
|
|
10
|
+
/** Verified user id (`sub`). */
|
|
11
|
+
userId: string
|
|
12
|
+
/** Verified device id (`dev`). */
|
|
13
|
+
deviceId: string
|
|
14
|
+
/** The user's email from the user store. */
|
|
15
|
+
email: string
|
|
16
|
+
/** The user's display name from the user store. */
|
|
17
|
+
name: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Server-side scope derivation for the sync auth provider (AUTH-1).
|
|
22
|
+
*
|
|
23
|
+
* The result is the session's complete grant: the client handshake can only
|
|
24
|
+
* narrow it, and a schema-scoped collection whose binding is unresolved is
|
|
25
|
+
* denied (`SCOPE_REQUIRED`), never widened.
|
|
26
|
+
*/
|
|
27
|
+
export interface SyncScopeOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Extra verified scope values merged over the default `{ userId }`, for
|
|
30
|
+
* example `{ orgId: await orgOf(userId) }`. Every schema-scoped collection is
|
|
31
|
+
* bound from these values. Return `undefined`/`null` for a key to deny the
|
|
32
|
+
* collections that need it.
|
|
33
|
+
*/
|
|
34
|
+
scopeValues?: (claims: VerifiedSyncClaims) => MaybePromise<Record<string, unknown>>
|
|
35
|
+
/**
|
|
36
|
+
* Full explicit grant. When provided it replaces the default derivation:
|
|
37
|
+
* collections it omits are not visible. Use `claimScopes(values, explicit)`
|
|
38
|
+
* from `@korajs/core` to combine claim binding with explicit predicates.
|
|
39
|
+
*/
|
|
40
|
+
resolveScopes?: (claims: VerifiedSyncClaims) => MaybePromise<ScopeMap>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Auth context returned to `@korajs/server` by the sync auth provider. */
|
|
44
|
+
export interface SyncAuthContext {
|
|
45
|
+
userId: string
|
|
46
|
+
scopes?: Record<string, Record<string, unknown>>
|
|
47
|
+
metadata?: Record<string, unknown>
|
|
48
|
+
/** Access-token expiry (ms since epoch); a session must not outlive it (AUTH-11). */
|
|
49
|
+
expiresAt?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Structural `AuthProvider` returned by `toSyncAuthProvider()`. */
|
|
53
|
+
export interface SyncAuthProvider {
|
|
54
|
+
authenticate(token: string): Promise<SyncAuthContext | null>
|
|
55
|
+
/**
|
|
56
|
+
* Revocation feed (device revoke, sign-out, password reset or change, admin
|
|
57
|
+
* revoke). `KoraSyncServer` subscribes automatically and terminates the
|
|
58
|
+
* matching live sessions (AUTH-11).
|
|
59
|
+
*
|
|
60
|
+
* @returns An unsubscribe function
|
|
61
|
+
*/
|
|
62
|
+
onRevoke?(
|
|
63
|
+
listener: (event: { userId: string; deviceId?: string }) => void | Promise<void>,
|
|
64
|
+
): () => void
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compute the server grant for a verified session.
|
|
69
|
+
*
|
|
70
|
+
* @param claims - Verified identity
|
|
71
|
+
* @param options - Scope derivation options
|
|
72
|
+
* @returns The scope grant (possibly carrying `$claims` for schema binding)
|
|
73
|
+
*/
|
|
74
|
+
export async function resolveSyncGrant(
|
|
75
|
+
claims: VerifiedSyncClaims,
|
|
76
|
+
options: SyncScopeOptions,
|
|
77
|
+
): Promise<ScopeMap> {
|
|
78
|
+
if (options.resolveScopes) {
|
|
79
|
+
return options.resolveScopes(claims)
|
|
80
|
+
}
|
|
81
|
+
const extra = options.scopeValues ? await options.scopeValues(claims) : {}
|
|
82
|
+
// The verified subject always wins over anything a resolver returns for
|
|
83
|
+
// `userId`, so a buggy resolver cannot rebind a session to another user.
|
|
84
|
+
return claimScopes({ ...extra, userId: claims.userId })
|
|
85
|
+
}
|