@oidcraft/drizzle 0.0.0-stage → 0.2.0

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/dist/index.mjs ADDED
@@ -0,0 +1,685 @@
1
+ import { noAccounts } from "@oidcraft/adapter";
2
+ import { and, eq, gt, isNull, lte } from "drizzle-orm";
3
+ import { index, integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
4
+ import { customType, index as index$1, integer as integer$1, pgSchema, pgTable, primaryKey as primaryKey$1, text as text$1, uniqueIndex as uniqueIndex$1 } from "drizzle-orm/pg-core";
5
+ // src/schema.ts
6
+ /**
7
+ * Real columns and real indexes for every value the core queries by (FR-A5).
8
+ *
9
+ * The shape a key-value adapter is forced into — one row of JSON per artifact — turns revocation
10
+ * and the user-code lookup into full scans. Those are on the hot path of every token request, so a
11
+ * relational adapter must not reproduce it.
12
+ */
13
+ const clients = sqliteTable("oidc_clients", {
14
+ clientId: text("client_id").primaryKey(),
15
+ data: text("data", { mode: "json" }).notNull(),
16
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
17
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull()
18
+ });
19
+ const artifacts = sqliteTable("oidc_artifacts", {
20
+ kind: text("kind").notNull(),
21
+ id: text("id").notNull(),
22
+ clientId: text("client_id").notNull(),
23
+ accountId: text("account_id"),
24
+ grantId: text("grant_id"),
25
+ familyId: text("family_id"),
26
+ userCode: text("user_code"),
27
+ payload: text("payload", { mode: "json" }).notNull(),
28
+ consumedAt: integer("consumed_at", { mode: "timestamp_ms" }),
29
+ expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull()
30
+ }, (table) => [
31
+ primaryKey({ columns: [table.kind, table.id] }),
32
+ index("oidc_artifacts_grant_idx").on(table.grantId),
33
+ index("oidc_artifacts_user_code_idx").on(table.userCode),
34
+ index("oidc_artifacts_expiry_idx").on(table.expiresAt),
35
+ index("oidc_artifacts_client_idx").on(table.clientId),
36
+ index("oidc_artifacts_family_idx").on(table.familyId)
37
+ ]);
38
+ const sessions = sqliteTable("oidc_sessions", {
39
+ id: text("id").primaryKey(),
40
+ accountId: text("account_id").notNull(),
41
+ authTime: integer("auth_time", { mode: "timestamp_ms" }).notNull(),
42
+ acr: text("acr"),
43
+ amr: text("amr", { mode: "json" }),
44
+ idp: text("idp"),
45
+ upstreamSessionId: text("upstream_session_id"),
46
+ clients: text("clients", { mode: "json" }).notNull(),
47
+ expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull()
48
+ }, (table) => [
49
+ index("oidc_sessions_account_idx").on(table.accountId),
50
+ index("oidc_sessions_upstream_idx").on(table.idp, table.upstreamSessionId),
51
+ index("oidc_sessions_expiry_idx").on(table.expiresAt)
52
+ ]);
53
+ const grants = sqliteTable("oidc_grants", {
54
+ id: text("id").primaryKey(),
55
+ accountId: text("account_id").notNull(),
56
+ clientId: text("client_id").notNull(),
57
+ scopes: text("scopes", { mode: "json" }).notNull(),
58
+ claims: text("claims", { mode: "json" }).notNull(),
59
+ resources: text("resources", { mode: "json" }).notNull(),
60
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
61
+ expiresAt: integer("expires_at", { mode: "timestamp_ms" })
62
+ }, (table) => [uniqueIndex("oidc_grants_account_client_idx").on(table.accountId, table.clientId), index("oidc_grants_client_idx").on(table.clientId)]);
63
+ const identities = sqliteTable("oidc_identities", {
64
+ provider: text("provider").notNull(),
65
+ subject: text("subject").notNull(),
66
+ accountId: text("account_id").notNull(),
67
+ email: text("email"),
68
+ claims: text("claims", { mode: "json" }).notNull(),
69
+ linkedAt: integer("linked_at", { mode: "timestamp_ms" }).notNull()
70
+ }, (table) => [primaryKey({ columns: [table.provider, table.subject] }), index("oidc_identities_account_idx").on(table.accountId)]);
71
+ const replay = sqliteTable("oidc_replay", {
72
+ namespace: text("namespace").notNull(),
73
+ value: text("value").notNull(),
74
+ expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull()
75
+ }, (table) => [primaryKey({ columns: [table.namespace, table.value] }), index("oidc_replay_expiry_idx").on(table.expiresAt)]);
76
+ const credentials = sqliteTable("oidc_credentials", {
77
+ accountId: text("account_id").primaryKey(),
78
+ username: text("username").notNull(),
79
+ passwordHash: text("password_hash").notNull(),
80
+ claims: text("claims", { mode: "json" }).notNull(),
81
+ totpSecret: text("totp_secret"),
82
+ totpConfirmedAt: integer("totp_confirmed_at", { mode: "timestamp_ms" }),
83
+ recoveryCodes: text("recovery_codes", { mode: "json" }),
84
+ failedAttempts: integer("failed_attempts").notNull().default(0),
85
+ lockedUntil: integer("locked_until", { mode: "timestamp_ms" }),
86
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
87
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull()
88
+ }, (table) => [uniqueIndex("oidc_credentials_username_idx").on(table.username)]);
89
+ const realms = sqliteTable("oidc_realms", {
90
+ id: text("id").primaryKey(),
91
+ displayName: text("display_name"),
92
+ enabled: integer("enabled", { mode: "boolean" }).notNull(),
93
+ settings: text("settings", { mode: "json" }).notNull(),
94
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
95
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull()
96
+ });
97
+ const schema = {
98
+ clients,
99
+ artifacts,
100
+ sessions,
101
+ grants,
102
+ identities,
103
+ replay,
104
+ credentials,
105
+ realms
106
+ };
107
+ // ../sql/dist/postgres.mjs
108
+ /**
109
+ * The same tables as `SQLITE_MIGRATIONS`, with the same ids, for Postgres (FR-A4). Milliseconds are
110
+ * `bigint` and JSON stays `text`, so every adapter reads and writes exactly what it does on SQLite.
111
+ * Table names are unqualified: they land in whatever schema the host migrates into, which is how a
112
+ * realm gets a schema of its own in one database (G-6).
113
+ */
114
+ const POSTGRES_MIGRATIONS = [
115
+ {
116
+ id: "0001_init",
117
+ statements: [
118
+ "create table oidc_clients (client_id text primary key, data text not null, created_at bigint not null, updated_at bigint not null)",
119
+ "create table oidc_artifacts (kind text not null, id text not null, client_id text not null, account_id text, grant_id text, user_code text, payload text not null, consumed_at bigint, expires_at bigint not null, primary key (kind, id))",
120
+ "create index oidc_artifacts_grant_idx on oidc_artifacts (grant_id)",
121
+ "create index oidc_artifacts_user_code_idx on oidc_artifacts (user_code)",
122
+ "create index oidc_artifacts_expiry_idx on oidc_artifacts (expires_at)",
123
+ "create table oidc_sessions (id text primary key, account_id text not null, auth_time bigint not null, acr text, amr text, idp text, upstream_session_id text, clients text not null, expires_at bigint not null)",
124
+ "create index oidc_sessions_account_idx on oidc_sessions (account_id)",
125
+ "create index oidc_sessions_upstream_idx on oidc_sessions (idp, upstream_session_id)",
126
+ "create table oidc_grants (id text primary key, account_id text not null, client_id text not null, scopes text not null, claims text not null, resources text not null, created_at bigint not null, expires_at bigint)",
127
+ "create unique index oidc_grants_account_client_idx on oidc_grants (account_id, client_id)",
128
+ "create table oidc_identities (provider text not null, subject text not null, account_id text not null, email text, claims text not null, linked_at bigint not null, primary key (provider, subject))",
129
+ "create table oidc_replay (namespace text not null, value text not null, expires_at bigint not null, primary key (namespace, value))"
130
+ ]
131
+ },
132
+ {
133
+ id: "0002_credentials",
134
+ statements: [
135
+ "create table oidc_credentials (account_id text primary key, username text not null, password_hash text not null, claims text not null, totp_secret text, totp_confirmed_at bigint, recovery_codes text, failed_attempts integer not null default 0, locked_until bigint, created_at bigint not null, updated_at bigint not null)",
136
+ "create unique index oidc_credentials_username_idx on oidc_credentials (username)",
137
+ "create index oidc_identities_account_idx on oidc_identities (account_id)",
138
+ "create index oidc_replay_expiry_idx on oidc_replay (expires_at)"
139
+ ]
140
+ },
141
+ {
142
+ id: "0003_realms",
143
+ statements: ["create table oidc_realms (id text primary key, display_name text, enabled integer not null, settings text not null, created_at bigint not null, updated_at bigint not null)"]
144
+ },
145
+ {
146
+ id: "0004_indexes",
147
+ statements: [
148
+ "create index oidc_artifacts_client_idx on oidc_artifacts (client_id)",
149
+ "create index oidc_grants_client_idx on oidc_grants (client_id)",
150
+ "create index oidc_sessions_expiry_idx on oidc_sessions (expires_at)"
151
+ ]
152
+ },
153
+ {
154
+ id: "0005_families",
155
+ statements: ["alter table oidc_artifacts add column family_id text", "create index oidc_artifacts_family_idx on oidc_artifacts (family_id)"]
156
+ }
157
+ ];
158
+ // ../sql/dist/sqlite.mjs
159
+ /**
160
+ * The SQLite schema every SQL adapter queries, as ordered migrations the host applies with its own
161
+ * tooling. The library never runs them: a library that migrates on import migrates during a rolling
162
+ * deploy, from several processes at once (ARCHITECTURE.md §4.2, FR-A4).
163
+ */
164
+ const SQLITE_MIGRATIONS = [
165
+ {
166
+ id: "0001_init",
167
+ statements: [
168
+ "create table oidc_clients (client_id text primary key, data text not null, created_at integer not null, updated_at integer not null)",
169
+ "create table oidc_artifacts (kind text not null, id text not null, client_id text not null, account_id text, grant_id text, user_code text, payload text not null, consumed_at integer, expires_at integer not null, primary key (kind, id))",
170
+ "create index oidc_artifacts_grant_idx on oidc_artifacts (grant_id)",
171
+ "create index oidc_artifacts_user_code_idx on oidc_artifacts (user_code)",
172
+ "create index oidc_artifacts_expiry_idx on oidc_artifacts (expires_at)",
173
+ "create table oidc_sessions (id text primary key, account_id text not null, auth_time integer not null, acr text, amr text, idp text, upstream_session_id text, clients text not null, expires_at integer not null)",
174
+ "create index oidc_sessions_account_idx on oidc_sessions (account_id)",
175
+ "create index oidc_sessions_upstream_idx on oidc_sessions (idp, upstream_session_id)",
176
+ "create table oidc_grants (id text primary key, account_id text not null, client_id text not null, scopes text not null, claims text not null, resources text not null, created_at integer not null, expires_at integer)",
177
+ "create unique index oidc_grants_account_client_idx on oidc_grants (account_id, client_id)",
178
+ "create table oidc_identities (provider text not null, subject text not null, account_id text not null, email text, claims text not null, linked_at integer not null, primary key (provider, subject))",
179
+ "create table oidc_replay (namespace text not null, value text not null, expires_at integer not null, primary key (namespace, value))"
180
+ ]
181
+ },
182
+ {
183
+ id: "0002_credentials",
184
+ statements: [
185
+ "create table oidc_credentials (account_id text primary key, username text not null, password_hash text not null, claims text not null, totp_secret text, totp_confirmed_at integer, recovery_codes text, failed_attempts integer not null default 0, locked_until integer, created_at integer not null, updated_at integer not null)",
186
+ "create unique index oidc_credentials_username_idx on oidc_credentials (username)",
187
+ "create index oidc_identities_account_idx on oidc_identities (account_id)",
188
+ "create index oidc_replay_expiry_idx on oidc_replay (expires_at)"
189
+ ]
190
+ },
191
+ {
192
+ id: "0003_realms",
193
+ statements: ["create table oidc_realms (id text primary key, display_name text, enabled integer not null, settings text not null, created_at integer not null, updated_at integer not null)"]
194
+ },
195
+ {
196
+ id: "0004_indexes",
197
+ statements: [
198
+ "create index oidc_artifacts_client_idx on oidc_artifacts (client_id)",
199
+ "create index oidc_grants_client_idx on oidc_grants (client_id)",
200
+ "create index oidc_sessions_expiry_idx on oidc_sessions (expires_at)"
201
+ ]
202
+ },
203
+ {
204
+ id: "0005_families",
205
+ statements: ["alter table oidc_artifacts add column family_id text", "create index oidc_artifacts_family_idx on oidc_artifacts (family_id)"]
206
+ }
207
+ ];
208
+ // src/postgres-schema.ts
209
+ const millis = customType({
210
+ dataType: () => "bigint",
211
+ toDriver: (value) => value.getTime(),
212
+ fromDriver: (value) => new Date(Number(value))
213
+ });
214
+ const json = customType({
215
+ dataType: () => "text",
216
+ toDriver: (value) => JSON.stringify(value),
217
+ fromDriver: (value) => JSON.parse(value)
218
+ });
219
+ const flag = customType({
220
+ dataType: () => "integer",
221
+ toDriver: (value) => value ? 1 : 0,
222
+ fromDriver: (value) => Boolean(value)
223
+ });
224
+ /**
225
+ * The oidcraft tables for Postgres, in `schema` or in the search path when it is omitted. A factory,
226
+ * because Drizzle fixes a table's schema when the table is defined: a realm with a schema of its own
227
+ * (G-6) needs tables of its own. Pass the result as `drizzleAdapter`'s `tables`.
228
+ */
229
+ const postgresTables = (schema) => {
230
+ const table = schema ? pgSchema(schema).table.bind(pgSchema(schema)) : pgTable;
231
+ return {
232
+ clients: table("oidc_clients", {
233
+ clientId: text$1("client_id").primaryKey(),
234
+ data: json("data").notNull(),
235
+ createdAt: millis("created_at").notNull(),
236
+ updatedAt: millis("updated_at").notNull()
237
+ }),
238
+ artifacts: table("oidc_artifacts", {
239
+ kind: text$1("kind").notNull(),
240
+ id: text$1("id").notNull(),
241
+ clientId: text$1("client_id").notNull(),
242
+ accountId: text$1("account_id"),
243
+ grantId: text$1("grant_id"),
244
+ familyId: text$1("family_id"),
245
+ userCode: text$1("user_code"),
246
+ payload: json("payload").notNull(),
247
+ consumedAt: millis("consumed_at"),
248
+ expiresAt: millis("expires_at").notNull()
249
+ }, (t) => [
250
+ primaryKey$1({ columns: [t.kind, t.id] }),
251
+ index$1("oidc_artifacts_grant_idx").on(t.grantId),
252
+ index$1("oidc_artifacts_user_code_idx").on(t.userCode),
253
+ index$1("oidc_artifacts_expiry_idx").on(t.expiresAt),
254
+ index$1("oidc_artifacts_client_idx").on(t.clientId),
255
+ index$1("oidc_artifacts_family_idx").on(t.familyId)
256
+ ]),
257
+ sessions: table("oidc_sessions", {
258
+ id: text$1("id").primaryKey(),
259
+ accountId: text$1("account_id").notNull(),
260
+ authTime: millis("auth_time").notNull(),
261
+ acr: text$1("acr"),
262
+ amr: json("amr"),
263
+ idp: text$1("idp"),
264
+ upstreamSessionId: text$1("upstream_session_id"),
265
+ clients: json("clients").notNull(),
266
+ expiresAt: millis("expires_at").notNull()
267
+ }, (t) => [
268
+ index$1("oidc_sessions_account_idx").on(t.accountId),
269
+ index$1("oidc_sessions_upstream_idx").on(t.idp, t.upstreamSessionId),
270
+ index$1("oidc_sessions_expiry_idx").on(t.expiresAt)
271
+ ]),
272
+ grants: table("oidc_grants", {
273
+ id: text$1("id").primaryKey(),
274
+ accountId: text$1("account_id").notNull(),
275
+ clientId: text$1("client_id").notNull(),
276
+ scopes: json("scopes").notNull(),
277
+ claims: json("claims").notNull(),
278
+ resources: json("resources").notNull(),
279
+ createdAt: millis("created_at").notNull(),
280
+ expiresAt: millis("expires_at")
281
+ }, (t) => [uniqueIndex$1("oidc_grants_account_client_idx").on(t.accountId, t.clientId), index$1("oidc_grants_client_idx").on(t.clientId)]),
282
+ identities: table("oidc_identities", {
283
+ provider: text$1("provider").notNull(),
284
+ subject: text$1("subject").notNull(),
285
+ accountId: text$1("account_id").notNull(),
286
+ email: text$1("email"),
287
+ claims: json("claims").notNull(),
288
+ linkedAt: millis("linked_at").notNull()
289
+ }, (t) => [primaryKey$1({ columns: [t.provider, t.subject] }), index$1("oidc_identities_account_idx").on(t.accountId)]),
290
+ replay: table("oidc_replay", {
291
+ namespace: text$1("namespace").notNull(),
292
+ value: text$1("value").notNull(),
293
+ expiresAt: millis("expires_at").notNull()
294
+ }, (t) => [primaryKey$1({ columns: [t.namespace, t.value] }), index$1("oidc_replay_expiry_idx").on(t.expiresAt)]),
295
+ credentials: table("oidc_credentials", {
296
+ accountId: text$1("account_id").primaryKey(),
297
+ username: text$1("username").notNull(),
298
+ passwordHash: text$1("password_hash").notNull(),
299
+ claims: json("claims").notNull(),
300
+ totpSecret: text$1("totp_secret"),
301
+ totpConfirmedAt: millis("totp_confirmed_at"),
302
+ recoveryCodes: json("recovery_codes"),
303
+ failedAttempts: integer$1("failed_attempts").notNull().default(0),
304
+ lockedUntil: millis("locked_until"),
305
+ createdAt: millis("created_at").notNull(),
306
+ updatedAt: millis("updated_at").notNull()
307
+ }, (t) => [uniqueIndex$1("oidc_credentials_username_idx").on(t.username)]),
308
+ realms: table("oidc_realms", {
309
+ id: text$1("id").primaryKey(),
310
+ displayName: text$1("display_name"),
311
+ enabled: flag("enabled").notNull(),
312
+ settings: json("settings").notNull(),
313
+ createdAt: millis("created_at").notNull(),
314
+ updatedAt: millis("updated_at").notNull()
315
+ })
316
+ };
317
+ };
318
+ // src/index.ts
319
+ const now = () => /* @__PURE__ */ new Date();
320
+ /** FR-T5: enforced by the query, so a missed prune still cannot serve an expired row. */
321
+ const live = (column) => gt(column, now());
322
+ const drizzleAdapter = (options) => {
323
+ const db = options.db;
324
+ const { clients, artifacts, sessions, grants, identities, replay, credentials, realms } = options.tables ?? schema;
325
+ const toArtifact = (row) => ({
326
+ id: row.id,
327
+ kind: row.kind,
328
+ clientId: row.clientId,
329
+ ...row.accountId && { accountId: row.accountId },
330
+ ...row.grantId && { grantId: row.grantId },
331
+ ...row.familyId && { familyId: row.familyId },
332
+ payload: row.payload,
333
+ ...row.consumedAt && { consumedAt: row.consumedAt },
334
+ expiresAt: row.expiresAt
335
+ });
336
+ const toSession = (row) => ({
337
+ id: row.id,
338
+ accountId: row.accountId,
339
+ authTime: row.authTime,
340
+ ...row.acr && { acr: row.acr },
341
+ ...row.amr ? { amr: row.amr } : {},
342
+ ...row.idp && { idp: row.idp },
343
+ ...row.upstreamSessionId && { upstreamSessionId: row.upstreamSessionId },
344
+ clients: row.clients,
345
+ expiresAt: row.expiresAt
346
+ });
347
+ const toGrant = (row) => ({
348
+ id: row.id,
349
+ accountId: row.accountId,
350
+ clientId: row.clientId,
351
+ scopes: row.scopes,
352
+ claims: row.claims,
353
+ resources: row.resources,
354
+ createdAt: row.createdAt,
355
+ ...row.expiresAt && { expiresAt: row.expiresAt }
356
+ });
357
+ return {
358
+ clients: {
359
+ async find(clientId) {
360
+ const [row] = await db.select().from(clients).where(eq(clients.clientId, clientId)).limit(1);
361
+ return row ? {
362
+ ...row.data,
363
+ createdAt: row.createdAt,
364
+ updatedAt: row.updatedAt
365
+ } : void 0;
366
+ },
367
+ async upsert(client) {
368
+ await db.insert(clients).values({
369
+ clientId: client.clientId,
370
+ data: client,
371
+ createdAt: client.createdAt,
372
+ updatedAt: client.updatedAt
373
+ }).onConflictDoUpdate({
374
+ target: clients.clientId,
375
+ set: {
376
+ data: client,
377
+ updatedAt: client.updatedAt
378
+ }
379
+ });
380
+ },
381
+ async destroy(clientId) {
382
+ await db.delete(clients).where(eq(clients.clientId, clientId));
383
+ },
384
+ async list(cursor, limit) {
385
+ const ordered = db.select().from(clients).where(cursor === void 0 ? void 0 : gt(clients.clientId, cursor)).orderBy(clients.clientId);
386
+ const rows = await (limit === void 0 ? ordered : ordered.limit(limit + 1));
387
+ const more = limit !== void 0 && rows.length > limit;
388
+ const shown = more ? rows.slice(0, limit) : rows;
389
+ return {
390
+ clients: shown.map((row) => ({
391
+ ...row.data,
392
+ createdAt: row.createdAt,
393
+ updatedAt: row.updatedAt
394
+ })),
395
+ ...more && { cursor: shown.at(-1)?.clientId }
396
+ };
397
+ }
398
+ },
399
+ artifacts: {
400
+ async upsert(artifact) {
401
+ const row = {
402
+ kind: artifact.kind,
403
+ id: artifact.id,
404
+ clientId: artifact.clientId,
405
+ accountId: artifact.accountId ?? null,
406
+ grantId: artifact.grantId ?? null,
407
+ familyId: artifact.familyId ?? null,
408
+ userCode: artifact.payload.userCode ?? null,
409
+ payload: artifact.payload,
410
+ consumedAt: artifact.consumedAt ?? null,
411
+ expiresAt: artifact.expiresAt
412
+ };
413
+ await db.insert(artifacts).values(row).onConflictDoUpdate({
414
+ target: [artifacts.kind, artifacts.id],
415
+ set: {
416
+ ...row,
417
+ consumedAt: void 0
418
+ }
419
+ });
420
+ },
421
+ async find(kind, id) {
422
+ const [row] = await db.select().from(artifacts).where(and(eq(artifacts.kind, kind), eq(artifacts.id, id), live(artifacts.expiresAt))).limit(1);
423
+ return row ? toArtifact(row) : void 0;
424
+ },
425
+ async findByUserCode(userCode) {
426
+ const [row] = await db.select().from(artifacts).where(and(eq(artifacts.userCode, userCode), live(artifacts.expiresAt))).limit(1);
427
+ return row ? toArtifact(row) : void 0;
428
+ },
429
+ async consume(kind, id) {
430
+ return (await db.update(artifacts).set({ consumedAt: now() }).where(and(eq(artifacts.kind, kind), eq(artifacts.id, id), isNull(artifacts.consumedAt), live(artifacts.expiresAt))).returning({ id: artifacts.id })).length === 1;
431
+ },
432
+ async destroy(kind, id) {
433
+ await db.delete(artifacts).where(and(eq(artifacts.kind, kind), eq(artifacts.id, id)));
434
+ },
435
+ async revokeByGrantId(grantId) {
436
+ await db.delete(artifacts).where(eq(artifacts.grantId, grantId));
437
+ },
438
+ async revokeByFamilyId(familyId) {
439
+ await db.delete(artifacts).where(eq(artifacts.familyId, familyId));
440
+ },
441
+ async revokeByClientId(clientId) {
442
+ await db.delete(artifacts).where(eq(artifacts.clientId, clientId));
443
+ },
444
+ async prune(before, limit) {
445
+ const doomed = await db.select({
446
+ kind: artifacts.kind,
447
+ id: artifacts.id
448
+ }).from(artifacts).where(lte(artifacts.expiresAt, before)).limit(limit);
449
+ for (const row of doomed) await db.delete(artifacts).where(and(eq(artifacts.kind, row.kind), eq(artifacts.id, row.id)));
450
+ return doomed.length;
451
+ }
452
+ },
453
+ sessions: {
454
+ async find(id) {
455
+ const [row] = await db.select().from(sessions).where(and(eq(sessions.id, id), gt(sessions.expiresAt, now()))).limit(1);
456
+ return row ? toSession(row) : void 0;
457
+ },
458
+ async listForAccount(accountId) {
459
+ return (await db.select().from(sessions).where(and(eq(sessions.accountId, accountId), gt(sessions.expiresAt, now())))).map(toSession);
460
+ },
461
+ async findByUpstreamSession(provider, upstreamSessionId) {
462
+ return (await db.select().from(sessions).where(and(eq(sessions.idp, provider), eq(sessions.upstreamSessionId, upstreamSessionId), gt(sessions.expiresAt, now())))).map(toSession);
463
+ },
464
+ async upsert(session) {
465
+ const row = {
466
+ id: session.id,
467
+ accountId: session.accountId,
468
+ authTime: session.authTime,
469
+ acr: session.acr ?? null,
470
+ amr: session.amr ?? null,
471
+ idp: session.idp ?? null,
472
+ upstreamSessionId: session.upstreamSessionId ?? null,
473
+ clients: session.clients,
474
+ expiresAt: session.expiresAt
475
+ };
476
+ await db.insert(sessions).values(row).onConflictDoUpdate({
477
+ target: sessions.id,
478
+ set: row
479
+ });
480
+ },
481
+ async addClient(id, clientId) {
482
+ for (let attempt = 0; attempt < 64; attempt++) {
483
+ const [row] = await db.select({ clients: sessions.clients }).from(sessions).where(eq(sessions.id, id)).limit(1);
484
+ if (!row) return;
485
+ const current = row.clients;
486
+ if (current.includes(clientId)) return;
487
+ if ((await db.update(sessions).set({ clients: [...current, clientId] }).where(and(eq(sessions.id, id), eq(sessions.clients, current))).returning({ id: sessions.id })).length) return;
488
+ }
489
+ throw new Error(`session ${id} kept changing; its client list could not be updated`);
490
+ },
491
+ async destroy(id) {
492
+ await db.delete(sessions).where(eq(sessions.id, id));
493
+ },
494
+ async prune(before, limit) {
495
+ const doomed = await db.select({ id: sessions.id }).from(sessions).where(lte(sessions.expiresAt, before)).limit(limit);
496
+ for (const row of doomed) await db.delete(sessions).where(and(eq(sessions.id, row.id), lte(sessions.expiresAt, before)));
497
+ return doomed.length;
498
+ }
499
+ },
500
+ grants: {
501
+ async find(id) {
502
+ const [row] = await db.select().from(grants).where(eq(grants.id, id)).limit(1);
503
+ return row ? toGrant(row) : void 0;
504
+ },
505
+ async findByAccountAndClient(accountId, clientId) {
506
+ const [row] = await db.select().from(grants).where(and(eq(grants.accountId, accountId), eq(grants.clientId, clientId))).limit(1);
507
+ return row ? toGrant(row) : void 0;
508
+ },
509
+ async listForAccount(accountId) {
510
+ return (await db.select().from(grants).where(eq(grants.accountId, accountId))).map(toGrant);
511
+ },
512
+ async upsert(grant) {
513
+ const row = {
514
+ id: grant.id,
515
+ accountId: grant.accountId,
516
+ clientId: grant.clientId,
517
+ scopes: grant.scopes,
518
+ claims: grant.claims,
519
+ resources: grant.resources,
520
+ createdAt: grant.createdAt,
521
+ expiresAt: grant.expiresAt ?? null
522
+ };
523
+ const { id: _id, createdAt: _createdAt, ...changed } = row;
524
+ await db.insert(grants).values(row).onConflictDoUpdate({
525
+ target: [grants.accountId, grants.clientId],
526
+ set: changed
527
+ });
528
+ },
529
+ async destroy(id) {
530
+ await db.delete(grants).where(eq(grants.id, id));
531
+ },
532
+ async destroyByClientId(clientId) {
533
+ await db.delete(grants).where(eq(grants.clientId, clientId));
534
+ }
535
+ },
536
+ accounts: options.accounts ?? noAccounts,
537
+ keys: options.keys,
538
+ replay: {
539
+ async claim(namespace, value, ttl) {
540
+ const expiresAt = new Date(Date.now() + ttl * 1e3);
541
+ return (await db.insert(replay).values({
542
+ namespace,
543
+ value,
544
+ expiresAt
545
+ }).onConflictDoUpdate({
546
+ target: [replay.namespace, replay.value],
547
+ set: { expiresAt },
548
+ setWhere: lte(replay.expiresAt, now())
549
+ }).returning({ value: replay.value })).length === 1;
550
+ },
551
+ async prune(before, limit) {
552
+ const doomed = await db.select({
553
+ namespace: replay.namespace,
554
+ value: replay.value
555
+ }).from(replay).where(lte(replay.expiresAt, before)).limit(limit);
556
+ for (const row of doomed) await db.delete(replay).where(and(eq(replay.namespace, row.namespace), eq(replay.value, row.value), lte(replay.expiresAt, before)));
557
+ return doomed.length;
558
+ }
559
+ },
560
+ credentials: {
561
+ async find(accountId) {
562
+ const [row] = await db.select().from(credentials).where(eq(credentials.accountId, accountId)).limit(1);
563
+ return row ? toCredential(row) : void 0;
564
+ },
565
+ async findByUsername(username) {
566
+ const [row] = await db.select().from(credentials).where(eq(credentials.username, username)).limit(1);
567
+ return row ? toCredential(row) : void 0;
568
+ },
569
+ async create(credential) {
570
+ return (await db.insert(credentials).values(credentialRow(credential)).onConflictDoNothing().returning({ id: credentials.accountId })).length === 1;
571
+ },
572
+ async upsert(credential) {
573
+ const row = credentialRow(credential);
574
+ await db.insert(credentials).values(row).onConflictDoUpdate({
575
+ target: credentials.accountId,
576
+ set: row
577
+ });
578
+ },
579
+ async destroy(accountId) {
580
+ await db.delete(credentials).where(eq(credentials.accountId, accountId));
581
+ }
582
+ },
583
+ realms: {
584
+ async find(id) {
585
+ const [row] = await db.select().from(realms).where(eq(realms.id, id)).limit(1);
586
+ return row ? toRealm(row) : void 0;
587
+ },
588
+ async list() {
589
+ return (await db.select().from(realms)).map(toRealm);
590
+ },
591
+ async create(realm) {
592
+ return (await db.insert(realms).values(realmRow(realm)).onConflictDoNothing().returning({ id: realms.id })).length === 1;
593
+ },
594
+ async upsert(realm) {
595
+ const row = realmRow(realm);
596
+ await db.insert(realms).values(row).onConflictDoUpdate({
597
+ target: realms.id,
598
+ set: row
599
+ });
600
+ },
601
+ async destroy(id) {
602
+ await db.delete(realms).where(eq(realms.id, id));
603
+ }
604
+ },
605
+ identities: {
606
+ async find(provider, subject) {
607
+ const [row] = await db.select().from(identities).where(and(eq(identities.provider, provider), eq(identities.subject, subject))).limit(1);
608
+ return row ? toIdentity(row) : void 0;
609
+ },
610
+ async listForAccount(accountId) {
611
+ return (await db.select().from(identities).where(eq(identities.accountId, accountId))).map(toIdentity);
612
+ },
613
+ async upsert(identity) {
614
+ const row = {
615
+ provider: identity.provider,
616
+ subject: identity.subject,
617
+ accountId: identity.accountId,
618
+ email: identity.email ?? null,
619
+ claims: identity.claims,
620
+ linkedAt: identity.linkedAt
621
+ };
622
+ await db.insert(identities).values(row).onConflictDoUpdate({
623
+ target: [identities.provider, identities.subject],
624
+ set: row
625
+ });
626
+ },
627
+ async destroy(provider, subject) {
628
+ await db.delete(identities).where(and(eq(identities.provider, provider), eq(identities.subject, subject)));
629
+ }
630
+ }
631
+ };
632
+ };
633
+ const credentialRow = (credential) => ({
634
+ accountId: credential.accountId,
635
+ username: credential.username,
636
+ passwordHash: credential.passwordHash,
637
+ claims: credential.claims,
638
+ totpSecret: credential.totp?.secret ?? null,
639
+ totpConfirmedAt: credential.totp?.confirmedAt ?? null,
640
+ recoveryCodes: credential.recoveryCodes ?? null,
641
+ failedAttempts: credential.failedAttempts,
642
+ lockedUntil: credential.lockedUntil ?? null,
643
+ createdAt: credential.createdAt,
644
+ updatedAt: credential.updatedAt
645
+ });
646
+ const toCredential = (row) => ({
647
+ accountId: row.accountId,
648
+ username: row.username,
649
+ passwordHash: row.passwordHash,
650
+ claims: row.claims,
651
+ ...row.totpSecret && { totp: {
652
+ secret: row.totpSecret,
653
+ ...row.totpConfirmedAt && { confirmedAt: row.totpConfirmedAt }
654
+ } },
655
+ ...row.recoveryCodes ? { recoveryCodes: row.recoveryCodes } : {},
656
+ failedAttempts: row.failedAttempts,
657
+ ...row.lockedUntil && { lockedUntil: row.lockedUntil },
658
+ createdAt: row.createdAt,
659
+ updatedAt: row.updatedAt
660
+ });
661
+ const realmRow = (realm) => ({
662
+ id: realm.id,
663
+ displayName: realm.displayName ?? null,
664
+ enabled: realm.enabled,
665
+ settings: realm.settings,
666
+ createdAt: realm.createdAt,
667
+ updatedAt: realm.updatedAt
668
+ });
669
+ const toRealm = (row) => ({
670
+ id: row.id,
671
+ ...row.displayName && { displayName: row.displayName },
672
+ enabled: row.enabled,
673
+ settings: row.settings,
674
+ createdAt: row.createdAt,
675
+ updatedAt: row.updatedAt
676
+ });
677
+ const toIdentity = (row) => ({
678
+ accountId: row.accountId,
679
+ provider: row.provider,
680
+ subject: row.subject,
681
+ ...row.email && { email: row.email },
682
+ claims: row.claims,
683
+ linkedAt: row.linkedAt
684
+ });
685
+ export { POSTGRES_MIGRATIONS, SQLITE_MIGRATIONS, artifacts, clients, credentials, drizzleAdapter, grants, identities, postgresTables, realms, replay, schema, sessions };