@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.
- 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,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
|
+
}
|