@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
@@ -0,0 +1,365 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { ensurePostgresSchema } from '../../postgres/ensure-schema'
3
+ import { PostgresTokenRevocationStore } from '../../tokens/postgres-token-revocation-store'
4
+ import type { TokenRevocationStore } from '../../tokens/token-manager'
5
+ import type { AuthDevice, AuthUser, StoredUser, UserStore } from './user-store'
6
+ import { DeviceOwnershipError, DuplicateEmailError } from './user-store'
7
+
8
+ /**
9
+ * Minimal typed subset of a postgres-js SQL tag for our queries.
10
+ * Avoids a hard dependency on the postgres package.
11
+ * Uses Record<string, unknown>[] for result type to match postgres-js return shape.
12
+ */
13
+ interface PostgresClient {
14
+ begin<T>(fn: (sql: PostgresClient) => Promise<T>): Promise<T>
15
+ (template: TemplateStringsArray, ...args: unknown[]): Promise<Record<string, unknown>[]>
16
+ /** postgres-js: close every connection of the pool (optional in this subset). */
17
+ end?(options?: { timeout?: number }): Promise<void>
18
+ }
19
+
20
+ /** Options for {@link PostgresUserStore}. */
21
+ export interface PostgresUserStoreOptions {
22
+ /**
23
+ * Whether {@link PostgresUserStore.close} ends the client's connections. True for
24
+ * a store made by `createPostgresUserStore` (it opened the client); false by
25
+ * default for a client you pass in, which you end yourself.
26
+ */
27
+ ownsClient?: boolean
28
+ }
29
+
30
+ /**
31
+ * PostgreSQL-backed user and device store using postgres-js.
32
+ *
33
+ * Provides persistent user storage suitable for production multi-server
34
+ * deployments. Uses parameterized queries for SQL injection safety and
35
+ * transactions for atomic operations.
36
+ *
37
+ * This implementation uses dynamic imports for postgres so projects
38
+ * that do not use PostgreSQL do not need to install it.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * import { createPostgresUserStore } from '@korajs/auth/server'
43
+ *
44
+ * const userStore = await createPostgresUserStore({
45
+ * connectionString: 'postgres://user:pass@localhost:5432/mydb',
46
+ * })
47
+ * const routes = new BuiltInAuthRoutes({ userStore, tokenManager })
48
+ * // On shutdown (or at the end of a script):
49
+ * await userStore.close()
50
+ * ```
51
+ */
52
+ export class PostgresUserStore implements UserStore {
53
+ private readonly sql: PostgresClient
54
+ private readonly ready: Promise<void>
55
+
56
+ private revocationStore: PostgresTokenRevocationStore | null = null
57
+
58
+ private readonly ownsClient: boolean
59
+ private closed = false
60
+
61
+ constructor(sql: PostgresClient, options: PostgresUserStoreOptions = {}) {
62
+ this.sql = sql
63
+ this.ownsClient = options.ownsClient ?? false
64
+ this.ready = this.ensureTables()
65
+ }
66
+
67
+ /**
68
+ * Release the store's database connections, so a script using it can exit. Waits
69
+ * for table creation to settle first. A client passed to the constructor is left
70
+ * open unless `ownsClient` was set (end it yourself). Idempotent.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const users = await createPostgresUserStore({ connectionString })
75
+ * const device = await users.findDevice(nodeId)
76
+ * await users.close()
77
+ * ```
78
+ */
79
+ async close(): Promise<void> {
80
+ if (this.closed) return
81
+ this.closed = true
82
+ await this.ready.catch(() => {})
83
+ if (this.ownsClient) await this.sql.end?.({ timeout: 5 })
84
+ }
85
+
86
+ /** Token revocations stored in the same Postgres database as the users. */
87
+ getTokenRevocationStore(): TokenRevocationStore {
88
+ if (!this.revocationStore) {
89
+ this.revocationStore = new PostgresTokenRevocationStore(this.sql)
90
+ }
91
+ return this.revocationStore
92
+ }
93
+
94
+ private async ensureTables(): Promise<void> {
95
+ // Concurrency-safe on an empty database shared by several instances.
96
+ await ensurePostgresSchema(this.sql, async (sql) => {
97
+ await sql`
98
+ CREATE TABLE IF NOT EXISTS auth_users (
99
+ id TEXT PRIMARY KEY,
100
+ email TEXT NOT NULL UNIQUE,
101
+ name TEXT NOT NULL,
102
+ email_verified BOOLEAN NOT NULL DEFAULT FALSE,
103
+ created_at BIGINT NOT NULL,
104
+ password_hash TEXT NOT NULL,
105
+ salt TEXT NOT NULL
106
+ )
107
+ `
108
+
109
+ await sql`
110
+ CREATE TABLE IF NOT EXISTS auth_devices (
111
+ id TEXT PRIMARY KEY,
112
+ user_id TEXT NOT NULL REFERENCES auth_users(id) ON DELETE CASCADE,
113
+ public_key TEXT NOT NULL,
114
+ name TEXT NOT NULL,
115
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
116
+ created_at BIGINT NOT NULL,
117
+ last_seen_at BIGINT NOT NULL
118
+ )
119
+ `
120
+
121
+ await sql`
122
+ CREATE INDEX IF NOT EXISTS idx_auth_devices_user_id ON auth_devices(user_id)
123
+ `
124
+
125
+ // Case-insensitive unique index on email for consistent lookups
126
+ await sql`
127
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_auth_users_email_lower ON auth_users(LOWER(email))
128
+ `
129
+ })
130
+ }
131
+
132
+ async createUser(params: {
133
+ email: string
134
+ passwordHash: string
135
+ salt: string
136
+ name: string
137
+ }): Promise<AuthUser> {
138
+ await this.ready
139
+ const normalizedEmail = params.email.toLowerCase()
140
+ const now = Date.now()
141
+ const id = randomUUID()
142
+
143
+ try {
144
+ await this.sql`
145
+ INSERT INTO auth_users (id, email, name, email_verified, created_at, password_hash, salt)
146
+ VALUES (${id}, ${normalizedEmail}, ${params.name}, FALSE, ${now}, ${params.passwordHash}, ${params.salt})
147
+ `
148
+ } catch (err: unknown) {
149
+ if (
150
+ err instanceof Error &&
151
+ (err.message.includes('unique constraint') || err.message.includes('duplicate key'))
152
+ ) {
153
+ throw new DuplicateEmailError()
154
+ }
155
+ throw err
156
+ }
157
+
158
+ return { id, email: normalizedEmail, name: params.name, emailVerified: false, createdAt: now }
159
+ }
160
+
161
+ async findByEmail(email: string): Promise<StoredUser | null> {
162
+ await this.ready
163
+ const rows = await this.sql`
164
+ SELECT * FROM auth_users WHERE LOWER(email) = ${email.toLowerCase()}
165
+ `
166
+ return rows.length > 0 ? rowToStoredUser(rows[0] as unknown as UserRow) : null
167
+ }
168
+
169
+ async findById(id: string): Promise<StoredUser | null> {
170
+ await this.ready
171
+ const rows = await this.sql`
172
+ SELECT * FROM auth_users WHERE id = ${id}
173
+ `
174
+ return rows.length > 0 ? rowToStoredUser(rows[0] as unknown as UserRow) : null
175
+ }
176
+
177
+ async registerDevice(params: {
178
+ id: string
179
+ userId: string
180
+ publicKey: string
181
+ name: string
182
+ }): Promise<AuthDevice> {
183
+ await this.ready
184
+ const now = Date.now()
185
+ // INSERT-or-ignore first, then read: the owner check sees whichever
186
+ // registration won, even when two users race for the same id.
187
+ await this.sql`
188
+ INSERT INTO auth_devices (id, user_id, public_key, name, revoked, created_at, last_seen_at)
189
+ VALUES (${params.id}, ${params.userId}, ${params.publicKey}, ${params.name}, FALSE, ${now}, ${now})
190
+ ON CONFLICT (id) DO NOTHING
191
+ `
192
+ const existingRows = (await this.sql`
193
+ SELECT * FROM auth_devices WHERE id = ${params.id}
194
+ `) as unknown as DeviceRow[]
195
+ const existing = existingRows[0]
196
+
197
+ if (existing && existing.user_id !== params.userId) {
198
+ throw new DeviceOwnershipError(params.id)
199
+ }
200
+ if (existing && !existing.revoked) {
201
+ return rowToDevice(existing)
202
+ }
203
+
204
+ if (existing) {
205
+ // Re-activate the owner's previously revoked device
206
+ await this.sql`
207
+ UPDATE auth_devices SET revoked = FALSE, public_key = ${params.publicKey}, name = ${params.name}, last_seen_at = ${now}
208
+ WHERE id = ${params.id} AND user_id = ${params.userId}
209
+ `
210
+ }
211
+
212
+ return {
213
+ id: params.id,
214
+ userId: params.userId,
215
+ publicKey: params.publicKey,
216
+ name: params.name,
217
+ revoked: false,
218
+ createdAt: existing ? Number(existing.created_at) : now,
219
+ lastSeenAt: now,
220
+ }
221
+ }
222
+
223
+ async findDevice(deviceId: string): Promise<AuthDevice | null> {
224
+ await this.ready
225
+ const rows = await this.sql`
226
+ SELECT * FROM auth_devices WHERE id = ${deviceId}
227
+ `
228
+ return rows.length > 0 ? rowToDevice(rows[0] as unknown as DeviceRow) : null
229
+ }
230
+
231
+ async listDevices(userId: string): Promise<AuthDevice[]> {
232
+ await this.ready
233
+ const rows = (await this.sql`
234
+ SELECT * FROM auth_devices WHERE user_id = ${userId}
235
+ `) as unknown as DeviceRow[]
236
+ return rows.map(rowToDevice)
237
+ }
238
+
239
+ async revokeDevice(deviceId: string): Promise<void> {
240
+ await this.ready
241
+ await this.sql`UPDATE auth_devices SET revoked = TRUE WHERE id = ${deviceId}`
242
+ }
243
+
244
+ async setEmailVerified(userId: string, verified: boolean): Promise<void> {
245
+ await this.ready
246
+ await this.sql`UPDATE auth_users SET email_verified = ${verified} WHERE id = ${userId}`
247
+ }
248
+
249
+ async updatePassword(userId: string, passwordHash: string, salt: string): Promise<void> {
250
+ await this.ready
251
+ await this.sql`
252
+ UPDATE auth_users SET password_hash = ${passwordHash}, salt = ${salt}
253
+ WHERE id = ${userId}
254
+ `
255
+ }
256
+
257
+ async listAll(): Promise<StoredUser[]> {
258
+ await this.ready
259
+ const rows = (await this.sql`SELECT * FROM auth_users`) as unknown as UserRow[]
260
+ return rows.map(rowToStoredUser)
261
+ }
262
+
263
+ async update(user: StoredUser): Promise<void> {
264
+ await this.ready
265
+ await this.sql`
266
+ UPDATE auth_users
267
+ SET email = ${user.email}, name = ${user.name}, email_verified = ${user.emailVerified},
268
+ password_hash = ${user.passwordHash}, salt = ${user.salt}
269
+ WHERE id = ${user.id}
270
+ `
271
+ }
272
+
273
+ async delete(userId: string): Promise<void> {
274
+ await this.ready
275
+ // Devices cascade-delete via FK constraint, but be explicit
276
+ await this.sql.begin(async (tx) => {
277
+ await tx`DELETE FROM auth_devices WHERE user_id = ${userId}`
278
+ await tx`DELETE FROM auth_users WHERE id = ${userId}`
279
+ })
280
+ }
281
+
282
+ async touchDevice(deviceId: string): Promise<void> {
283
+ await this.ready
284
+ const now = Date.now()
285
+ await this.sql`UPDATE auth_devices SET last_seen_at = ${now} WHERE id = ${deviceId}`
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Creates a PostgresUserStore from a connection string.
291
+ *
292
+ * Uses runtime dynamic imports so projects that do not use PostgreSQL
293
+ * do not need to install `postgres`.
294
+ *
295
+ * @param options.connectionString - PostgreSQL connection URL (e.g., `postgres://user:pass@host:5432/db`)
296
+ */
297
+ export async function createPostgresUserStore(options: {
298
+ connectionString: string
299
+ }): Promise<PostgresUserStore> {
300
+ const postgresClient = await loadPostgresDeps()
301
+ const sql = postgresClient(options.connectionString) as unknown as PostgresClient
302
+ return new PostgresUserStore(sql, { ownsClient: true })
303
+ }
304
+
305
+ async function loadPostgresDeps(): Promise<(connectionString: string) => unknown> {
306
+ try {
307
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as (
308
+ specifier: string,
309
+ ) => Promise<unknown>
310
+
311
+ const postgresMod = (await dynamicImport('postgres')) as { default: (cs: string) => unknown }
312
+ return postgresMod.default
313
+ } catch {
314
+ throw new Error(
315
+ 'PostgreSQL backend requires the "postgres" package. Install it in your project dependencies.',
316
+ )
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Row shapes from postgres-js queries. Field names match SQL column names.
322
+ */
323
+ interface UserRow {
324
+ id: string
325
+ email: string
326
+ name: string
327
+ email_verified: boolean
328
+ created_at: string | number
329
+ password_hash: string
330
+ salt: string
331
+ }
332
+
333
+ interface DeviceRow {
334
+ id: string
335
+ user_id: string
336
+ public_key: string
337
+ name: string
338
+ revoked: boolean
339
+ created_at: string | number
340
+ last_seen_at: string | number
341
+ }
342
+
343
+ function rowToStoredUser(row: UserRow): StoredUser {
344
+ return {
345
+ id: row.id,
346
+ email: row.email,
347
+ name: row.name,
348
+ emailVerified: Boolean(row.email_verified),
349
+ createdAt: Number(row.created_at),
350
+ passwordHash: row.password_hash,
351
+ salt: row.salt,
352
+ }
353
+ }
354
+
355
+ function rowToDevice(row: DeviceRow): AuthDevice {
356
+ return {
357
+ id: row.id,
358
+ userId: row.user_id,
359
+ publicKey: row.public_key,
360
+ name: row.name,
361
+ revoked: Boolean(row.revoked),
362
+ createdAt: Number(row.created_at),
363
+ lastSeenAt: Number(row.last_seen_at),
364
+ }
365
+ }