@julr/sesame 0.5.1 → 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.
Files changed (97) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +470 -64
  3. package/build/authorize_controller-sCZIjnc-.js +251 -0
  4. package/build/{client_info_controller-BucHGx4u.js → client_info_controller-B5A3tpbk.js} +11 -4
  5. package/build/commands/sesame_client.d.ts +20 -0
  6. package/build/commands/sesame_key.d.ts +12 -0
  7. package/build/commands/sesame_purge.d.ts +0 -2
  8. package/build/commands/sesame_purge.js +12 -3
  9. package/build/configure-t9q3KonY.js +31 -0
  10. package/build/configure.js +1 -24
  11. package/build/consent_controller-ChBboaSd.js +96 -0
  12. package/build/consent_retry-DMkfhmk9.js +41 -0
  13. package/build/{decorate-BKZEjPRg.js → decorate-RQD1h28J.js} +3 -9
  14. package/build/id_token_service-BtBUiL_n.js +54 -0
  15. package/build/index.d.ts +2 -8
  16. package/build/index.js +59 -10
  17. package/build/{introspect_controller-6bRt9sZt.js → introspect_controller-LVYGWhMj.js} +28 -10
  18. package/build/issue_authorization_code-Dr4-bbMp.js +39 -0
  19. package/build/jwks_controller-4HUVS_HC.js +22 -0
  20. package/build/{main-EbeMS5S9.js → main-Dx9kZv07.js} +35 -43
  21. package/build/{metadata_controller-DeaMRnUr.js → metadata_controller-hwfmxlF7.js} +81 -6
  22. package/build/oauth_error-CvxtZvjp.js +189 -0
  23. package/build/providers/sesame_provider.d.ts +1 -1
  24. package/build/providers/sesame_provider.js +17 -6
  25. package/build/{register_controller-sIJ1rxdM.js → register_controller-Cdk3c32j.js} +44 -8
  26. package/build/revoke_controller-Daz4Yqfs.js +48 -0
  27. package/build/services/main.js +3 -3
  28. package/build/sesame_manager-CeEmlzba.js +515 -0
  29. package/build/sesame_manager-DQxvsTEq.js +2 -0
  30. package/build/src/actions/authorize.d.ts +46 -0
  31. package/build/src/actions/exchange_authorization_code.d.ts +34 -0
  32. package/build/src/actions/exchange_client_credentials.d.ts +28 -0
  33. package/build/src/actions/exchange_refresh_token.d.ts +59 -0
  34. package/build/src/actions/issue_authorization_code.d.ts +26 -0
  35. package/build/src/controllers/authorize_controller.d.ts +18 -17
  36. package/build/src/controllers/consent_controller.d.ts +5 -0
  37. package/build/src/controllers/jwks_controller.d.ts +14 -0
  38. package/build/src/controllers/metadata_controller.d.ts +10 -3
  39. package/build/src/controllers/register_controller.d.ts +30 -30
  40. package/build/src/controllers/token_controller.d.ts +8 -5
  41. package/build/src/controllers/userinfo_controller.d.ts +14 -0
  42. package/build/src/guard/guard.d.ts +1 -1
  43. package/build/src/guard/lucid.d.ts +8 -0
  44. package/build/src/guard/lucid.js +46 -0
  45. package/build/src/guard/main.d.ts +2 -9
  46. package/build/src/guard/main.js +2 -6
  47. package/build/src/guard/types.d.ts +0 -9
  48. package/build/src/guard/user_provider.d.ts +9 -1
  49. package/build/src/middleware/any_scope_middleware.js +10 -1
  50. package/build/src/middleware/scope_middleware.js +10 -1
  51. package/build/src/models/oauth_authorization_code.d.ts +1 -0
  52. package/build/src/models/oauth_pending_authorization_request.d.ts +1 -0
  53. package/build/src/oauth_error.d.ts +74 -74
  54. package/build/src/routes.d.ts +3 -1
  55. package/build/src/services/client_service.d.ts +5 -2
  56. package/build/src/services/id_token_service.d.ts +30 -0
  57. package/build/src/services/key_service.d.ts +20 -0
  58. package/build/src/sesame_manager.d.ts +56 -4
  59. package/build/src/storage/consent_retry.d.ts +4 -0
  60. package/build/src/storage/drivers/kysely.d.ts +166 -0
  61. package/build/src/storage/drivers/kysely.js +480 -0
  62. package/build/src/storage/drivers/lucid.d.ts +162 -0
  63. package/build/src/storage/drivers/lucid.js +549 -0
  64. package/build/src/storage/migrations/kysely.d.ts +6 -0
  65. package/build/src/storage/types.d.ts +197 -0
  66. package/build/src/storage/types.js +1 -0
  67. package/build/src/stores.d.ts +21 -0
  68. package/build/src/types.d.ts +120 -0
  69. package/build/src/types.js +2 -0
  70. package/build/stubs/config/sesame.stub +4 -1
  71. package/build/stubs/config/sesame_kysely.stub +35 -0
  72. package/build/stubs/main.ts +5 -0
  73. package/build/stubs/migrations/create_oauth_authorization_codes_table.stub +1 -0
  74. package/build/stubs/migrations/create_oauth_pending_authorization_requests_table.stub +1 -0
  75. package/build/stubs/migrations/create_oauth_refresh_tokens_table.stub +1 -1
  76. package/build/stubs/migrations/kysely/create_oauth_tables.stub +151 -0
  77. package/build/taze.config.d.ts +2 -0
  78. package/build/token_controller-DRatCQNT.js +492 -0
  79. package/build/token_service-DDQ3Dxaj.js +59 -0
  80. package/build/types-DEtn2Zdb.js +75 -0
  81. package/build/userinfo_controller-YwN26wK-.js +37 -0
  82. package/build/vite.config.d.ts +2 -0
  83. package/package.json +55 -54
  84. package/build/authorize_controller-YUfAy-R2.js +0 -138
  85. package/build/client_service-WTNMqWzY.js +0 -65
  86. package/build/consent_controller-Dprwd1ed.js +0 -85
  87. package/build/oauth_access_token-bsoM5KeU.js +0 -18
  88. package/build/oauth_client-BIoY5jBR.js +0 -24
  89. package/build/oauth_error-CnJ3L8tf.js +0 -94
  90. package/build/revoke_controller-D6isoQCi.js +0 -41
  91. package/build/sesame_manager-Bu4MHqZV.js +0 -4
  92. package/build/sesame_manager-DwDZy5Vy.js +0 -167
  93. package/build/src/grants/authorization_code_grant.d.ts +0 -23
  94. package/build/src/grants/client_credentials_grant.d.ts +0 -23
  95. package/build/src/grants/refresh_token_grant.d.ts +0 -27
  96. package/build/token_controller-DzcrLMyS.js +0 -194
  97. package/build/token_service-fhoA4slP.js +0 -31
