@oidcraft/kysely 0.1.0 → 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.d.mts CHANGED
@@ -14,6 +14,7 @@ export type ArtifactsTable = {
14
14
  client_id: string;
15
15
  account_id: string | null;
16
16
  grant_id: string | null;
17
+ family_id: string | null;
17
18
  user_code: string | null;
18
19
  payload: string;
19
20
  consumed_at: Millis | null;
package/dist/index.mjs CHANGED
@@ -1,3 +1,146 @@
1
+ import { noAccounts } from "@oidcraft/adapter";
2
+ // ../sql/dist/rows.mjs
3
+ /** Never `new Date` on the raw value: a string there is read as a date, not as milliseconds. */
4
+ const at = (millis) => new Date(Number(millis));
5
+ const json = (value) => JSON.stringify(value);
6
+ const parse = (value) => JSON.parse(value);
7
+ const clientRow = (client) => ({
8
+ client_id: client.clientId,
9
+ data: json(client),
10
+ created_at: client.createdAt.getTime(),
11
+ updated_at: client.updatedAt.getTime()
12
+ });
13
+ const toClient = (row) => ({
14
+ ...parse(row.data),
15
+ createdAt: at(row.created_at),
16
+ updatedAt: at(row.updated_at)
17
+ });
18
+ /** `user_code` is lifted out of the payload so a device's code is found by index (FR-C7). */
19
+ const artifactRow = (artifact) => ({
20
+ kind: artifact.kind,
21
+ id: artifact.id,
22
+ client_id: artifact.clientId,
23
+ account_id: artifact.accountId ?? null,
24
+ grant_id: artifact.grantId ?? null,
25
+ family_id: artifact.familyId ?? null,
26
+ user_code: artifact.payload.userCode ?? null,
27
+ payload: json(artifact.payload),
28
+ consumed_at: artifact.consumedAt ? artifact.consumedAt.getTime() : null,
29
+ expires_at: artifact.expiresAt.getTime()
30
+ });
31
+ const toArtifact = (row) => ({
32
+ id: row.id,
33
+ kind: row.kind,
34
+ clientId: row.client_id,
35
+ ...row.account_id && { accountId: row.account_id },
36
+ ...row.grant_id && { grantId: row.grant_id },
37
+ ...row.family_id && { familyId: row.family_id },
38
+ payload: parse(row.payload),
39
+ ...row.consumed_at && { consumedAt: at(row.consumed_at) },
40
+ expiresAt: at(row.expires_at)
41
+ });
42
+ const sessionRow = (session) => ({
43
+ id: session.id,
44
+ account_id: session.accountId,
45
+ auth_time: session.authTime.getTime(),
46
+ acr: session.acr ?? null,
47
+ amr: session.amr ? json(session.amr) : null,
48
+ idp: session.idp ?? null,
49
+ upstream_session_id: session.upstreamSessionId ?? null,
50
+ clients: json(session.clients),
51
+ expires_at: session.expiresAt.getTime()
52
+ });
53
+ const toSession = (row) => ({
54
+ id: row.id,
55
+ accountId: row.account_id,
56
+ authTime: at(row.auth_time),
57
+ ...row.acr && { acr: row.acr },
58
+ ...row.amr ? { amr: parse(row.amr) } : {},
59
+ ...row.idp && { idp: row.idp },
60
+ ...row.upstream_session_id && { upstreamSessionId: row.upstream_session_id },
61
+ clients: parse(row.clients),
62
+ expiresAt: at(row.expires_at)
63
+ });
64
+ const grantRow = (grant) => ({
65
+ id: grant.id,
66
+ account_id: grant.accountId,
67
+ client_id: grant.clientId,
68
+ scopes: json(grant.scopes),
69
+ claims: json(grant.claims),
70
+ resources: json(grant.resources),
71
+ created_at: grant.createdAt.getTime(),
72
+ expires_at: grant.expiresAt ? grant.expiresAt.getTime() : null
73
+ });
74
+ const toGrant = (row) => ({
75
+ id: row.id,
76
+ accountId: row.account_id,
77
+ clientId: row.client_id,
78
+ scopes: parse(row.scopes),
79
+ claims: parse(row.claims),
80
+ resources: parse(row.resources),
81
+ createdAt: at(row.created_at),
82
+ ...row.expires_at && { expiresAt: at(row.expires_at) }
83
+ });
84
+ const identityRow = (identity) => ({
85
+ provider: identity.provider,
86
+ subject: identity.subject,
87
+ account_id: identity.accountId,
88
+ email: identity.email ?? null,
89
+ claims: json(identity.claims),
90
+ linked_at: identity.linkedAt.getTime()
91
+ });
92
+ const toIdentity = (row) => ({
93
+ accountId: row.account_id,
94
+ provider: row.provider,
95
+ subject: row.subject,
96
+ ...row.email && { email: row.email },
97
+ claims: parse(row.claims),
98
+ linkedAt: at(row.linked_at)
99
+ });
100
+ const credentialRow = (credential) => ({
101
+ account_id: credential.accountId,
102
+ username: credential.username,
103
+ password_hash: credential.passwordHash,
104
+ claims: json(credential.claims),
105
+ totp_secret: credential.totp?.secret ?? null,
106
+ totp_confirmed_at: credential.totp?.confirmedAt?.getTime() ?? null,
107
+ recovery_codes: credential.recoveryCodes ? json(credential.recoveryCodes) : null,
108
+ failed_attempts: credential.failedAttempts,
109
+ locked_until: credential.lockedUntil?.getTime() ?? null,
110
+ created_at: credential.createdAt.getTime(),
111
+ updated_at: credential.updatedAt.getTime()
112
+ });
113
+ const toCredential = (row) => ({
114
+ accountId: row.account_id,
115
+ username: row.username,
116
+ passwordHash: row.password_hash,
117
+ claims: parse(row.claims),
118
+ ...row.totp_secret && { totp: {
119
+ secret: row.totp_secret,
120
+ ...row.totp_confirmed_at && { confirmedAt: at(row.totp_confirmed_at) }
121
+ } },
122
+ ...row.recovery_codes ? { recoveryCodes: parse(row.recovery_codes) } : {},
123
+ failedAttempts: Number(row.failed_attempts),
124
+ ...row.locked_until && { lockedUntil: at(row.locked_until) },
125
+ createdAt: at(row.created_at),
126
+ updatedAt: at(row.updated_at)
127
+ });
128
+ const realmRow = (realm) => ({
129
+ id: realm.id,
130
+ display_name: realm.displayName ?? null,
131
+ enabled: realm.enabled ? 1 : 0,
132
+ settings: json(realm.settings),
133
+ created_at: realm.createdAt.getTime(),
134
+ updated_at: realm.updatedAt.getTime()
135
+ });
136
+ const toRealm = (row) => ({
137
+ id: row.id,
138
+ ...row.display_name && { displayName: row.display_name },
139
+ enabled: Boolean(Number(row.enabled)),
140
+ settings: parse(row.settings),
141
+ createdAt: at(row.created_at),
142
+ updatedAt: at(row.updated_at)
143
+ });
1
144
  // ../sql/dist/postgres.mjs
2
145
  /**
3
146
  * The same tables as `SQLITE_MIGRATIONS`, with the same ids, for Postgres (FR-A4). Milliseconds are
@@ -35,6 +178,18 @@ const POSTGRES_MIGRATIONS = [
35
178
  {
36
179
  id: "0003_realms",
37
180
  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)"]
181
+ },
182
+ {
183
+ id: "0004_indexes",
184
+ statements: [
185
+ "create index oidc_artifacts_client_idx on oidc_artifacts (client_id)",
186
+ "create index oidc_grants_client_idx on oidc_grants (client_id)",
187
+ "create index oidc_sessions_expiry_idx on oidc_sessions (expires_at)"
188
+ ]
189
+ },
190
+ {
191
+ id: "0005_families",
192
+ statements: ["alter table oidc_artifacts add column family_id text", "create index oidc_artifacts_family_idx on oidc_artifacts (family_id)"]
38
193
  }
39
194
  ];
40
195
  // ../sql/dist/sqlite.mjs
@@ -73,100 +228,22 @@ const SQLITE_MIGRATIONS = [
73
228
  {
74
229
  id: "0003_realms",
75
230
  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)"]
231
+ },
232
+ {
233
+ id: "0004_indexes",
234
+ statements: [
235
+ "create index oidc_artifacts_client_idx on oidc_artifacts (client_id)",
236
+ "create index oidc_grants_client_idx on oidc_grants (client_id)",
237
+ "create index oidc_sessions_expiry_idx on oidc_sessions (expires_at)"
238
+ ]
239
+ },
240
+ {
241
+ id: "0005_families",
242
+ statements: ["alter table oidc_artifacts add column family_id text", "create index oidc_artifacts_family_idx on oidc_artifacts (family_id)"]
76
243
  }
77
244
  ];
78
245
  // src/index.ts
79
246
  const now = () => Date.now();
80
- const at = (millis) => new Date(Number(millis));
81
- const json = (value) => JSON.stringify(value);
82
- const parse = (value) => JSON.parse(value);
83
- const toArtifact = (row) => ({
84
- id: row.id,
85
- kind: row.kind,
86
- clientId: row.client_id,
87
- ...row.account_id && { accountId: row.account_id },
88
- ...row.grant_id && { grantId: row.grant_id },
89
- payload: parse(row.payload),
90
- ...row.consumed_at && { consumedAt: at(row.consumed_at) },
91
- expiresAt: at(row.expires_at)
92
- });
93
- const toSession = (row) => ({
94
- id: row.id,
95
- accountId: row.account_id,
96
- authTime: at(row.auth_time),
97
- ...row.acr && { acr: row.acr },
98
- ...row.amr ? { amr: parse(row.amr) } : {},
99
- ...row.idp && { idp: row.idp },
100
- ...row.upstream_session_id && { upstreamSessionId: row.upstream_session_id },
101
- clients: parse(row.clients),
102
- expiresAt: at(row.expires_at)
103
- });
104
- const toGrant = (row) => ({
105
- id: row.id,
106
- accountId: row.account_id,
107
- clientId: row.client_id,
108
- scopes: parse(row.scopes),
109
- claims: parse(row.claims),
110
- resources: parse(row.resources),
111
- createdAt: at(row.created_at),
112
- ...row.expires_at && { expiresAt: at(row.expires_at) }
113
- });
114
- const noAccounts = {
115
- async find() {},
116
- async claims() {}
117
- };
118
- const credentialRow = (credential) => ({
119
- account_id: credential.accountId,
120
- username: credential.username,
121
- password_hash: credential.passwordHash,
122
- claims: json(credential.claims),
123
- totp_secret: credential.totp?.secret ?? null,
124
- totp_confirmed_at: credential.totp?.confirmedAt?.getTime() ?? null,
125
- recovery_codes: credential.recoveryCodes ? json(credential.recoveryCodes) : null,
126
- failed_attempts: credential.failedAttempts,
127
- locked_until: credential.lockedUntil?.getTime() ?? null,
128
- created_at: credential.createdAt.getTime(),
129
- updated_at: credential.updatedAt.getTime()
130
- });
131
- const toCredential = (row) => ({
132
- accountId: row.account_id,
133
- username: row.username,
134
- passwordHash: row.password_hash,
135
- claims: parse(row.claims),
136
- ...row.totp_secret && { totp: {
137
- secret: row.totp_secret,
138
- ...row.totp_confirmed_at && { confirmedAt: at(row.totp_confirmed_at) }
139
- } },
140
- ...row.recovery_codes ? { recoveryCodes: parse(row.recovery_codes) } : {},
141
- failedAttempts: row.failed_attempts,
142
- ...row.locked_until && { lockedUntil: at(row.locked_until) },
143
- createdAt: at(row.created_at),
144
- updatedAt: at(row.updated_at)
145
- });
146
- const realmRow = (realm) => ({
147
- id: realm.id,
148
- display_name: realm.displayName ?? null,
149
- enabled: realm.enabled ? 1 : 0,
150
- settings: json(realm.settings),
151
- created_at: realm.createdAt.getTime(),
152
- updated_at: realm.updatedAt.getTime()
153
- });
154
- const toRealm = (row) => ({
155
- id: row.id,
156
- ...row.display_name && { displayName: row.display_name },
157
- enabled: Boolean(row.enabled),
158
- settings: parse(row.settings),
159
- createdAt: at(row.created_at),
160
- updatedAt: at(row.updated_at)
161
- });
162
- const toIdentity = (row) => ({
163
- accountId: row.account_id,
164
- provider: row.provider,
165
- subject: row.subject,
166
- ...row.email && { email: row.email },
167
- claims: parse(row.claims),
168
- linkedAt: at(row.linked_at)
169
- });
170
247
  /**
171
248
  * The same contract as the Drizzle adapter, on a query builder rather than an ORM (FR-A4). Both run
172
249
  * the one conformance suite, which is what makes "interchangeable" checkable rather than asserted.
@@ -175,17 +252,7 @@ const kyselyAdapter = (options) => {
175
252
  const { db } = options;
176
253
  const artifacts = {
177
254
  async upsert(artifact) {
178
- const row = {
179
- kind: artifact.kind,
180
- id: artifact.id,
181
- client_id: artifact.clientId,
182
- account_id: artifact.accountId ?? null,
183
- grant_id: artifact.grantId ?? null,
184
- user_code: artifact.payload.userCode ?? null,
185
- payload: json(artifact.payload),
186
- consumed_at: artifact.consumedAt ? artifact.consumedAt.getTime() : null,
187
- expires_at: artifact.expiresAt.getTime()
188
- };
255
+ const row = artifactRow(artifact);
189
256
  await db.insertInto("oidc_artifacts").values(row).onConflict((conflict) => conflict.columns(["kind", "id"]).doUpdateSet({
190
257
  ...row,
191
258
  consumed_at: void 0
@@ -208,6 +275,12 @@ const kyselyAdapter = (options) => {
208
275
  async revokeByGrantId(grantId) {
209
276
  await db.deleteFrom("oidc_artifacts").where("grant_id", "=", grantId).execute();
210
277
  },
278
+ async revokeByFamilyId(familyId) {
279
+ await db.deleteFrom("oidc_artifacts").where("family_id", "=", familyId).execute();
280
+ },
281
+ async revokeByClientId(clientId) {
282
+ await db.deleteFrom("oidc_artifacts").where("client_id", "=", clientId).execute();
283
+ },
211
284
  async prune(before, limit) {
212
285
  const doomed = await db.selectFrom("oidc_artifacts").select(["kind", "id"]).where("expires_at", "<=", before.getTime()).limit(limit).execute();
213
286
  for (const row of doomed) await artifacts.destroy(row.kind, row.id);
@@ -219,21 +292,12 @@ const kyselyAdapter = (options) => {
219
292
  async find(clientId) {
220
293
  const row = await db.selectFrom("oidc_clients").selectAll().where("client_id", "=", clientId).executeTakeFirst();
221
294
  if (!row) return void 0;
222
- return {
223
- ...parse(row.data),
224
- createdAt: at(row.created_at),
225
- updatedAt: at(row.updated_at)
226
- };
295
+ return toClient(row);
227
296
  },
228
297
  async upsert(client) {
229
- const data = json(client);
230
- const updated_at = client.updatedAt.getTime();
231
- await db.insertInto("oidc_clients").values({
232
- client_id: client.clientId,
233
- data,
234
- created_at: client.createdAt.getTime(),
235
- updated_at
236
- }).onConflict((conflict) => conflict.column("client_id").doUpdateSet({
298
+ const row = clientRow(client);
299
+ const { data, updated_at } = row;
300
+ await db.insertInto("oidc_clients").values(row).onConflict((conflict) => conflict.column("client_id").doUpdateSet({
237
301
  data,
238
302
  updated_at
239
303
  })).execute();
@@ -241,12 +305,16 @@ const kyselyAdapter = (options) => {
241
305
  async destroy(clientId) {
242
306
  await db.deleteFrom("oidc_clients").where("client_id", "=", clientId).execute();
243
307
  },
244
- async list() {
245
- return { clients: (await db.selectFrom("oidc_clients").selectAll().execute()).map((row) => ({
246
- ...parse(row.data),
247
- createdAt: at(row.created_at),
248
- updatedAt: at(row.updated_at)
249
- })) };
308
+ async list(cursor, limit) {
309
+ let query = db.selectFrom("oidc_clients").selectAll().orderBy("client_id");
310
+ if (cursor !== void 0) query = query.where("client_id", ">", cursor);
311
+ const rows = await (limit === void 0 ? query : query.limit(limit + 1)).execute();
312
+ const more = limit !== void 0 && rows.length > limit;
313
+ const shown = more ? rows.slice(0, limit) : rows;
314
+ return {
315
+ clients: shown.map(toClient),
316
+ ...more && { cursor: shown.at(-1)?.client_id }
317
+ };
250
318
  }
251
319
  },
252
320
  artifacts,
@@ -259,20 +327,10 @@ const kyselyAdapter = (options) => {
259
327
  return (await db.selectFrom("oidc_sessions").selectAll().where("account_id", "=", accountId).where("expires_at", ">", now()).execute()).map(toSession);
260
328
  },
261
329
  async findByUpstreamSession(provider, upstreamSessionId) {
262
- return (await db.selectFrom("oidc_sessions").selectAll().where("idp", "=", provider).where("upstream_session_id", "=", upstreamSessionId).execute()).map(toSession);
330
+ return (await db.selectFrom("oidc_sessions").selectAll().where("idp", "=", provider).where("upstream_session_id", "=", upstreamSessionId).where("expires_at", ">", now()).execute()).map(toSession);
263
331
  },
264
332
  async upsert(session) {
265
- const row = {
266
- id: session.id,
267
- account_id: session.accountId,
268
- auth_time: session.authTime.getTime(),
269
- acr: session.acr ?? null,
270
- amr: session.amr ? json(session.amr) : null,
271
- idp: session.idp ?? null,
272
- upstream_session_id: session.upstreamSessionId ?? null,
273
- clients: json(session.clients),
274
- expires_at: session.expiresAt.getTime()
275
- };
333
+ const row = sessionRow(session);
276
334
  await db.insertInto("oidc_sessions").values(row).onConflict((conflict) => conflict.column("id").doUpdateSet(row)).execute();
277
335
  },
278
336
  async addClient(id, clientId) {
@@ -288,7 +346,12 @@ const kyselyAdapter = (options) => {
288
346
  },
289
347
  async destroy(id) {
290
348
  await db.deleteFrom("oidc_sessions").where("id", "=", id).execute();
291
- }
349
+ },
350
+ prune: { async prune(before, limit) {
351
+ const doomed = await db.selectFrom("oidc_sessions").select("id").where("expires_at", "<=", before.getTime()).limit(limit).execute();
352
+ if (doomed.length) await db.deleteFrom("oidc_sessions").where("id", "in", doomed.map((row) => row.id)).where("expires_at", "<=", before.getTime()).execute();
353
+ return doomed.length;
354
+ } }.prune
292
355
  },
293
356
  grants: {
294
357
  async find(id) {
@@ -303,20 +366,19 @@ const kyselyAdapter = (options) => {
303
366
  return (await db.selectFrom("oidc_grants").selectAll().where("account_id", "=", accountId).execute()).map(toGrant);
304
367
  },
305
368
  async upsert(grant) {
306
- const row = {
307
- id: grant.id,
308
- account_id: grant.accountId,
309
- client_id: grant.clientId,
310
- scopes: json(grant.scopes),
311
- claims: json(grant.claims),
312
- resources: json(grant.resources),
313
- created_at: grant.createdAt.getTime(),
314
- expires_at: grant.expiresAt ? grant.expiresAt.getTime() : null
315
- };
316
- await db.insertInto("oidc_grants").values(row).onConflict((conflict) => conflict.column("id").doUpdateSet(row)).execute();
369
+ const row = grantRow(grant);
370
+ await db.insertInto("oidc_grants").values(row).onConflict((conflict) => conflict.columns(["account_id", "client_id"]).doUpdateSet({
371
+ scopes: row.scopes,
372
+ claims: row.claims,
373
+ resources: row.resources,
374
+ expires_at: row.expires_at
375
+ })).execute();
317
376
  },
318
377
  async destroy(id) {
319
378
  await db.deleteFrom("oidc_grants").where("id", "=", id).execute();
379
+ },
380
+ async destroyByClientId(clientId) {
381
+ await db.deleteFrom("oidc_grants").where("client_id", "=", clientId).execute();
320
382
  }
321
383
  },
322
384
  accounts: options.accounts ?? noAccounts,
@@ -360,14 +422,21 @@ const kyselyAdapter = (options) => {
360
422
  }
361
423
  },
362
424
  keys: options.keys,
363
- replay: { async claim(namespace, value, ttl) {
364
- const expires_at = now() + ttl * 1e3;
365
- return await db.insertInto("oidc_replay").values({
366
- namespace,
367
- value,
368
- expires_at
369
- }).onConflict((conflict) => conflict.columns(["namespace", "value"]).doUpdateSet({ expires_at }).where("oidc_replay.expires_at", "<=", now())).returning("value").executeTakeFirst() !== void 0;
370
- } },
425
+ replay: {
426
+ async claim(namespace, value, ttl) {
427
+ const expires_at = now() + ttl * 1e3;
428
+ return await db.insertInto("oidc_replay").values({
429
+ namespace,
430
+ value,
431
+ expires_at
432
+ }).onConflict((conflict) => conflict.columns(["namespace", "value"]).doUpdateSet({ expires_at }).where("oidc_replay.expires_at", "<=", now())).returning("value").executeTakeFirst() !== void 0;
433
+ },
434
+ prune: { async prune(before, limit) {
435
+ const doomed = await db.selectFrom("oidc_replay").select(["namespace", "value"]).where("expires_at", "<=", before.getTime()).limit(limit).execute();
436
+ for (const row of doomed) await db.deleteFrom("oidc_replay").where("namespace", "=", row.namespace).where("value", "=", row.value).where("expires_at", "<=", before.getTime()).execute();
437
+ return doomed.length;
438
+ } }.prune
439
+ },
371
440
  identities: {
372
441
  async find(provider, subject) {
373
442
  const row = await db.selectFrom("oidc_identities").selectAll().where("provider", "=", provider).where("subject", "=", subject).executeTakeFirst();
@@ -377,14 +446,7 @@ const kyselyAdapter = (options) => {
377
446
  return (await db.selectFrom("oidc_identities").selectAll().where("account_id", "=", accountId).execute()).map(toIdentity);
378
447
  },
379
448
  async upsert(identity) {
380
- const row = {
381
- provider: identity.provider,
382
- subject: identity.subject,
383
- account_id: identity.accountId,
384
- email: identity.email ?? null,
385
- claims: json(identity.claims),
386
- linked_at: identity.linkedAt.getTime()
387
- };
449
+ const row = identityRow(identity);
388
450
  await db.insertInto("oidc_identities").values(row).onConflict((conflict) => conflict.columns(["provider", "subject"]).doUpdateSet(row)).execute();
389
451
  },
390
452
  async destroy(provider, subject) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oidcraft/kysely",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A Kysely storage adapter for @oidcraft/provider, on Postgres or SQLite.",
5
5
  "license": "MIT",
6
6
  "author": "Fl0r14n <florian.chis@gmail.com>",
@@ -41,11 +41,11 @@
41
41
  "verify:entries": "bun run verify-entries.ts"
42
42
  },
43
43
  "peerDependencies": {
44
- "@oidcraft/adapter": "^0.1.0",
44
+ "@oidcraft/adapter": "^0.2.0",
45
45
  "kysely": ">=0.28"
46
46
  },
47
47
  "devDependencies": {
48
- "@oidcraft/adapter": "^0.1.0",
48
+ "@oidcraft/adapter": "^0.2.0",
49
49
  "@oidcraft/sql": "0.0.0",
50
50
  "@types/pg": "^8.23.1",
51
51
  "kysely": "^0.29.8",