@oidcraft/kysely 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Florian Chiș
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @oidcraft/kysely
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A [Kysely](https://kysely.dev) storage adapter for `@oidcraft/provider`, on SQLite or Postgres.
4
+
5
+ ```sh
6
+ bun add @oidcraft/kysely @oidcraft/adapter kysely
7
+ ```
8
+
9
+ ```ts
10
+ import { staticKeyStore } from '@oidcraft/adapter'
11
+ import { kyselyAdapter } from '@oidcraft/kysely'
12
+
13
+ const adapter = kyselyAdapter({ db, keys: staticKeyStore(keys) }) // db: Kysely<OidcraftDatabase>
14
+ ```
15
+
16
+ Table names are unqualified, so pick a Postgres schema with the search path or `db.withSchema(...)`.
17
+
18
+ The schema ships as migrations, `SQLITE_MIGRATIONS` and `POSTGRES_MIGRATIONS` — the same tables as
19
+ the other SQL adapters, so one database works with any of them — which you run with your own tooling
20
+ before you deploy. The adapter never
21
+ creates or alters a table at startup.
22
+
23
+ It returns a `FullAdapter`, so it satisfies every `@oidcraft/provider` entry point: brokering, local
24
+ accounts and realms included. It passes the shared suite in `@oidcraft/adapter/conformance`, on
25
+ SQLite and Postgres.
26
+
27
+ See the [storage guide](https://github.com/Fl0r14n/oidcraft/blob/main/apps/docs/guide/adapters.md).
@@ -0,0 +1,117 @@
1
+ import { AccountStore, FullAdapter, KeyStore } from "@oidcraft/adapter";
2
+ import { ColumnType, Generated, Kysely } from "kysely";
3
+ /** Milliseconds since the epoch: SQLite has no date type. Postgres keeps them in `bigint`, which it reads back as a string. */
4
+ type Millis = ColumnType<number | string, number, number>;
5
+ export type ClientsTable = {
6
+ client_id: string;
7
+ data: string;
8
+ created_at: Millis;
9
+ updated_at: Millis;
10
+ };
11
+ export type ArtifactsTable = {
12
+ kind: string;
13
+ id: string;
14
+ client_id: string;
15
+ account_id: string | null;
16
+ grant_id: string | null;
17
+ family_id: string | null;
18
+ user_code: string | null;
19
+ payload: string;
20
+ consumed_at: Millis | null;
21
+ expires_at: Millis;
22
+ };
23
+ export type SessionsTable = {
24
+ id: string;
25
+ account_id: string;
26
+ auth_time: Millis;
27
+ acr: string | null;
28
+ amr: string | null;
29
+ idp: string | null;
30
+ upstream_session_id: string | null;
31
+ clients: string;
32
+ expires_at: Millis;
33
+ };
34
+ export type GrantsTable = {
35
+ id: string;
36
+ account_id: string;
37
+ client_id: string;
38
+ scopes: string;
39
+ claims: string;
40
+ resources: string;
41
+ created_at: Millis;
42
+ expires_at: Millis | null;
43
+ };
44
+ export type IdentitiesTable = {
45
+ provider: string;
46
+ subject: string;
47
+ account_id: string;
48
+ email: string | null;
49
+ claims: string;
50
+ linked_at: Millis;
51
+ };
52
+ export type ReplayTable = {
53
+ namespace: string;
54
+ value: string;
55
+ expires_at: Millis;
56
+ };
57
+ export type CredentialsTable = {
58
+ account_id: string;
59
+ username: string;
60
+ password_hash: string;
61
+ claims: string;
62
+ totp_secret: string | null;
63
+ totp_confirmed_at: Millis | null;
64
+ recovery_codes: string | null;
65
+ failed_attempts: number;
66
+ locked_until: Millis | null;
67
+ created_at: Millis;
68
+ updated_at: Millis;
69
+ };
70
+ export type RealmsTable = {
71
+ id: string;
72
+ display_name: string | null;
73
+ /** 0 or 1: SQLite has no boolean. */
74
+ enabled: number;
75
+ settings: string;
76
+ created_at: Millis;
77
+ updated_at: Millis;
78
+ };
79
+ export type OidcraftDatabase = {
80
+ oidc_clients: ClientsTable;
81
+ oidc_artifacts: ArtifactsTable;
82
+ oidc_sessions: SessionsTable;
83
+ oidc_grants: GrantsTable;
84
+ oidc_identities: IdentitiesTable;
85
+ oidc_replay: ReplayTable;
86
+ oidc_credentials: CredentialsTable;
87
+ oidc_realms: RealmsTable;
88
+ };
89
+ type Migration = {
90
+ id: string;
91
+ statements: string[];
92
+ };
93
+ /**
94
+ * The same tables as `SQLITE_MIGRATIONS`, with the same ids, for Postgres (FR-A4). Milliseconds are
95
+ * `bigint` and JSON stays `text`, so every adapter reads and writes exactly what it does on SQLite.
96
+ * Table names are unqualified: they land in whatever schema the host migrates into, which is how a
97
+ * realm gets a schema of its own in one database (G-6).
98
+ */
99
+ export declare const POSTGRES_MIGRATIONS: Migration[];
100
+ /**
101
+ * The SQLite schema every SQL adapter queries, as ordered migrations the host applies with its own
102
+ * tooling. The library never runs them: a library that migrates on import migrates during a rolling
103
+ * deploy, from several processes at once (ARCHITECTURE.md §4.2, FR-A4).
104
+ */
105
+ export declare const SQLITE_MIGRATIONS: Migration[];
106
+ export type KyselyAdapterOptions = {
107
+ db: Kysely<OidcraftDatabase>;
108
+ keys: KeyStore;
109
+ /** Omitted, every account lookup misses: the host composes one in later, as `@oidcraft/provider/accounts` does. */
110
+ accounts?: AccountStore;
111
+ };
112
+ /**
113
+ * The same contract as the Drizzle adapter, on a query builder rather than an ORM (FR-A4). Both run
114
+ * the one conformance suite, which is what makes "interchangeable" checkable rather than asserted.
115
+ */
116
+ export declare const kyselyAdapter: (options: KyselyAdapterOptions) => FullAdapter;
117
+ export type { Generated, Migration };
package/dist/index.mjs ADDED
@@ -0,0 +1,458 @@
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
+ });
144
+ // ../sql/dist/postgres.mjs
145
+ /**
146
+ * The same tables as `SQLITE_MIGRATIONS`, with the same ids, for Postgres (FR-A4). Milliseconds are
147
+ * `bigint` and JSON stays `text`, so every adapter reads and writes exactly what it does on SQLite.
148
+ * Table names are unqualified: they land in whatever schema the host migrates into, which is how a
149
+ * realm gets a schema of its own in one database (G-6).
150
+ */
151
+ const POSTGRES_MIGRATIONS = [
152
+ {
153
+ id: "0001_init",
154
+ statements: [
155
+ "create table oidc_clients (client_id text primary key, data text not null, created_at bigint not null, updated_at bigint not null)",
156
+ "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))",
157
+ "create index oidc_artifacts_grant_idx on oidc_artifacts (grant_id)",
158
+ "create index oidc_artifacts_user_code_idx on oidc_artifacts (user_code)",
159
+ "create index oidc_artifacts_expiry_idx on oidc_artifacts (expires_at)",
160
+ "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)",
161
+ "create index oidc_sessions_account_idx on oidc_sessions (account_id)",
162
+ "create index oidc_sessions_upstream_idx on oidc_sessions (idp, upstream_session_id)",
163
+ "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)",
164
+ "create unique index oidc_grants_account_client_idx on oidc_grants (account_id, client_id)",
165
+ "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))",
166
+ "create table oidc_replay (namespace text not null, value text not null, expires_at bigint not null, primary key (namespace, value))"
167
+ ]
168
+ },
169
+ {
170
+ id: "0002_credentials",
171
+ statements: [
172
+ "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)",
173
+ "create unique index oidc_credentials_username_idx on oidc_credentials (username)",
174
+ "create index oidc_identities_account_idx on oidc_identities (account_id)",
175
+ "create index oidc_replay_expiry_idx on oidc_replay (expires_at)"
176
+ ]
177
+ },
178
+ {
179
+ id: "0003_realms",
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)"]
193
+ }
194
+ ];
195
+ // ../sql/dist/sqlite.mjs
196
+ /**
197
+ * The SQLite schema every SQL adapter queries, as ordered migrations the host applies with its own
198
+ * tooling. The library never runs them: a library that migrates on import migrates during a rolling
199
+ * deploy, from several processes at once (ARCHITECTURE.md §4.2, FR-A4).
200
+ */
201
+ const SQLITE_MIGRATIONS = [
202
+ {
203
+ id: "0001_init",
204
+ statements: [
205
+ "create table oidc_clients (client_id text primary key, data text not null, created_at integer not null, updated_at integer not null)",
206
+ "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))",
207
+ "create index oidc_artifacts_grant_idx on oidc_artifacts (grant_id)",
208
+ "create index oidc_artifacts_user_code_idx on oidc_artifacts (user_code)",
209
+ "create index oidc_artifacts_expiry_idx on oidc_artifacts (expires_at)",
210
+ "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)",
211
+ "create index oidc_sessions_account_idx on oidc_sessions (account_id)",
212
+ "create index oidc_sessions_upstream_idx on oidc_sessions (idp, upstream_session_id)",
213
+ "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)",
214
+ "create unique index oidc_grants_account_client_idx on oidc_grants (account_id, client_id)",
215
+ "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))",
216
+ "create table oidc_replay (namespace text not null, value text not null, expires_at integer not null, primary key (namespace, value))"
217
+ ]
218
+ },
219
+ {
220
+ id: "0002_credentials",
221
+ statements: [
222
+ "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)",
223
+ "create unique index oidc_credentials_username_idx on oidc_credentials (username)",
224
+ "create index oidc_identities_account_idx on oidc_identities (account_id)",
225
+ "create index oidc_replay_expiry_idx on oidc_replay (expires_at)"
226
+ ]
227
+ },
228
+ {
229
+ id: "0003_realms",
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)"]
243
+ }
244
+ ];
245
+ // src/index.ts
246
+ const now = () => Date.now();
247
+ /**
248
+ * The same contract as the Drizzle adapter, on a query builder rather than an ORM (FR-A4). Both run
249
+ * the one conformance suite, which is what makes "interchangeable" checkable rather than asserted.
250
+ */
251
+ const kyselyAdapter = (options) => {
252
+ const { db } = options;
253
+ const artifacts = {
254
+ async upsert(artifact) {
255
+ const row = artifactRow(artifact);
256
+ await db.insertInto("oidc_artifacts").values(row).onConflict((conflict) => conflict.columns(["kind", "id"]).doUpdateSet({
257
+ ...row,
258
+ consumed_at: void 0
259
+ })).execute();
260
+ },
261
+ async find(kind, id) {
262
+ const row = await db.selectFrom("oidc_artifacts").selectAll().where("kind", "=", kind).where("id", "=", id).where("expires_at", ">", now()).executeTakeFirst();
263
+ return row ? toArtifact(row) : void 0;
264
+ },
265
+ async findByUserCode(userCode) {
266
+ const row = await db.selectFrom("oidc_artifacts").selectAll().where("user_code", "=", userCode).where("expires_at", ">", now()).executeTakeFirst();
267
+ return row ? toArtifact(row) : void 0;
268
+ },
269
+ async consume(kind, id) {
270
+ return await db.updateTable("oidc_artifacts").set({ consumed_at: now() }).where("kind", "=", kind).where("id", "=", id).where("consumed_at", "is", null).where("expires_at", ">", now()).returning("id").executeTakeFirst() !== void 0;
271
+ },
272
+ async destroy(kind, id) {
273
+ await db.deleteFrom("oidc_artifacts").where("kind", "=", kind).where("id", "=", id).execute();
274
+ },
275
+ async revokeByGrantId(grantId) {
276
+ await db.deleteFrom("oidc_artifacts").where("grant_id", "=", grantId).execute();
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
+ },
284
+ async prune(before, limit) {
285
+ const doomed = await db.selectFrom("oidc_artifacts").select(["kind", "id"]).where("expires_at", "<=", before.getTime()).limit(limit).execute();
286
+ for (const row of doomed) await artifacts.destroy(row.kind, row.id);
287
+ return doomed.length;
288
+ }
289
+ };
290
+ return {
291
+ clients: {
292
+ async find(clientId) {
293
+ const row = await db.selectFrom("oidc_clients").selectAll().where("client_id", "=", clientId).executeTakeFirst();
294
+ if (!row) return void 0;
295
+ return toClient(row);
296
+ },
297
+ async upsert(client) {
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({
301
+ data,
302
+ updated_at
303
+ })).execute();
304
+ },
305
+ async destroy(clientId) {
306
+ await db.deleteFrom("oidc_clients").where("client_id", "=", clientId).execute();
307
+ },
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
+ };
318
+ }
319
+ },
320
+ artifacts,
321
+ sessions: {
322
+ async find(id) {
323
+ const row = await db.selectFrom("oidc_sessions").selectAll().where("id", "=", id).where("expires_at", ">", now()).executeTakeFirst();
324
+ return row ? toSession(row) : void 0;
325
+ },
326
+ async listForAccount(accountId) {
327
+ return (await db.selectFrom("oidc_sessions").selectAll().where("account_id", "=", accountId).where("expires_at", ">", now()).execute()).map(toSession);
328
+ },
329
+ async findByUpstreamSession(provider, upstreamSessionId) {
330
+ return (await db.selectFrom("oidc_sessions").selectAll().where("idp", "=", provider).where("upstream_session_id", "=", upstreamSessionId).where("expires_at", ">", now()).execute()).map(toSession);
331
+ },
332
+ async upsert(session) {
333
+ const row = sessionRow(session);
334
+ await db.insertInto("oidc_sessions").values(row).onConflict((conflict) => conflict.column("id").doUpdateSet(row)).execute();
335
+ },
336
+ async addClient(id, clientId) {
337
+ for (let attempt = 0; attempt < 64; attempt++) {
338
+ const row = await db.selectFrom("oidc_sessions").select("clients").where("id", "=", id).executeTakeFirst();
339
+ if (!row) return;
340
+ const current = JSON.parse(row.clients);
341
+ if (current.includes(clientId)) return;
342
+ const result = await db.updateTable("oidc_sessions").set({ clients: json([...current, clientId]) }).where("id", "=", id).where("clients", "=", row.clients).executeTakeFirst();
343
+ if (Number(result.numUpdatedRows) > 0) return;
344
+ }
345
+ throw new Error(`session ${id} kept changing; its client list could not be updated`);
346
+ },
347
+ async destroy(id) {
348
+ await db.deleteFrom("oidc_sessions").where("id", "=", id).execute();
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
355
+ },
356
+ grants: {
357
+ async find(id) {
358
+ const row = await db.selectFrom("oidc_grants").selectAll().where("id", "=", id).executeTakeFirst();
359
+ return row ? toGrant(row) : void 0;
360
+ },
361
+ async findByAccountAndClient(accountId, clientId) {
362
+ const row = await db.selectFrom("oidc_grants").selectAll().where("account_id", "=", accountId).where("client_id", "=", clientId).executeTakeFirst();
363
+ return row ? toGrant(row) : void 0;
364
+ },
365
+ async listForAccount(accountId) {
366
+ return (await db.selectFrom("oidc_grants").selectAll().where("account_id", "=", accountId).execute()).map(toGrant);
367
+ },
368
+ async upsert(grant) {
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();
376
+ },
377
+ async destroy(id) {
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();
382
+ }
383
+ },
384
+ accounts: options.accounts ?? noAccounts,
385
+ credentials: {
386
+ async find(accountId) {
387
+ const row = await db.selectFrom("oidc_credentials").selectAll().where("account_id", "=", accountId).executeTakeFirst();
388
+ return row ? toCredential(row) : void 0;
389
+ },
390
+ async findByUsername(username) {
391
+ const row = await db.selectFrom("oidc_credentials").selectAll().where("username", "=", username).executeTakeFirst();
392
+ return row ? toCredential(row) : void 0;
393
+ },
394
+ async create(credential) {
395
+ return await db.insertInto("oidc_credentials").values(credentialRow(credential)).onConflict((conflict) => conflict.doNothing()).returning("account_id").executeTakeFirst() !== void 0;
396
+ },
397
+ async upsert(credential) {
398
+ const row = credentialRow(credential);
399
+ await db.insertInto("oidc_credentials").values(row).onConflict((conflict) => conflict.column("account_id").doUpdateSet(row)).execute();
400
+ },
401
+ async destroy(accountId) {
402
+ await db.deleteFrom("oidc_credentials").where("account_id", "=", accountId).execute();
403
+ }
404
+ },
405
+ realms: {
406
+ async find(id) {
407
+ const row = await db.selectFrom("oidc_realms").selectAll().where("id", "=", id).executeTakeFirst();
408
+ return row ? toRealm(row) : void 0;
409
+ },
410
+ async list() {
411
+ return (await db.selectFrom("oidc_realms").selectAll().execute()).map(toRealm);
412
+ },
413
+ async create(realm) {
414
+ return await db.insertInto("oidc_realms").values(realmRow(realm)).onConflict((conflict) => conflict.doNothing()).returning("id").executeTakeFirst() !== void 0;
415
+ },
416
+ async upsert(realm) {
417
+ const row = realmRow(realm);
418
+ await db.insertInto("oidc_realms").values(row).onConflict((conflict) => conflict.column("id").doUpdateSet(row)).execute();
419
+ },
420
+ async destroy(id) {
421
+ await db.deleteFrom("oidc_realms").where("id", "=", id).execute();
422
+ }
423
+ },
424
+ keys: options.keys,
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
+ },
440
+ identities: {
441
+ async find(provider, subject) {
442
+ const row = await db.selectFrom("oidc_identities").selectAll().where("provider", "=", provider).where("subject", "=", subject).executeTakeFirst();
443
+ return row ? toIdentity(row) : void 0;
444
+ },
445
+ async listForAccount(accountId) {
446
+ return (await db.selectFrom("oidc_identities").selectAll().where("account_id", "=", accountId).execute()).map(toIdentity);
447
+ },
448
+ async upsert(identity) {
449
+ const row = identityRow(identity);
450
+ await db.insertInto("oidc_identities").values(row).onConflict((conflict) => conflict.columns(["provider", "subject"]).doUpdateSet(row)).execute();
451
+ },
452
+ async destroy(provider, subject) {
453
+ await db.deleteFrom("oidc_identities").where("provider", "=", provider).where("subject", "=", subject).execute();
454
+ }
455
+ }
456
+ };
457
+ };
458
+ export { POSTGRES_MIGRATIONS, SQLITE_MIGRATIONS, kyselyAdapter };
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@oidcraft/kysely",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "A Kysely 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/kysely",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/Fl0r14n/oidcraft",
11
+ "directory": "packages/kysely"
12
+ },
13
+ "keywords": [
14
+ "oidc",
15
+ "openid",
16
+ "oauth2",
17
+ "authorization-server",
18
+ "oidcraft",
19
+ "storage",
20
+ "adapter",
21
+ "kysely"
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.2.0",
45
+ "kysely": ">=0.28"
46
+ },
47
+ "devDependencies": {
48
+ "@oidcraft/adapter": "^0.2.0",
49
+ "@oidcraft/sql": "0.0.0",
50
+ "@types/pg": "^8.23.1",
51
+ "kysely": "^0.29.8",
52
+ "kysely-bun-sqlite": "^0.4.0",
53
+ "pg": "^8.23.1"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "engines": {
59
+ "node": ">=24.7"
60
+ }
61
+ }