@korajs/auth 1.0.0-beta.12 → 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.
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 +2852 -1675
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +779 -168
  17. package/dist/server.d.ts +779 -168
  18. package/dist/server.js +2831 -1665
  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 +328 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +322 -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 +272 -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 +334 -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
@@ -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
+ }