@@ -0,0 +1,166 @@
1
+ import { type Kysely, type Transaction } from 'kysely';
2
+ import { DateTime } from 'luxon';
3
+ import type { CreateAccessTokenRecord, CreateAuthorizationCodeRecord, CreateClientRecord, CreatePendingAuthorizationRequestRecord, ExchangeAuthorizationCodeOptions, IssueTokenPairOptions, OAuthAccessTokenRecord, OAuthAuthorizationCodeRecord, OAuthClientRecord, OAuthConsentRecord, OAuthPendingAuthorizationRequestRecord, OAuthRefreshTokenRecord, PurgeTokensOptions, RotateRefreshTokenOptions, SesamePurgeResult, SesameStore, UpdateClientRecord } from '../types.js';
4
+ export type KyselyDialect = 'sqlite' | 'postgres' | 'mysql';
5
+ export interface KyselyStoreOptions<DB> {
6
+ db: Kysely<DB>;
7
+ /**
8
+ * Required only when a custom Kysely adapter cannot be identified.
9
+ */
10
+ dialect?: KyselyDialect;
11
+ }
12
+ type DatabaseConnection = Kysely<any> | Transaction<any>;
13
+ /**
14
+ * OAuth persistence backed by a Kysely connection.
15
+ */
16
+ export declare class KyselyStore implements SesameStore {
17
+ #private;
18
+ /**
19
+ * Bind the store to a Kysely connection or an active transaction.
20
+ */
21
+ constructor(db: DatabaseConnection, dialect: KyselyDialect, inTransaction?: boolean);
22
+ /**
23
+ * Return the client registered under a public client ID.
24
+ */
25
+ findClient(clientId: string): Promise<OAuthClientRecord | null>;
26
+ /**
27
+ * List clients in descending creation order, optionally for one user.
28
+ */
29
+ listClients(options?: {
30
+ userId?: string;
31
+ }): Promise<OAuthClientRecord[]>;
32
+ /**
33
+ * Insert a new OAuth client and return its persisted representation.
34
+ */
35
+ createClient(data: CreateClientRecord): Promise<OAuthClientRecord>;
36
+ /**
37
+ * Update the mutable metadata of one OAuth client.
38
+ */
39
+ updateClient(options: {
40
+ id: string;
41
+ data: UpdateClientRecord;
42
+ }): Promise<void>;
43
+ /**
44
+ * Replace a confidential client's hashed secret.
45
+ */
46
+ updateClientSecret(options: {
47
+ id: string;
48
+ secret: string;
49
+ }): Promise<void>;
50
+ /**
51
+ * Delete a client and all OAuth records that refer to it.
52
+ */
53
+ deleteClient(clientId: string): Promise<boolean>;
54
+ /**
55
+ * Find an access token by its hash, optionally restricted to a client.
56
+ */
57
+ findAccessToken(options: {
58
+ hash: string;
59
+ clientId?: string;
60
+ }): Promise<OAuthAccessTokenRecord | null>;
61
+ /**
62
+ * Persist a hashed access token.
63
+ */
64
+ createAccessToken(data: CreateAccessTokenRecord): Promise<void>;
65
+ /**
66
+ * Conditionally revoke an active access token owned by a client.
67
+ */
68
+ revokeAccessToken(options: {
69
+ hash: string;
70
+ clientId: string;
71
+ now: DateTime;
72
+ }): Promise<boolean>;
73
+ /**
74
+ * Find a refresh token by its hash and owning client.
75
+ */
76
+ findRefreshToken(options: {
77
+ hash: string;
78
+ clientId: string;
79
+ }): Promise<OAuthRefreshTokenRecord | null>;
80
+ /**
81
+ * Revoke a refresh token and the access token paired with it atomically.
82
+ */
83
+ revokeRefreshToken(options: {
84
+ hash: string;
85
+ clientId: string;
86
+ now: DateTime;
87
+ }): Promise<void>;
88
+ /**
89
+ * Delete refresh tokens and revoke access tokens after replay detection.
90
+ */
91
+ revokeTokenFamily(options: {
92
+ clientId: string;
93
+ userId: string;
94
+ now: DateTime;
95
+ }): Promise<void>;
96
+ /**
97
+ * Find an authorization code by its hash and owning client.
98
+ */
99
+ findAuthorizationCode(options: {
100
+ code: string;
101
+ clientId: string;
102
+ }): Promise<OAuthAuthorizationCodeRecord | null>;
103
+ /**
104
+ * Persist a hashed, short-lived authorization code.
105
+ */
106
+ createAuthorizationCode(data: CreateAuthorizationCodeRecord): Promise<void>;
107
+ /**
108
+ * Remove an invalid or expired authorization code.
109
+ */
110
+ deleteAuthorizationCode(id: string): Promise<void>;
111
+ /**
112
+ * Consume a code once and issue its token pair in one transaction.
113
+ */
114
+ exchangeAuthorizationCode(options: ExchangeAuthorizationCodeOptions): Promise<boolean>;
115
+ /**
116
+ * Find scopes consented to by a user for a client.
117
+ */
118
+ findConsent(options: {
119
+ clientId: string;
120
+ userId: string;
121
+ }): Promise<OAuthConsentRecord | null>;
122
+ /**
123
+ * Merge newly granted scopes into the user's existing consent.
124
+ */
125
+ grantConsent(options: {
126
+ clientId: string;
127
+ userId: string;
128
+ scopes: string[];
129
+ }): Promise<void>;
130
+ /**
131
+ * Persist the authorization context for the consent redirect.
132
+ */
133
+ createPendingAuthorizationRequest(data: CreatePendingAuthorizationRequestRecord): Promise<void>;
134
+ /**
135
+ * Delete and return an unexpired pending request exactly once.
136
+ */
137
+ consumePendingAuthorizationRequest(options: {
138
+ token: string;
139
+ userId: string;
140
+ now: DateTime;
141
+ }): Promise<OAuthPendingAuthorizationRequestRecord | null>;
142
+ /**
143
+ * Issue an access and refresh token together for grace-period reuse.
144
+ */
145
+ issueTokenPair(options: IssueTokenPairOptions): Promise<void>;
146
+ /**
147
+ * Conditionally rotate a refresh token and replace its access token.
148
+ */
149
+ rotateRefreshToken(options: RotateRefreshTokenOptions): Promise<boolean>;
150
+ /**
151
+ * Revoke tokens and discard codes and consents for a deleted user.
152
+ */
153
+ revokeAllForUser(options: {
154
+ userId: string;
155
+ now: DateTime;
156
+ }): Promise<void>;
157
+ /**
158
+ * Purge revoked and expired records, returning deleted row counts.
159
+ */
160
+ purgeTokens(options: PurgeTokensOptions): Promise<SesamePurgeResult>;
161
+ }
162
+ /**
163
+ * Create a Sesame persistence store backed by an existing Kysely connection.
164
+ */
165
+ export declare function kyselyStore<DB>(options: KyselyStoreOptions<DB>): SesameStore;
166
+ export {};
@@ -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 };