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