@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,515 @@
1
+ import { n as OIDC_SCOPES, t as BUILTIN_SCOPES } from "./types-DEtn2Zdb.js";
2
+ import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE } from "./oauth_error-CvxtZvjp.js";
3
+ import { DateTime } from "luxon";
4
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
5
+ import { SignJWT, importJWK } from "jose";
6
+ //#region src/services/client_service.ts
7
+ /**
8
+ * Handles OAuth client authentication and credential management.
9
+ *
10
+ * Supports the client authentication methods defined in RFC 6749 §2.3:
11
+ * - `client_secret_basic`: HTTP Basic auth with client_id:client_secret
12
+ * - `client_secret_post`: credentials in the request body
13
+ * - `none`: public clients (no secret)
14
+ *
15
+ * Client secrets are stored as SHA-256 hashes and compared using
16
+ * timing-safe equality to prevent timing attacks.
17
+ *
18
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3
19
+ */
20
+ var ClientService = class {
21
+ #manager;
22
+ constructor(manager) {
23
+ this.#manager = manager;
24
+ }
25
+ /**
26
+ * Parse an HTTP Basic Authorization header into client credentials.
27
+ * Follows RFC 6749 §2.3.1 — the client_id and client_secret are
28
+ * URL-decoded after base64 decoding.
29
+ *
30
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
31
+ */
32
+ parseBasicAuth(header) {
33
+ if (!header.startsWith("Basic ")) return null;
34
+ try {
35
+ const decoded = Buffer.from(header.slice(6), "base64").toString("utf-8");
36
+ const colonIndex = decoded.indexOf(":");
37
+ if (colonIndex === -1) return null;
38
+ return {
39
+ clientId: decodeURIComponent(decoded.slice(0, colonIndex)),
40
+ clientSecret: decodeURIComponent(decoded.slice(colonIndex + 1))
41
+ };
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ /**
47
+ * Extract client credentials from a request. Checks the
48
+ * Authorization header first (Basic auth), then falls back
49
+ * to POST body parameters (`client_id` / `client_secret`).
50
+ */
51
+ extractCredentials(options) {
52
+ const basic = options.authorizationHeader ? this.parseBasicAuth(options.authorizationHeader) : null;
53
+ if (basic && options.bodyClientId) throw new E_INVALID_REQUEST("Multiple client authentication methods are not allowed");
54
+ if (basic) return basic;
55
+ if (options.bodyClientId) return {
56
+ clientId: options.bodyClientId,
57
+ clientSecret: options.bodyClientSecret
58
+ };
59
+ return null;
60
+ }
61
+ /**
62
+ * Authenticate a client from request credentials.
63
+ * Extracts credentials, looks up the client in DB, and verifies the secret
64
+ * for confidential clients.
65
+ */
66
+ async authenticateClient(options) {
67
+ const credentials = this.extractCredentials(options);
68
+ if (!credentials) throw new E_INVALID_CLIENT("Client authentication failed");
69
+ if (!this.#manager) throw new Error("ClientService requires SesameManager for authentication");
70
+ const client = await this.#manager.store.findClient(credentials.clientId);
71
+ if (!client || client.isDisabled) throw new E_INVALID_CLIENT("Client authentication failed");
72
+ if (!client.isPublic) {
73
+ if (!credentials.clientSecret || !this.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Client authentication failed");
74
+ }
75
+ return client;
76
+ }
77
+ /**
78
+ * Validate that requested scopes are within the client's
79
+ * allowed scopes. Throws `E_INVALID_SCOPE` if any scope
80
+ * is not permitted. An empty `clientScopes` array means the
81
+ * client has no scope permissions (RFC 6749 §2, §3.3).
82
+ *
83
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
84
+ */
85
+ validateClientScopes(requestedScopes, clientScopes) {
86
+ const nonBuiltinScopes = requestedScopes.filter((s) => !BUILTIN_SCOPES.has(s));
87
+ if (clientScopes.length === 0 && nonBuiltinScopes.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${nonBuiltinScopes.join(", ")}`);
88
+ const allowedSet = new Set(clientScopes);
89
+ const invalid = nonBuiltinScopes.filter((s) => !allowedSet.has(s));
90
+ if (invalid.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${invalid.join(", ")}`);
91
+ }
92
+ /**
93
+ * Hash a client secret for storage using SHA-256
94
+ * (base64url-encoded).
95
+ */
96
+ hashSecret(secret) {
97
+ return createHash("sha256").update(secret).digest("base64url");
98
+ }
99
+ /**
100
+ * Verify a client secret against its stored hash using
101
+ * timing-safe comparison to prevent timing attacks.
102
+ */
103
+ verifySecret(secret, storedHash) {
104
+ const hash = this.hashSecret(secret);
105
+ try {
106
+ return timingSafeEqual(Buffer.from(hash), Buffer.from(storedHash));
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+ /**
112
+ * Generate a random client ID (16 bytes, hex-encoded).
113
+ */
114
+ generateClientId() {
115
+ return randomBytes(16).toString("hex");
116
+ }
117
+ /**
118
+ * Generate a random client secret (32 bytes, base64url-encoded).
119
+ */
120
+ generateClientSecret() {
121
+ return randomBytes(32).toString("base64url");
122
+ }
123
+ };
124
+ //#endregion
125
+ //#region src/services/key_service.ts
126
+ /**
127
+ * Manages the RSA key pair used to sign ID tokens.
128
+ * Accepts a JWK from config, caches the imported private key
129
+ * and public JWK for JWKS export.
130
+ */
131
+ var KeyService = class KeyService {
132
+ #privateKey = null;
133
+ #publicJwk;
134
+ #kid;
135
+ #jwk;
136
+ constructor(jwk) {
137
+ this.#jwk = jwk;
138
+ this.#kid = jwk.kid ?? KeyService.computeKid(jwk);
139
+ this.#publicJwk = {
140
+ kty: jwk.kty,
141
+ n: jwk.n,
142
+ e: jwk.e,
143
+ kid: this.#kid,
144
+ use: "sig",
145
+ alg: "RS256"
146
+ };
147
+ }
148
+ /**
149
+ * Compute a `kid` from public key components (SHA-256, base64url).
150
+ * Same approach as node-oidc-provider.
151
+ */
152
+ static computeKid(jwk) {
153
+ const components = JSON.stringify({
154
+ e: jwk.e,
155
+ kty: jwk.kty,
156
+ n: jwk.n
157
+ });
158
+ return createHash("sha256").update(components).digest("base64url");
159
+ }
160
+ async #getPrivateKey() {
161
+ if (this.#privateKey) return this.#privateKey;
162
+ this.#privateKey = await importJWK(this.#jwk, "RS256");
163
+ return this.#privateKey;
164
+ }
165
+ async sign(payload) {
166
+ const key = await this.#getPrivateKey();
167
+ return new SignJWT(payload).setProtectedHeader({
168
+ alg: "RS256",
169
+ kid: this.#kid,
170
+ typ: "JWT"
171
+ }).sign(key);
172
+ }
173
+ getPublicJwks() {
174
+ return { keys: [this.#publicJwk] };
175
+ }
176
+ get kid() {
177
+ return this.#kid;
178
+ }
179
+ };
180
+ //#endregion
181
+ //#region src/routes.ts
182
+ /**
183
+ * Lazy-loaded controller imports for all OAuth 2.1 endpoints.
184
+ * Using lazy imports ensures controllers are only loaded when
185
+ * their routes are hit.
186
+ */
187
+ const controllers = {
188
+ token: () => import("./token_controller-DRatCQNT.js"),
189
+ authorize: () => import("./authorize_controller-sCZIjnc-.js"),
190
+ consent: () => import("./consent_controller-ChBboaSd.js"),
191
+ introspect: () => import("./introspect_controller-LVYGWhMj.js"),
192
+ revoke: () => import("./revoke_controller-Daz4Yqfs.js"),
193
+ register: () => import("./register_controller-Cdk3c32j.js"),
194
+ metadata: () => import("./metadata_controller-hwfmxlF7.js"),
195
+ clientInfo: () => import("./client_info_controller-B5A3tpbk.js"),
196
+ jwks: () => import("./jwks_controller-4HUVS_HC.js"),
197
+ userinfo: () => import("./userinfo_controller-YwN26wK-.js")
198
+ };
199
+ /**
200
+ * Register OAuth 2.1 endpoint routes on the given router.
201
+ *
202
+ * Paths are relative (no prefix) — the user wraps the call
203
+ * in a `router.group().prefix('/oauth')` to control the mount point.
204
+ *
205
+ * Endpoints registered:
206
+ * - `POST /token` — Token endpoint (RFC 6749 §3.2)
207
+ * - `GET /authorize` — Authorization endpoint (RFC 6749 §3.1)
208
+ * - `POST /consent` — User consent submission
209
+ * - `POST /introspect` — Token introspection (RFC 7662)
210
+ * - `POST /revoke` — Token revocation (RFC 7009)
211
+ * - `POST /register` — Dynamic client registration (RFC 7591)
212
+ * - `GET /client-info` — Public client information (RFC 6819 §4.4.1.4)
213
+ */
214
+ function registerOAuthRoutes(router) {
215
+ router.post("/token", [controllers.token]).as("sesame.token");
216
+ router.get("/authorize", [controllers.authorize]).as("sesame.authorize");
217
+ router.post("/consent", [controllers.consent]).as("sesame.consent");
218
+ router.get("/client-info", [controllers.clientInfo]).as("sesame.clientInfo");
219
+ router.post("/introspect", [controllers.introspect]).as("sesame.introspect");
220
+ router.post("/revoke", [controllers.revoke]).as("sesame.revoke");
221
+ router.post("/register", [controllers.register]).as("sesame.register");
222
+ router.get("/userinfo", [controllers.userinfo]).as("sesame.userinfo");
223
+ router.post("/userinfo", [controllers.userinfo]).as("sesame.userinfo.post");
224
+ }
225
+ /**
226
+ * Register well-known discovery routes at the root level.
227
+ *
228
+ * These must be registered outside any prefix group so they
229
+ * remain at `/.well-known/...`.
230
+ *
231
+ * Endpoints registered:
232
+ * - `GET /.well-known/oauth-authorization-server` — Server metadata (RFC 8414)
233
+ * - `GET /.well-known/openid-configuration` — OpenID Connect discovery
234
+ * - `GET /.well-known/oauth-protected-resource` — Protected resource metadata (RFC 9728)
235
+ */
236
+ function registerWellKnownRoutes(router, options) {
237
+ const jwksPath = options?.jwksPath ?? "/jwks";
238
+ router.get("/.well-known/oauth-authorization-server", [controllers.metadata, "authServer"]).as("sesame.metadata.authServer");
239
+ router.get("/.well-known/openid-configuration", [controllers.metadata, "oidc"]).as("sesame.metadata.oidc");
240
+ router.get("/.well-known/oauth-protected-resource", [controllers.metadata, "protectedResource"]).as("sesame.metadata.protectedResource");
241
+ router.get(jwksPath, [controllers.jwks]).as("sesame.jwks");
242
+ }
243
+ //#endregion
244
+ //#region src/sesame_manager.ts
245
+ /**
246
+ * Central manager for the Sésame OAuth 2.1 server.
247
+ *
248
+ * Holds the resolved configuration. Registered as a singleton
249
+ * in the AdonisJS IoC container by `SesameProvider`.
250
+ */
251
+ var SesameManager = class {
252
+ #config;
253
+ #router;
254
+ #keyService;
255
+ #store;
256
+ constructor(config, router, store) {
257
+ this.#config = config;
258
+ this.#router = router;
259
+ this.#store = store;
260
+ this.#keyService = config.jwk ? new KeyService(config.jwk) : null;
261
+ }
262
+ #publicClient(client) {
263
+ const publicClient = { ...client };
264
+ Object.defineProperty(publicClient, "clientSecret", {
265
+ value: client.clientSecret,
266
+ enumerable: false,
267
+ writable: false
268
+ });
269
+ return publicClient;
270
+ }
271
+ get config() {
272
+ return this.#config;
273
+ }
274
+ get store() {
275
+ return this.#store;
276
+ }
277
+ get keyService() {
278
+ if (!this.#keyService) throw new Error("OIDC requires a JWK. Set the `jwk` option in defineConfig().");
279
+ return this.#keyService;
280
+ }
281
+ get isOidcEnabled() {
282
+ return this.#keyService !== null && this.#config.oidcProvider !== void 0;
283
+ }
284
+ /**
285
+ * Load a user by ID using the configured user provider.
286
+ * Returns the original user model instance, or null if not found.
287
+ */
288
+ async findUserById(userId) {
289
+ if (!this.#config.oidcProvider) return null;
290
+ return (await this.#config.oidcProvider.findById(userId))?.getOriginal() ?? null;
291
+ }
292
+ /**
293
+ * Check if a scope is registered in the server configuration.
294
+ *
295
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
296
+ */
297
+ hasScope(scope) {
298
+ return scope in this.#config.scopes;
299
+ }
300
+ /**
301
+ * Check if the requested scope list uses any OIDC-specific scopes.
302
+ */
303
+ usesOidcScopes(scopes) {
304
+ return scopes.some((scope) => OIDC_SCOPES.has(scope));
305
+ }
306
+ /**
307
+ * Return the list of scopes that are not registered in the
308
+ * server configuration. When no scopes are configured, all
309
+ * requested scopes are considered unknown per RFC 6749 §3.3
310
+ * (`invalid_scope` — "The requested scope is invalid, unknown,
311
+ * or malformed").
312
+ *
313
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
314
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
315
+ */
316
+ validateScopes(scopes) {
317
+ const invalidScopes = /* @__PURE__ */ new Set();
318
+ if (!scopes.includes("openid")) scopes.filter((scope) => scope !== "openid" && OIDC_SCOPES.has(scope)).forEach((scope) => invalidScopes.add(scope));
319
+ if (Object.keys(this.#config.scopes).length === 0) {
320
+ scopes.filter((scope) => !BUILTIN_SCOPES.has(scope) && !OIDC_SCOPES.has(scope)).forEach((scope) => invalidScopes.add(scope));
321
+ return [...invalidScopes];
322
+ }
323
+ scopes.filter((scope) => !BUILTIN_SCOPES.has(scope) && !OIDC_SCOPES.has(scope) && !this.hasScope(scope)).forEach((scope) => invalidScopes.add(scope));
324
+ return [...invalidScopes];
325
+ }
326
+ /**
327
+ * Check if a grant type is enabled in the server configuration.
328
+ */
329
+ isGrantTypeEnabled(grantType) {
330
+ return this.#config.grantTypes.includes(grantType);
331
+ }
332
+ /**
333
+ * Revoke all OAuth artifacts for a given user.
334
+ *
335
+ * Call this when a user is deleted or deactivated to ensure
336
+ * none of their tokens remain usable. Revokes access tokens
337
+ * and refresh tokens, and deletes authorization codes and
338
+ * consent records.
339
+ */
340
+ async revokeAllForUser(userId) {
341
+ const now = DateTime.now();
342
+ await this.#store.revokeAllForUser({
343
+ userId,
344
+ now
345
+ });
346
+ }
347
+ /**
348
+ * Purge revoked and/or expired tokens and authorization codes.
349
+ *
350
+ * Returns the total number of deleted records. Expired tokens are
351
+ * retained for `retentionHours` (default 168 = 7 days) to allow
352
+ * for debugging and audit trails.
353
+ */
354
+ async purgeTokens(options) {
355
+ const revokedOnly = options?.revokedOnly ?? false;
356
+ const expiredOnly = options?.expiredOnly ?? false;
357
+ const retentionHours = options?.retentionHours ?? 168;
358
+ const purgeRevoked = revokedOnly || !expiredOnly;
359
+ const purgeExpired = expiredOnly || !revokedOnly;
360
+ const cutoff = DateTime.now().minus({ hours: retentionHours });
361
+ return this.#store.purgeTokens({
362
+ purgeRevoked,
363
+ purgeExpired,
364
+ cutoff,
365
+ now: DateTime.now()
366
+ });
367
+ }
368
+ /**
369
+ * Create a new OAuth client programmatically.
370
+ * Returns the client and the raw secret (only available at creation time).
371
+ */
372
+ async createClient(options) {
373
+ const clientService = new ClientService();
374
+ const isPublic = options.isPublic ?? false;
375
+ const grantTypes = options.grantTypes ?? ["authorization_code"];
376
+ const scopes = options.scopes ?? this.#config.defaultScopes;
377
+ const clientId = clientService.generateClientId();
378
+ const clientSecret = isPublic ? null : clientService.generateClientSecret();
379
+ const hashedSecret = clientSecret ? clientService.hashSecret(clientSecret) : null;
380
+ const client = await this.#store.createClient({
381
+ id: crypto.randomUUID(),
382
+ clientId,
383
+ clientSecret: hashedSecret,
384
+ name: options.name,
385
+ redirectUris: options.redirectUris,
386
+ scopes,
387
+ grantTypes,
388
+ isPublic,
389
+ isDisabled: false,
390
+ requirePkce: options.requirePkce ?? true,
391
+ type: isPublic ? "public" : "confidential",
392
+ metadata: options.metadata ?? null,
393
+ userId: options.userId ?? null
394
+ });
395
+ return {
396
+ client: this.#publicClient(client),
397
+ clientSecret
398
+ };
399
+ }
400
+ /**
401
+ * Find a client by its public client_id.
402
+ */
403
+ async findClient(clientId) {
404
+ const client = await this.#store.findClient(clientId);
405
+ return client ? this.#publicClient(client) : null;
406
+ }
407
+ /**
408
+ * List all clients, optionally filtered by userId.
409
+ */
410
+ async listClients(options) {
411
+ return (await this.#store.listClients(options)).map((client) => this.#publicClient(client));
412
+ }
413
+ /**
414
+ * Update an existing client by its public client_id.
415
+ * Returns the updated client, or null if not found.
416
+ */
417
+ async updateClient(clientId, options) {
418
+ const store = this.#store;
419
+ const client = await store.findClient(clientId);
420
+ if (!client) return null;
421
+ if (options.name !== void 0) client.name = options.name;
422
+ if (options.redirectUris !== void 0) client.redirectUris = options.redirectUris;
423
+ if (options.scopes !== void 0) client.scopes = options.scopes;
424
+ if (options.grantTypes !== void 0) client.grantTypes = options.grantTypes;
425
+ if (options.isDisabled !== void 0) client.isDisabled = options.isDisabled;
426
+ if (options.requirePkce !== void 0) client.requirePkce = options.requirePkce;
427
+ if (options.metadata !== void 0) client.metadata = options.metadata;
428
+ await store.updateClient({
429
+ id: client.id,
430
+ data: options
431
+ });
432
+ const updated = await store.findClient(clientId);
433
+ return updated ? this.#publicClient(updated) : null;
434
+ }
435
+ /**
436
+ * Delete a client and all its associated tokens, codes, and consents.
437
+ * Returns true if the client was found and deleted.
438
+ */
439
+ async deleteClient(clientId) {
440
+ return this.#store.deleteClient(clientId);
441
+ }
442
+ /**
443
+ * Rotate the secret of a confidential client.
444
+ * Returns the new raw secret, or null if the client is public or not found.
445
+ */
446
+ async rotateClientSecret(clientId) {
447
+ const store = this.#store;
448
+ const client = await store.findClient(clientId);
449
+ if (!client || client.isPublic) return null;
450
+ const clientService = new ClientService();
451
+ const newSecret = clientService.generateClientSecret();
452
+ client.clientSecret = clientService.hashSecret(newSecret);
453
+ await store.updateClientSecret({
454
+ id: client.id,
455
+ secret: client.clientSecret
456
+ });
457
+ return newSecret;
458
+ }
459
+ /**
460
+ * Register OAuth 2.1 endpoint routes (token, authorize, consent, etc.).
461
+ *
462
+ * Paths are relative — wrap the call in a `router.group().prefix()`
463
+ * to control the mount point.
464
+ *
465
+ * Do not apply session-auth middleware to the entire OAuth group.
466
+ * Endpoints like `/token`, `/introspect`, `/revoke`, and `/register`
467
+ * must stay callable without a browser session.
468
+ *
469
+ * @example
470
+ * ```ts
471
+ * router.group(() => {
472
+ * sesame.registerRoutes()
473
+ * }).prefix('/oauth')
474
+ * ```
475
+ */
476
+ registerRoutes() {
477
+ registerOAuthRoutes(this.#router);
478
+ }
479
+ /**
480
+ * Register discovery routes at the root level.
481
+ *
482
+ * Must be called outside any prefix group so endpoints
483
+ * remain at `/.well-known/...`.
484
+ */
485
+ registerDiscoveryRoutes(options) {
486
+ registerWellKnownRoutes(this.#router, options);
487
+ }
488
+ /**
489
+ * @deprecated Use `registerDiscoveryRoutes()` instead.
490
+ */
491
+ registerWellKnownRoutes(options) {
492
+ this.registerDiscoveryRoutes(options);
493
+ }
494
+ /**
495
+ * Register a `/.well-known/oauth-protected-resource` endpoint
496
+ * for a specific resource path (RFC 9728). Useful for MCP
497
+ * servers that need per-resource discovery.
498
+ *
499
+ * @see https://datatracker.ietf.org/doc/html/rfc9728
500
+ */
501
+ registerProtectedResource(options) {
502
+ const wellKnownPath = `/.well-known/oauth-protected-resource${options.resource}`;
503
+ this.#router.get(wellKnownPath, async (ctx) => {
504
+ ctx.response.header("Cache-Control", "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400");
505
+ return {
506
+ resource: `${this.#config.issuer}${options.resource}`,
507
+ authorization_servers: [this.#config.issuer],
508
+ scopes_supported: [...options.scopes ?? Object.keys(this.#config.scopes), ...BUILTIN_SCOPES],
509
+ bearer_methods_supported: ["header"]
510
+ };
511
+ });
512
+ }
513
+ };
514
+ //#endregion
515
+ export { ClientService as n, SesameManager as t };
@@ -0,0 +1,2 @@
1
+ import { t as SesameManager } from "./sesame_manager-CeEmlzba.js";
2
+ export { SesameManager };
@@ -0,0 +1,46 @@
1
+ import type { SesameManager } from '../sesame_manager.ts';
2
+ export interface AuthorizeInput {
3
+ clientId: string;
4
+ responseType: string;
5
+ redirectUri: string;
6
+ scope?: string;
7
+ state?: string;
8
+ codeChallenge?: string;
9
+ codeChallengeMethod?: string;
10
+ nonce?: string;
11
+ userId?: string;
12
+ }
13
+ export type AuthorizeResult = {
14
+ type: 'redirect_error';
15
+ error: string;
16
+ description: string;
17
+ } | {
18
+ type: 'login_required';
19
+ } | {
20
+ type: 'consent_required';
21
+ authToken: string;
22
+ scopes: string[];
23
+ } | {
24
+ type: 'authorized';
25
+ code: string;
26
+ };
27
+ /**
28
+ * Handles the OAuth 2.0 authorization request business logic.
29
+ *
30
+ * Validates the client, scopes, and PKCE parameters, checks
31
+ * existing consent, and either issues an authorization code
32
+ * or signals that login/consent is required.
33
+ *
34
+ * Errors before redirect_uri validation are thrown as exceptions.
35
+ * Errors after are returned as `redirect_error` results so the
36
+ * controller can redirect back to the client per spec.
37
+ */
38
+ export declare class AuthorizeAction {
39
+ #private;
40
+ /**
41
+ * Process an authorization request. Returns a discriminated
42
+ * union that the controller interprets as the appropriate
43
+ * HTTP redirect.
44
+ */
45
+ execute(manager: SesameManager, input: AuthorizeInput): Promise<AuthorizeResult>;
46
+ }
@@ -0,0 +1,34 @@
1
+ import type { SesameManager } from '../sesame_manager.ts';
2
+ import type { OAuthClientRecord } from '../storage/types.ts';
3
+ export interface ExchangeAuthorizationCodeInput {
4
+ client: OAuthClientRecord;
5
+ code: string;
6
+ redirectUri: string;
7
+ codeVerifier: string;
8
+ }
9
+ /**
10
+ * Handle the Authorization Code Grant (RFC 6749 §4.1.3).
11
+ *
12
+ * Exchanges an authorization code for an access token and
13
+ * optionally a refresh token and id_token. Verifies PKCE,
14
+ * validates scopes, and atomically consumes the code to
15
+ * prevent replay.
16
+ *
17
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
18
+ * @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6
19
+ */
20
+ export declare class ExchangeAuthorizationCodeAction {
21
+ #private;
22
+ /**
23
+ * Exchange an authorization code for tokens. The code is
24
+ * consumed atomically inside a transaction.
25
+ */
26
+ execute(manager: SesameManager, input: ExchangeAuthorizationCodeInput): Promise<{
27
+ access_token: string;
28
+ token_type: 'Bearer';
29
+ expires_in: number;
30
+ scope: string;
31
+ refresh_token?: string | undefined;
32
+ id_token?: string | undefined;
33
+ }>;
34
+ }
@@ -0,0 +1,28 @@
1
+ import type { SesameManager } from '../sesame_manager.ts';
2
+ import type { OAuthClientRecord } from '../storage/types.ts';
3
+ export interface ExchangeClientCredentialsInput {
4
+ client: OAuthClientRecord;
5
+ scope?: string;
6
+ }
7
+ /**
8
+ * Handle the Client Credentials Grant (RFC 6749 §4.4).
9
+ *
10
+ * Issues an access token directly to a confidential client
11
+ * for machine-to-machine communication. No refresh token
12
+ * is issued. User-centric OIDC scopes are rejected.
13
+ *
14
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
15
+ */
16
+ export declare class ExchangeClientCredentialsAction {
17
+ #private;
18
+ /**
19
+ * Validate the client, resolve scopes, and issue an
20
+ * access token for M2M usage.
21
+ */
22
+ execute(manager: SesameManager, input: ExchangeClientCredentialsInput): Promise<{
23
+ access_token: string;
24
+ token_type: 'Bearer';
25
+ expires_in: number;
26
+ scope: string;
27
+ }>;
28
+ }
@@ -0,0 +1,59 @@
1
+ import type { SesameManager } from '../sesame_manager.ts';
2
+ import type { OAuthClientRecord } from '../storage/types.ts';
3
+ export interface ExchangeRefreshTokenInput {
4
+ client: OAuthClientRecord;
5
+ refreshToken: string;
6
+ scope?: string;
7
+ }
8
+ /**
9
+ * Handle the Refresh Token Grant (RFC 6749 §6).
10
+ *
11
+ * Exchanges a refresh token for a new access token and a new
12
+ * refresh token (rotation). The old refresh token is revoked
13
+ * immediately after use.
14
+ *
15
+ * ## Replay detection
16
+ *
17
+ * If a revoked refresh token is presented **outside** the grace
18
+ * period, all tokens for that client+user pair are nuked to
19
+ * mitigate stolen-token reuse (RFC 6819 §5.2.2.3, RFC 9700 §4.14.2).
20
+ *
21
+ * ## Grace period (rotation reuse window)
22
+ *
23
+ * OAuth 2.1 requires that rotated refresh tokens be single-use.
24
+ * However, that requirement conflicts with the realities of
25
+ * distributed systems: if the server rotates the token but the
26
+ * client never receives (or persists) the new token — due to a
27
+ * network failure, a concurrent refresh from another process, or
28
+ * a retry after timeout — the client loses its grant permanently.
29
+ *
30
+ * To handle this, we allow a recently-rotated refresh token to be
31
+ * reused within a short configurable window (`refreshTokenRotationGracePeriod`,
32
+ * defaults to 120 s). During that window the old token issues fresh
33
+ * tokens without triggering replay-attack revocation.
34
+ *
35
+ * This is the same approach used by Auth0 ("reuse interval") and
36
+ * Cloudflare workers-oauth-provider ("previous token"). It provides
37
+ * most of the security benefits of strict rotation while remaining
38
+ * reliable for real-world clients (MCP SDK, multi-process CLIs, etc.).
39
+ *
40
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-6
41
+ * @see https://datatracker.ietf.org/doc/html/rfc6819#section-5.2.2.3
42
+ * @see https://datatracker.ietf.org/doc/html/rfc9700#section-4.14.2
43
+ */
44
+ export declare class ExchangeRefreshTokenAction {
45
+ #private;
46
+ /**
47
+ * Rotate the refresh token, issue a new access token,
48
+ * and optionally reissue an id_token if openid scope
49
+ * is present.
50
+ */
51
+ execute(manager: SesameManager, input: ExchangeRefreshTokenInput): Promise<{
52
+ access_token: string;
53
+ token_type: 'Bearer';
54
+ expires_in: number;
55
+ scope: string;
56
+ refresh_token: string;
57
+ id_token?: string | undefined;
58
+ }>;
59
+ }