@julr/sesame 0.6.0 → 0.7.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/README.md +69 -6
- package/build/{authorize_controller-BiycO4be.js → authorize_controller-sCZIjnc-.js} +10 -10
- package/build/{client_info_controller-AcOG8lWu.js → client_info_controller-B5A3tpbk.js} +3 -4
- package/build/commands/sesame_key.d.ts +1 -1
- package/build/commands/sesame_purge.js +2 -5
- package/build/{configure-DkDkIlt8.js → configure-t9q3KonY.js} +7 -3
- package/build/configure.js +1 -2
- package/build/{consent_controller-Dsdhv6-f.js → consent_controller-ChBboaSd.js} +17 -29
- package/build/consent_retry-DMkfhmk9.js +41 -0
- package/build/decorate-RQD1h28J.js +9 -0
- package/build/{id_token_service-CpTzOUDe.js → id_token_service-BtBUiL_n.js} +1 -1
- package/build/index.d.ts +2 -8
- package/build/index.js +38 -9
- package/build/{introspect_controller-DvOp9scr.js → introspect_controller-LVYGWhMj.js} +13 -9
- package/build/{issue_authorization_code-B9ERu1uO.js → issue_authorization_code-Dr4-bbMp.js} +2 -3
- package/build/{jwks_controller-keo4kBZc.js → jwks_controller-4HUVS_HC.js} +1 -5
- package/build/{main-DGBJhq3E.js → main-Dx9kZv07.js} +14 -52
- package/build/{metadata_controller-BVsTo0Gp.js → metadata_controller-hwfmxlF7.js} +4 -6
- package/build/providers/sesame_provider.d.ts +1 -1
- package/build/providers/sesame_provider.js +8 -8
- package/build/{register_controller-gbq7p8a5.js → register_controller-Cdk3c32j.js} +3 -6
- package/build/{revoke_controller-z_ghrEB7.js → revoke_controller-Daz4Yqfs.js} +15 -21
- package/build/services/main.js +1 -5
- package/build/{sesame_manager-DYUSZ0NC.js → sesame_manager-CeEmlzba.js} +78 -256
- package/build/sesame_manager-DQxvsTEq.js +2 -0
- package/build/src/actions/exchange_authorization_code.d.ts +5 -5
- package/build/src/actions/exchange_client_credentials.d.ts +3 -3
- package/build/src/actions/exchange_refresh_token.d.ts +4 -4
- package/build/src/actions/issue_authorization_code.d.ts +2 -2
- package/build/src/controllers/authorize_controller.d.ts +11 -11
- package/build/src/controllers/metadata_controller.d.ts +7 -7
- package/build/src/controllers/register_controller.d.ts +30 -30
- package/build/src/guard/guard.d.ts +1 -1
- package/build/src/guard/lucid.d.ts +8 -0
- package/build/src/guard/lucid.js +46 -0
- package/build/src/guard/main.d.ts +2 -9
- package/build/src/guard/main.js +2 -6
- package/build/src/guard/types.d.ts +0 -9
- package/build/src/guard/user_provider.d.ts +9 -1
- package/build/src/middleware/any_scope_middleware.js +1 -2
- package/build/src/middleware/scope_middleware.js +1 -2
- package/build/src/oauth_error.d.ts +74 -74
- package/build/src/services/client_service.d.ts +5 -2
- package/build/src/sesame_manager.d.ts +6 -5
- package/build/src/storage/consent_retry.d.ts +4 -0
- package/build/src/storage/drivers/kysely.d.ts +166 -0
- package/build/src/storage/drivers/kysely.js +480 -0
- package/build/src/storage/drivers/lucid.d.ts +162 -0
- package/build/src/storage/drivers/lucid.js +549 -0
- package/build/src/storage/migrations/kysely.d.ts +6 -0
- package/build/src/storage/types.d.ts +197 -0
- package/build/src/storage/types.js +1 -0
- package/build/src/stores.d.ts +21 -0
- package/build/src/types.d.ts +9 -1
- package/build/src/types.js +2 -0
- package/build/stubs/config/sesame.stub +4 -1
- package/build/stubs/config/sesame_kysely.stub +35 -0
- package/build/stubs/migrations/kysely/create_oauth_tables.stub +151 -0
- package/build/taze.config.d.ts +2 -0
- package/build/{token_controller-DyI7oy-U.js → token_controller-DRatCQNT.js} +74 -63
- package/build/types-DEtn2Zdb.js +75 -0
- package/build/{userinfo_controller-RLk8cN_o.js → userinfo_controller-YwN26wK-.js} +5 -8
- package/build/vite.config.d.ts +1 -1
- package/package.json +33 -17
- package/build/chunk-DF48asd8.js +0 -9
- package/build/oauth_access_token-Cz_5gNBx.js +0 -29
- package/build/oauth_client-BSanvSql.js +0 -63
- package/build/sesame_manager-B1Jgq1v2.js +0 -6
- /package/build/{oauth_error-C7UhDb2q.js → oauth_error-CvxtZvjp.js} +0 -0
- /package/build/{token_service-DwnfAR9F.js → token_service-DDQ3Dxaj.js} +0 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { t as retryConsentConflict } from "../../../consent_retry-DMkfhmk9.js";
|
|
2
|
+
import { DateTime } from "luxon";
|
|
3
|
+
import { MysqlAdapter, PostgresAdapter, SqliteAdapter } from "kysely";
|
|
4
|
+
//#region src/storage/drivers/kysely.ts
|
|
5
|
+
const tables = {
|
|
6
|
+
clients: {
|
|
7
|
+
name: "oauth_clients",
|
|
8
|
+
json: [
|
|
9
|
+
"redirectUris",
|
|
10
|
+
"scopes",
|
|
11
|
+
"grantTypes",
|
|
12
|
+
"metadata"
|
|
13
|
+
],
|
|
14
|
+
dates: ["createdAt", "updatedAt"],
|
|
15
|
+
booleans: [
|
|
16
|
+
"isPublic",
|
|
17
|
+
"isDisabled",
|
|
18
|
+
"requirePkce"
|
|
19
|
+
],
|
|
20
|
+
nullable: [
|
|
21
|
+
"clientSecret",
|
|
22
|
+
"type",
|
|
23
|
+
"metadata",
|
|
24
|
+
"userId"
|
|
25
|
+
],
|
|
26
|
+
updatedAt: true
|
|
27
|
+
},
|
|
28
|
+
accessTokens: {
|
|
29
|
+
name: "oauth_access_tokens",
|
|
30
|
+
json: ["scopes"],
|
|
31
|
+
dates: [
|
|
32
|
+
"expiresAt",
|
|
33
|
+
"revokedAt",
|
|
34
|
+
"createdAt",
|
|
35
|
+
"updatedAt"
|
|
36
|
+
],
|
|
37
|
+
nullable: ["userId", "revokedAt"],
|
|
38
|
+
updatedAt: true
|
|
39
|
+
},
|
|
40
|
+
refreshTokens: {
|
|
41
|
+
name: "oauth_refresh_tokens",
|
|
42
|
+
json: ["scopes"],
|
|
43
|
+
dates: [
|
|
44
|
+
"expiresAt",
|
|
45
|
+
"revokedAt",
|
|
46
|
+
"createdAt",
|
|
47
|
+
"updatedAt"
|
|
48
|
+
],
|
|
49
|
+
nullable: ["revokedAt"],
|
|
50
|
+
updatedAt: true
|
|
51
|
+
},
|
|
52
|
+
authorizationCodes: {
|
|
53
|
+
name: "oauth_authorization_codes",
|
|
54
|
+
json: ["scopes"],
|
|
55
|
+
dates: [
|
|
56
|
+
"expiresAt",
|
|
57
|
+
"createdAt",
|
|
58
|
+
"updatedAt"
|
|
59
|
+
],
|
|
60
|
+
nullable: [
|
|
61
|
+
"codeChallenge",
|
|
62
|
+
"codeChallengeMethod",
|
|
63
|
+
"nonce"
|
|
64
|
+
],
|
|
65
|
+
updatedAt: true
|
|
66
|
+
},
|
|
67
|
+
consents: {
|
|
68
|
+
name: "oauth_consents",
|
|
69
|
+
json: ["scopes"],
|
|
70
|
+
dates: ["createdAt", "updatedAt"],
|
|
71
|
+
updatedAt: true
|
|
72
|
+
},
|
|
73
|
+
pendingAuthorizationRequests: {
|
|
74
|
+
name: "oauth_pending_authorization_requests",
|
|
75
|
+
json: ["scopes"],
|
|
76
|
+
dates: ["expiresAt", "createdAt"],
|
|
77
|
+
nullable: [
|
|
78
|
+
"state",
|
|
79
|
+
"codeChallenge",
|
|
80
|
+
"codeChallengeMethod",
|
|
81
|
+
"nonce"
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Translate record fields to database column names.
|
|
87
|
+
*/
|
|
88
|
+
function snakeCase(value) {
|
|
89
|
+
return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Translate database column names to record fields.
|
|
93
|
+
*/
|
|
94
|
+
function camelCase(value) {
|
|
95
|
+
return value.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Normalize driver timestamps to Luxon values.
|
|
99
|
+
*/
|
|
100
|
+
function toDateTime(value) {
|
|
101
|
+
if (DateTime.isDateTime(value)) return value;
|
|
102
|
+
if (value instanceof Date) return DateTime.fromJSDate(value);
|
|
103
|
+
if (typeof value === "number") return DateTime.fromMillis(value);
|
|
104
|
+
if (typeof value === "string") {
|
|
105
|
+
const iso = DateTime.fromISO(value, { setZone: true });
|
|
106
|
+
if (iso.isValid) return iso;
|
|
107
|
+
const sql = DateTime.fromSQL(value, { setZone: true });
|
|
108
|
+
if (sql.isValid) return sql;
|
|
109
|
+
}
|
|
110
|
+
throw new TypeError(`Invalid database timestamp: ${String(value)}`);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Encode a field according to its table and SQL dialect.
|
|
114
|
+
*/
|
|
115
|
+
function encodeValue(value, field, shape, dialect) {
|
|
116
|
+
if (value === null || value === void 0) return value;
|
|
117
|
+
if (shape.json.includes(field)) return JSON.stringify(value);
|
|
118
|
+
if (shape.dates.includes(field)) {
|
|
119
|
+
const date = toDateTime(value);
|
|
120
|
+
return dialect === "sqlite" ? date.toUTC().toISO() : date.toJSDate();
|
|
121
|
+
}
|
|
122
|
+
if (shape.booleans?.includes(field)) return dialect === "postgres" ? Boolean(value) : Number(value);
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Decode a field returned by a SQL driver.
|
|
127
|
+
*/
|
|
128
|
+
function decodeValue(value, field, shape) {
|
|
129
|
+
if (value === null || value === void 0) return value;
|
|
130
|
+
if (shape.json.includes(field)) return typeof value === "string" ? JSON.parse(value) : value;
|
|
131
|
+
if (shape.dates.includes(field)) return toDateTime(value);
|
|
132
|
+
if (shape.booleans?.includes(field)) return value === true || value === 1 || value === "1";
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Encode a record for a concrete SQL insert or update.
|
|
137
|
+
*/
|
|
138
|
+
function encodeRow(row, shape, dialect) {
|
|
139
|
+
return Object.fromEntries(Object.entries(row).map(([field, value]) => [snakeCase(field), encodeValue(value, field, shape, dialect)]));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Decode a SQL row into an OAuth persistence record.
|
|
143
|
+
*/
|
|
144
|
+
function decodeRow(row, shape) {
|
|
145
|
+
return Object.fromEntries(Object.entries(row).map(([column, value]) => {
|
|
146
|
+
const field = camelCase(column);
|
|
147
|
+
return [field, decodeValue(value, field, shape)];
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Identify the connection's SQL dialect for value encoding.
|
|
152
|
+
*/
|
|
153
|
+
function inferDialect(db) {
|
|
154
|
+
const adapter = db.getExecutor().adapter;
|
|
155
|
+
if (adapter instanceof SqliteAdapter) return "sqlite";
|
|
156
|
+
if (adapter instanceof PostgresAdapter) return "postgres";
|
|
157
|
+
if (adapter instanceof MysqlAdapter) return "mysql";
|
|
158
|
+
throw new Error("Unknown Kysely dialect. Pass the dialect option to kyselyStore().");
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* OAuth persistence backed by a Kysely connection.
|
|
162
|
+
*/
|
|
163
|
+
var KyselyStore = class KyselyStore {
|
|
164
|
+
#db;
|
|
165
|
+
#dialect;
|
|
166
|
+
#inTransaction;
|
|
167
|
+
/**
|
|
168
|
+
* Bind the store to a Kysely connection or an active transaction.
|
|
169
|
+
*/
|
|
170
|
+
constructor(db, dialect, inTransaction = false) {
|
|
171
|
+
this.#db = db;
|
|
172
|
+
this.#dialect = dialect;
|
|
173
|
+
this.#inTransaction = inTransaction;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Add nullable fields and timestamps before encoding a new record.
|
|
177
|
+
*/
|
|
178
|
+
#newRow(data, shape) {
|
|
179
|
+
const row = { ...data };
|
|
180
|
+
const now = DateTime.now();
|
|
181
|
+
for (const field of shape.nullable ?? []) if (row[field] === void 0) row[field] = null;
|
|
182
|
+
if (!row.createdAt) row.createdAt = now;
|
|
183
|
+
if (shape.updatedAt && !row.updatedAt) row.updatedAt = now;
|
|
184
|
+
return encodeRow(row, shape, this.#dialect);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Execute one OAuth operation in a transaction.
|
|
188
|
+
*/
|
|
189
|
+
#transaction(operation) {
|
|
190
|
+
if (this.#inTransaction) throw new Error("Nested Sesame transactions are not supported");
|
|
191
|
+
return this.#db.transaction().execute(async (trx) => {
|
|
192
|
+
return operation(new KyselyStore(trx, this.#dialect, true));
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Return the client registered under a public client ID.
|
|
197
|
+
*/
|
|
198
|
+
async findClient(clientId) {
|
|
199
|
+
const row = await this.#db.selectFrom(tables.clients.name).selectAll().where("client_id", "=", clientId).executeTakeFirst();
|
|
200
|
+
return row ? decodeRow(row, tables.clients) : null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* List clients in descending creation order, optionally for one user.
|
|
204
|
+
*/
|
|
205
|
+
async listClients(options) {
|
|
206
|
+
let query = this.#db.selectFrom(tables.clients.name).selectAll();
|
|
207
|
+
if (options?.userId) query = query.where("user_id", "=", options.userId);
|
|
208
|
+
return (await query.orderBy("created_at", "desc").execute()).map((row) => decodeRow(row, tables.clients));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Insert a new OAuth client and return its persisted representation.
|
|
212
|
+
*/
|
|
213
|
+
async createClient(data) {
|
|
214
|
+
await this.#db.insertInto(tables.clients.name).values(this.#newRow(data, tables.clients)).execute();
|
|
215
|
+
const client = await this.findClient(data.clientId);
|
|
216
|
+
if (!client) throw new Error("Failed to reload inserted OAuth client");
|
|
217
|
+
return client;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Update the mutable metadata of one OAuth client.
|
|
221
|
+
*/
|
|
222
|
+
async updateClient(options) {
|
|
223
|
+
if (!Object.keys(options.data).length) return;
|
|
224
|
+
await this.#db.updateTable(tables.clients.name).set(encodeRow({
|
|
225
|
+
...options.data,
|
|
226
|
+
updatedAt: DateTime.now()
|
|
227
|
+
}, tables.clients, this.#dialect)).where("id", "=", options.id).execute();
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Replace a confidential client's hashed secret.
|
|
231
|
+
*/
|
|
232
|
+
async updateClientSecret(options) {
|
|
233
|
+
await this.#db.updateTable(tables.clients.name).set(encodeRow({
|
|
234
|
+
clientSecret: options.secret,
|
|
235
|
+
updatedAt: DateTime.now()
|
|
236
|
+
}, tables.clients, this.#dialect)).where("id", "=", options.id).execute();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Delete a client and all OAuth records that refer to it.
|
|
240
|
+
*/
|
|
241
|
+
async deleteClient(clientId) {
|
|
242
|
+
return this.#transaction(async (store) => {
|
|
243
|
+
const db = store.#db;
|
|
244
|
+
await db.deleteFrom(tables.refreshTokens.name).where("client_id", "=", clientId).execute();
|
|
245
|
+
await db.deleteFrom(tables.accessTokens.name).where("client_id", "=", clientId).execute();
|
|
246
|
+
await db.deleteFrom(tables.authorizationCodes.name).where("client_id", "=", clientId).execute();
|
|
247
|
+
await db.deleteFrom(tables.pendingAuthorizationRequests.name).where("client_id", "=", clientId).execute();
|
|
248
|
+
await db.deleteFrom(tables.consents.name).where("client_id", "=", clientId).execute();
|
|
249
|
+
const result = await db.deleteFrom(tables.clients.name).where("client_id", "=", clientId).executeTakeFirst();
|
|
250
|
+
return Number(result.numDeletedRows ?? 0) > 0;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Find an access token by its hash, optionally restricted to a client.
|
|
255
|
+
*/
|
|
256
|
+
async findAccessToken(options) {
|
|
257
|
+
let query = this.#db.selectFrom(tables.accessTokens.name).selectAll().where("token_hash", "=", options.hash);
|
|
258
|
+
if (options.clientId) query = query.where("client_id", "=", options.clientId);
|
|
259
|
+
const row = await query.executeTakeFirst();
|
|
260
|
+
return row ? decodeRow(row, tables.accessTokens) : null;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Persist a hashed access token.
|
|
264
|
+
*/
|
|
265
|
+
async createAccessToken(data) {
|
|
266
|
+
await this.#db.insertInto(tables.accessTokens.name).values(this.#newRow(data, tables.accessTokens)).execute();
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Conditionally revoke an active access token owned by a client.
|
|
270
|
+
*/
|
|
271
|
+
async revokeAccessToken(options) {
|
|
272
|
+
const result = await this.#db.updateTable(tables.accessTokens.name).set(encodeRow({
|
|
273
|
+
revokedAt: options.now,
|
|
274
|
+
updatedAt: options.now
|
|
275
|
+
}, tables.accessTokens, this.#dialect)).where("token_hash", "=", options.hash).where("client_id", "=", options.clientId).where("revoked_at", "is", null).executeTakeFirst();
|
|
276
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Find a refresh token by its hash and owning client.
|
|
280
|
+
*/
|
|
281
|
+
async findRefreshToken(options) {
|
|
282
|
+
const row = await this.#db.selectFrom(tables.refreshTokens.name).selectAll().where("token", "=", options.hash).where("client_id", "=", options.clientId).executeTakeFirst();
|
|
283
|
+
return row ? decodeRow(row, tables.refreshTokens) : null;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Revoke a refresh token and the access token paired with it atomically.
|
|
287
|
+
*/
|
|
288
|
+
async revokeRefreshToken(options) {
|
|
289
|
+
await this.#transaction(async (store) => {
|
|
290
|
+
const token = await store.findRefreshToken(options);
|
|
291
|
+
if (!token || token.revokedAt) return;
|
|
292
|
+
const values = encodeRow({
|
|
293
|
+
revokedAt: options.now,
|
|
294
|
+
updatedAt: options.now
|
|
295
|
+
}, tables.refreshTokens, this.#dialect);
|
|
296
|
+
const result = await store.#db.updateTable(tables.refreshTokens.name).set(values).where("id", "=", token.id).where("revoked_at", "is", null).executeTakeFirst();
|
|
297
|
+
if (Number(result.numUpdatedRows ?? 0) === 0) return;
|
|
298
|
+
await store.#db.updateTable(tables.accessTokens.name).set(encodeRow({
|
|
299
|
+
revokedAt: options.now,
|
|
300
|
+
updatedAt: options.now
|
|
301
|
+
}, tables.accessTokens, this.#dialect)).where("id", "=", token.accessTokenId).where("revoked_at", "is", null).execute();
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Delete refresh tokens and revoke access tokens after replay detection.
|
|
306
|
+
*/
|
|
307
|
+
async revokeTokenFamily(options) {
|
|
308
|
+
await this.#transaction(async (store) => {
|
|
309
|
+
await store.#db.deleteFrom(tables.refreshTokens.name).where("client_id", "=", options.clientId).where("user_id", "=", options.userId).execute();
|
|
310
|
+
await store.#db.updateTable(tables.accessTokens.name).set(encodeRow({
|
|
311
|
+
revokedAt: options.now,
|
|
312
|
+
updatedAt: options.now
|
|
313
|
+
}, tables.accessTokens, this.#dialect)).where("client_id", "=", options.clientId).where("user_id", "=", options.userId).where("revoked_at", "is", null).execute();
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Find an authorization code by its hash and owning client.
|
|
318
|
+
*/
|
|
319
|
+
async findAuthorizationCode(options) {
|
|
320
|
+
const row = await this.#db.selectFrom(tables.authorizationCodes.name).selectAll().where("code", "=", options.code).where("client_id", "=", options.clientId).executeTakeFirst();
|
|
321
|
+
return row ? decodeRow(row, tables.authorizationCodes) : null;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Persist a hashed, short-lived authorization code.
|
|
325
|
+
*/
|
|
326
|
+
async createAuthorizationCode(data) {
|
|
327
|
+
await this.#db.insertInto(tables.authorizationCodes.name).values(this.#newRow(data, tables.authorizationCodes)).execute();
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Remove an invalid or expired authorization code.
|
|
331
|
+
*/
|
|
332
|
+
async deleteAuthorizationCode(id) {
|
|
333
|
+
await this.#db.deleteFrom(tables.authorizationCodes.name).where("id", "=", id).execute();
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Consume a code once and issue its token pair in one transaction.
|
|
337
|
+
*/
|
|
338
|
+
async exchangeAuthorizationCode(options) {
|
|
339
|
+
return this.#transaction(async (store) => {
|
|
340
|
+
const deleted = await store.#db.deleteFrom(tables.authorizationCodes.name).where("id", "=", options.codeId).executeTakeFirst();
|
|
341
|
+
if (Number(deleted.numDeletedRows ?? 0) !== 1) return false;
|
|
342
|
+
await store.createAccessToken(options.accessToken);
|
|
343
|
+
if (options.refreshToken) await store.#db.insertInto(tables.refreshTokens.name).values(store.#newRow(options.refreshToken, tables.refreshTokens)).execute();
|
|
344
|
+
return true;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Find scopes consented to by a user for a client.
|
|
349
|
+
*/
|
|
350
|
+
async findConsent(options) {
|
|
351
|
+
const row = await this.#db.selectFrom(tables.consents.name).selectAll().where("client_id", "=", options.clientId).where("user_id", "=", options.userId).executeTakeFirst();
|
|
352
|
+
return row ? decodeRow(row, tables.consents) : null;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Merge newly granted scopes into the user's existing consent.
|
|
356
|
+
*/
|
|
357
|
+
async grantConsent(options) {
|
|
358
|
+
await retryConsentConflict(() => this.#transaction(async (store) => {
|
|
359
|
+
const query = store.#db.selectFrom(tables.consents.name).selectAll().where("client_id", "=", options.clientId).where("user_id", "=", options.userId);
|
|
360
|
+
const row = await (this.#dialect === "sqlite" ? query : query.forUpdate()).executeTakeFirst();
|
|
361
|
+
if (row) {
|
|
362
|
+
const existing = decodeRow(row, tables.consents);
|
|
363
|
+
const scopes = [.../* @__PURE__ */ new Set([...existing.scopes, ...options.scopes])];
|
|
364
|
+
await store.#db.updateTable(tables.consents.name).set(encodeRow({
|
|
365
|
+
scopes,
|
|
366
|
+
updatedAt: DateTime.now()
|
|
367
|
+
}, tables.consents, this.#dialect)).where("id", "=", existing.id).execute();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
await store.#db.insertInto(tables.consents.name).values(store.#newRow({
|
|
371
|
+
id: crypto.randomUUID(),
|
|
372
|
+
...options
|
|
373
|
+
}, tables.consents)).execute();
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Persist the authorization context for the consent redirect.
|
|
378
|
+
*/
|
|
379
|
+
async createPendingAuthorizationRequest(data) {
|
|
380
|
+
await this.#db.insertInto(tables.pendingAuthorizationRequests.name).values(this.#newRow(data, tables.pendingAuthorizationRequests)).execute();
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Delete and return an unexpired pending request exactly once.
|
|
384
|
+
*/
|
|
385
|
+
async consumePendingAuthorizationRequest(options) {
|
|
386
|
+
return this.#transaction(async (store) => {
|
|
387
|
+
const row = await store.#db.selectFrom(tables.pendingAuthorizationRequests.name).selectAll().where("token", "=", options.token).where("user_id", "=", options.userId).where("expires_at", ">", encodeValue(options.now, "expiresAt", tables.pendingAuthorizationRequests, this.#dialect)).executeTakeFirst();
|
|
388
|
+
if (!row) return null;
|
|
389
|
+
const deleted = await store.#db.deleteFrom(tables.pendingAuthorizationRequests.name).where("id", "=", row.id).where("expires_at", ">", encodeValue(options.now, "expiresAt", tables.pendingAuthorizationRequests, this.#dialect)).executeTakeFirst();
|
|
390
|
+
if (Number(deleted.numDeletedRows ?? 0) !== 1) return null;
|
|
391
|
+
return decodeRow(row, tables.pendingAuthorizationRequests);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Issue an access and refresh token together for grace-period reuse.
|
|
396
|
+
*/
|
|
397
|
+
async issueTokenPair(options) {
|
|
398
|
+
await this.#transaction(async (store) => {
|
|
399
|
+
await store.createAccessToken(options.accessToken);
|
|
400
|
+
await store.#db.insertInto(tables.refreshTokens.name).values(store.#newRow(options.refreshToken, tables.refreshTokens)).execute();
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Conditionally rotate a refresh token and replace its access token.
|
|
405
|
+
*/
|
|
406
|
+
async rotateRefreshToken(options) {
|
|
407
|
+
return this.#transaction(async (store) => {
|
|
408
|
+
const result = await store.#db.updateTable(tables.refreshTokens.name).set(encodeRow({
|
|
409
|
+
revokedAt: options.revokedAt,
|
|
410
|
+
updatedAt: options.revokedAt
|
|
411
|
+
}, tables.refreshTokens, this.#dialect)).where("id", "=", options.oldRefreshTokenId).where("revoked_at", "is", null).executeTakeFirst();
|
|
412
|
+
if (Number(result.numUpdatedRows ?? 0) !== 1) return false;
|
|
413
|
+
await store.#db.updateTable(tables.accessTokens.name).set(encodeRow({
|
|
414
|
+
revokedAt: options.revokedAt,
|
|
415
|
+
updatedAt: options.revokedAt
|
|
416
|
+
}, tables.accessTokens, this.#dialect)).where("id", "=", options.oldAccessTokenId).where("revoked_at", "is", null).execute();
|
|
417
|
+
await store.createAccessToken(options.accessToken);
|
|
418
|
+
await store.#db.insertInto(tables.refreshTokens.name).values(store.#newRow(options.refreshToken, tables.refreshTokens)).execute();
|
|
419
|
+
return true;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Revoke tokens and discard codes and consents for a deleted user.
|
|
424
|
+
*/
|
|
425
|
+
async revokeAllForUser(options) {
|
|
426
|
+
await this.#transaction(async (store) => {
|
|
427
|
+
await store.#db.updateTable(tables.accessTokens.name).set(encodeRow({
|
|
428
|
+
revokedAt: options.now,
|
|
429
|
+
updatedAt: options.now
|
|
430
|
+
}, tables.accessTokens, this.#dialect)).where("user_id", "=", options.userId).where("revoked_at", "is", null).execute();
|
|
431
|
+
await store.#db.updateTable(tables.refreshTokens.name).set(encodeRow({
|
|
432
|
+
revokedAt: options.now,
|
|
433
|
+
updatedAt: options.now
|
|
434
|
+
}, tables.refreshTokens, this.#dialect)).where("user_id", "=", options.userId).where("revoked_at", "is", null).execute();
|
|
435
|
+
await store.#db.deleteFrom(tables.authorizationCodes.name).where("user_id", "=", options.userId).execute();
|
|
436
|
+
await store.#db.deleteFrom(tables.pendingAuthorizationRequests.name).where("user_id", "=", options.userId).execute();
|
|
437
|
+
await store.#db.deleteFrom(tables.consents.name).where("user_id", "=", options.userId).execute();
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Purge revoked and expired records, returning deleted row counts.
|
|
442
|
+
*/
|
|
443
|
+
async purgeTokens(options) {
|
|
444
|
+
return this.#transaction(async (store) => {
|
|
445
|
+
const result = {
|
|
446
|
+
accessTokens: 0,
|
|
447
|
+
refreshTokens: 0,
|
|
448
|
+
authorizationCodes: 0,
|
|
449
|
+
pendingRequests: 0
|
|
450
|
+
};
|
|
451
|
+
const db = store.#db;
|
|
452
|
+
if (options.purgeRevoked) {
|
|
453
|
+
const access = await db.deleteFrom(tables.accessTokens.name).where("revoked_at", "is not", null).executeTakeFirst();
|
|
454
|
+
const refresh = await db.deleteFrom(tables.refreshTokens.name).where("revoked_at", "is not", null).executeTakeFirst();
|
|
455
|
+
result.accessTokens += Number(access.numDeletedRows ?? 0);
|
|
456
|
+
result.refreshTokens += Number(refresh.numDeletedRows ?? 0);
|
|
457
|
+
}
|
|
458
|
+
if (options.purgeExpired) {
|
|
459
|
+
const access = await db.deleteFrom(tables.accessTokens.name).where("expires_at", "<", encodeValue(options.cutoff, "expiresAt", tables.accessTokens, this.#dialect)).where("revoked_at", "is", null).executeTakeFirst();
|
|
460
|
+
const refresh = await db.deleteFrom(tables.refreshTokens.name).where("expires_at", "<", encodeValue(options.cutoff, "expiresAt", tables.refreshTokens, this.#dialect)).where("revoked_at", "is", null).executeTakeFirst();
|
|
461
|
+
const codes = await db.deleteFrom(tables.authorizationCodes.name).where("expires_at", "<", encodeValue(options.cutoff, "expiresAt", tables.authorizationCodes, this.#dialect)).executeTakeFirst();
|
|
462
|
+
result.accessTokens += Number(access.numDeletedRows ?? 0);
|
|
463
|
+
result.refreshTokens += Number(refresh.numDeletedRows ?? 0);
|
|
464
|
+
result.authorizationCodes += Number(codes.numDeletedRows ?? 0);
|
|
465
|
+
}
|
|
466
|
+
const pending = await db.deleteFrom(tables.pendingAuthorizationRequests.name).where("expires_at", "<", encodeValue(options.now, "expiresAt", tables.pendingAuthorizationRequests, this.#dialect)).executeTakeFirst();
|
|
467
|
+
result.pendingRequests = Number(pending.numDeletedRows ?? 0);
|
|
468
|
+
return result;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
/**
|
|
473
|
+
* Create a Sesame persistence store backed by an existing Kysely connection.
|
|
474
|
+
*/
|
|
475
|
+
function kyselyStore(options) {
|
|
476
|
+
const dialect = options.dialect ?? inferDialect(options.db);
|
|
477
|
+
return new KyselyStore(options.db, dialect);
|
|
478
|
+
}
|
|
479
|
+
//#endregion
|
|
480
|
+
export { KyselyStore, kyselyStore };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { DateTime } from 'luxon';
|
|
2
|
+
import { OAuthAccessToken } from '../../models/oauth_access_token.js';
|
|
3
|
+
import { OAuthAuthorizationCode } from '../../models/oauth_authorization_code.js';
|
|
4
|
+
import { OAuthClient } from '../../models/oauth_client.js';
|
|
5
|
+
import { OAuthConsent } from '../../models/oauth_consent.js';
|
|
6
|
+
import { OAuthPendingAuthorizationRequest } from '../../models/oauth_pending_authorization_request.js';
|
|
7
|
+
import { OAuthRefreshToken } from '../../models/oauth_refresh_token.js';
|
|
8
|
+
import type { CreateAccessTokenRecord, CreateAuthorizationCodeRecord, CreateClientRecord, CreatePendingAuthorizationRequestRecord, ExchangeAuthorizationCodeOptions, IssueTokenPairOptions, OAuthAccessTokenRecord, OAuthAuthorizationCodeRecord, OAuthClientRecord, OAuthConsentRecord, OAuthPendingAuthorizationRequestRecord, OAuthRefreshTokenRecord, PurgeTokensOptions, RotateRefreshTokenOptions, SesamePurgeResult, SesameStore, UpdateClientRecord } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Persist OAuth records through the application's Lucid connection.
|
|
11
|
+
*/
|
|
12
|
+
export declare class LucidStore implements SesameStore {
|
|
13
|
+
#private;
|
|
14
|
+
/**
|
|
15
|
+
* Bind this store to a Lucid transaction when one is supplied.
|
|
16
|
+
*/
|
|
17
|
+
constructor(client?: any);
|
|
18
|
+
/**
|
|
19
|
+
* Find a registered client by its public identifier.
|
|
20
|
+
*/
|
|
21
|
+
findClient(clientId: string): Promise<OAuthClientRecord | null>;
|
|
22
|
+
/**
|
|
23
|
+
* List clients newest first, optionally for a single owner.
|
|
24
|
+
*/
|
|
25
|
+
listClients(options?: {
|
|
26
|
+
userId?: string;
|
|
27
|
+
}): Promise<OAuthClientRecord[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Insert a client and return its stored record.
|
|
30
|
+
*/
|
|
31
|
+
createClient(data: CreateClientRecord): Promise<OAuthClientRecord>;
|
|
32
|
+
/**
|
|
33
|
+
* Update only the supplied public client fields.
|
|
34
|
+
*/
|
|
35
|
+
updateClient(options: {
|
|
36
|
+
id: string;
|
|
37
|
+
data: UpdateClientRecord;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Replace a confidential client's stored secret hash.
|
|
41
|
+
*/
|
|
42
|
+
updateClientSecret(options: {
|
|
43
|
+
id: string;
|
|
44
|
+
secret: string;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Delete a client and all its associated OAuth records atomically.
|
|
48
|
+
*/
|
|
49
|
+
deleteClient(clientId: string): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Look up an access-token hash, optionally limited to one client.
|
|
52
|
+
*/
|
|
53
|
+
findAccessToken(options: {
|
|
54
|
+
hash: string;
|
|
55
|
+
clientId?: string;
|
|
56
|
+
}): Promise<OAuthAccessTokenRecord | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Insert a hashed access token.
|
|
59
|
+
*/
|
|
60
|
+
createAccessToken(data: CreateAccessTokenRecord): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Revoke an active access token owned by the requesting client.
|
|
63
|
+
*/
|
|
64
|
+
revokeAccessToken(options: {
|
|
65
|
+
hash: string;
|
|
66
|
+
clientId: string;
|
|
67
|
+
now: DateTime;
|
|
68
|
+
}): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* Look up a refresh-token hash owned by a client.
|
|
71
|
+
*/
|
|
72
|
+
findRefreshToken(options: {
|
|
73
|
+
hash: string;
|
|
74
|
+
clientId: string;
|
|
75
|
+
}): Promise<OAuthRefreshTokenRecord | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Revoke a refresh token and its associated active access token together.
|
|
78
|
+
*/
|
|
79
|
+
revokeRefreshToken(options: {
|
|
80
|
+
hash: string;
|
|
81
|
+
clientId: string;
|
|
82
|
+
now: DateTime;
|
|
83
|
+
}): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Invalidate a client's token family after refresh-token replay.
|
|
86
|
+
*/
|
|
87
|
+
revokeTokenFamily(options: {
|
|
88
|
+
clientId: string;
|
|
89
|
+
userId: string;
|
|
90
|
+
now: DateTime;
|
|
91
|
+
}): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Find an authorization-code hash issued to a client.
|
|
94
|
+
*/
|
|
95
|
+
findAuthorizationCode(options: {
|
|
96
|
+
code: string;
|
|
97
|
+
clientId: string;
|
|
98
|
+
}): Promise<OAuthAuthorizationCodeRecord | null>;
|
|
99
|
+
/**
|
|
100
|
+
* Store a short-lived authorization-code hash.
|
|
101
|
+
*/
|
|
102
|
+
createAuthorizationCode(data: CreateAuthorizationCodeRecord): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Discard an expired or invalid authorization code.
|
|
105
|
+
*/
|
|
106
|
+
deleteAuthorizationCode(id: string): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Consume a code exactly once and issue its tokens in one transaction.
|
|
109
|
+
*/
|
|
110
|
+
exchangeAuthorizationCode(options: ExchangeAuthorizationCodeOptions): Promise<boolean>;
|
|
111
|
+
/**
|
|
112
|
+
* Find a user's current consent for a client.
|
|
113
|
+
*/
|
|
114
|
+
findConsent(options: {
|
|
115
|
+
clientId: string;
|
|
116
|
+
userId: string;
|
|
117
|
+
}): Promise<OAuthConsentRecord | null>;
|
|
118
|
+
/**
|
|
119
|
+
* Merge newly approved scopes into the user's consent.
|
|
120
|
+
*/
|
|
121
|
+
grantConsent(options: {
|
|
122
|
+
clientId: string;
|
|
123
|
+
userId: string;
|
|
124
|
+
scopes: string[];
|
|
125
|
+
}): Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Persist an authorization request awaiting user consent.
|
|
128
|
+
*/
|
|
129
|
+
createPendingAuthorizationRequest(data: CreatePendingAuthorizationRequestRecord): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Return a valid pending request only to the process that deletes it.
|
|
132
|
+
*/
|
|
133
|
+
consumePendingAuthorizationRequest(options: {
|
|
134
|
+
token: string;
|
|
135
|
+
userId: string;
|
|
136
|
+
now: DateTime;
|
|
137
|
+
}): Promise<OAuthPendingAuthorizationRequestRecord | null>;
|
|
138
|
+
/**
|
|
139
|
+
* Persist a new access/refresh pair atomically during the grace period.
|
|
140
|
+
*/
|
|
141
|
+
issueTokenPair(options: IssueTokenPairOptions): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Conditionally consume a refresh token and persist its replacement pair.
|
|
144
|
+
*/
|
|
145
|
+
rotateRefreshToken(options: RotateRefreshTokenOptions): Promise<boolean>;
|
|
146
|
+
/**
|
|
147
|
+
* Revoke a user's tokens and remove their short-lived OAuth records.
|
|
148
|
+
*/
|
|
149
|
+
revokeAllForUser(options: {
|
|
150
|
+
userId: string;
|
|
151
|
+
now: DateTime;
|
|
152
|
+
}): Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* Remove revoked and aged OAuth records and return per-table counts.
|
|
155
|
+
*/
|
|
156
|
+
purgeTokens(options: PurgeTokensOptions): Promise<SesamePurgeResult>;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Create a Sesame store backed by the application's Lucid connection.
|
|
160
|
+
*/
|
|
161
|
+
export declare function lucidStore(): SesameStore;
|
|
162
|
+
export { OAuthAccessToken, OAuthAuthorizationCode, OAuthClient, OAuthConsent, OAuthPendingAuthorizationRequest, OAuthRefreshToken, };
|