@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/LICENSE +21 -0
- package/README.md +33 -2
- package/dist/index.d.mts +3038 -0
- package/dist/index.mjs +685 -0
- package/package.json +58 -4
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 };
|