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