@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,251 @@
1
+ import { d as E_UNSUPPORTED_RESPONSE_TYPE, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CvxtZvjp.js";
2
+ import { n as ClientService, t as SesameManager } from "./sesame_manager-CeEmlzba.js";
3
+ import { t as TokenService } from "./token_service-DDQ3Dxaj.js";
4
+ import { t as IssueAuthorizationCodeAction } from "./issue_authorization_code-Dr4-bbMp.js";
5
+ import { DateTime } from "luxon";
6
+ import string from "@adonisjs/core/helpers/string";
7
+ import vine from "@vinejs/vine";
8
+ //#region src/actions/authorize.ts
9
+ /**
10
+ * Handles the OAuth 2.0 authorization request business logic.
11
+ *
12
+ * Validates the client, scopes, and PKCE parameters, checks
13
+ * existing consent, and either issues an authorization code
14
+ * or signals that login/consent is required.
15
+ *
16
+ * Errors before redirect_uri validation are thrown as exceptions.
17
+ * Errors after are returned as `redirect_error` results so the
18
+ * controller can redirect back to the client per spec.
19
+ */
20
+ var AuthorizeAction = class {
21
+ /**
22
+ * Process an authorization request. Returns a discriminated
23
+ * union that the controller interprets as the appropriate
24
+ * HTTP redirect.
25
+ */
26
+ async execute(manager, input) {
27
+ if (input.responseType !== "code") throw new E_UNSUPPORTED_RESPONSE_TYPE("Only \"code\" is supported");
28
+ const client = await manager.store.findClient(input.clientId);
29
+ if (!client) throw new E_INVALID_CLIENT("Client not found");
30
+ if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
31
+ if (!client.redirectUris.includes(input.redirectUri)) throw new E_INVALID_REQUEST("Invalid redirect_uri");
32
+ if (!client.grantTypes.includes("authorization_code")) return {
33
+ type: "redirect_error",
34
+ error: "unauthorized_client",
35
+ description: "Client is not allowed to use the authorization_code grant"
36
+ };
37
+ const requestedScopes = input.scope ? input.scope.split(" ") : manager.config.defaultScopes;
38
+ const scopeError = this.#validateScopes(manager, requestedScopes, client);
39
+ if (scopeError) return scopeError;
40
+ const pkceError = this.#validatePkce(input);
41
+ if (pkceError) return pkceError;
42
+ if (!input.userId) return { type: "login_required" };
43
+ return this.#resolveConsent(manager, {
44
+ ...input,
45
+ userId: input.userId
46
+ }, client, requestedScopes);
47
+ }
48
+ /**
49
+ * Validate requested scopes against server config, client
50
+ * permissions, and OIDC availability.
51
+ */
52
+ #validateScopes(manager, scopes, client) {
53
+ const invalidScopes = manager.validateScopes(scopes);
54
+ if (invalidScopes.length > 0) return {
55
+ type: "redirect_error",
56
+ error: "invalid_scope",
57
+ description: `Invalid scopes: ${invalidScopes.join(", ")}`
58
+ };
59
+ const clientService = new ClientService();
60
+ try {
61
+ clientService.validateClientScopes(scopes, client.scopes);
62
+ } catch (err) {
63
+ return {
64
+ type: "redirect_error",
65
+ error: "invalid_scope",
66
+ description: err.message
67
+ };
68
+ }
69
+ if (manager.usesOidcScopes(scopes) && !manager.isOidcEnabled) return {
70
+ type: "redirect_error",
71
+ error: "invalid_scope",
72
+ description: "OIDC scopes require OIDC to be configured (set jwk and oidcProvider in config)"
73
+ };
74
+ return null;
75
+ }
76
+ /**
77
+ * Ensure PKCE code_challenge is present and uses S256
78
+ * (mandatory per OAuth 2.1).
79
+ */
80
+ #validatePkce(input) {
81
+ if (!input.codeChallenge) return {
82
+ type: "redirect_error",
83
+ error: "invalid_request",
84
+ description: "PKCE code_challenge is required"
85
+ };
86
+ if (input.codeChallengeMethod !== "S256") return {
87
+ type: "redirect_error",
88
+ error: "invalid_request",
89
+ description: "Only S256 code_challenge_method is supported"
90
+ };
91
+ return null;
92
+ }
93
+ /**
94
+ * Check if the user has already consented to all requested
95
+ * scopes. If so, issue the code directly. Otherwise, create
96
+ * a pending authorization request for the consent page.
97
+ */
98
+ async #resolveConsent(manager, input, client, scopes) {
99
+ const existingConsent = await manager.store.findConsent({
100
+ clientId: client.clientId,
101
+ userId: input.userId
102
+ });
103
+ if (existingConsent) {
104
+ const consentedSet = new Set(existingConsent.scopes);
105
+ if (scopes.every((s) => consentedSet.has(s))) return {
106
+ type: "authorized",
107
+ code: await new IssueAuthorizationCodeAction().execute(manager, {
108
+ client,
109
+ userId: input.userId,
110
+ scopes,
111
+ redirectUri: input.redirectUri,
112
+ codeChallenge: input.codeChallenge,
113
+ codeChallengeMethod: input.codeChallengeMethod,
114
+ nonce: input.nonce
115
+ })
116
+ };
117
+ }
118
+ return {
119
+ type: "consent_required",
120
+ authToken: await this.#createPendingRequest(manager, input, client.clientId, scopes),
121
+ scopes
122
+ };
123
+ }
124
+ /**
125
+ * Store the authorization request in the database so it can
126
+ * be consumed atomically by the consent controller.
127
+ */
128
+ async #createPendingRequest(manager, input, clientId, scopes) {
129
+ const tokenService = new TokenService(manager);
130
+ const rawToken = tokenService.generateOpaqueToken();
131
+ const ttl = string.seconds.parse(manager.config.authorizationRequestTtl);
132
+ await manager.store.createPendingAuthorizationRequest({
133
+ id: crypto.randomUUID(),
134
+ token: tokenService.hashToken(rawToken),
135
+ userId: input.userId,
136
+ clientId,
137
+ redirectUri: input.redirectUri,
138
+ scopes,
139
+ state: input.state ?? null,
140
+ codeChallenge: input.codeChallenge ?? null,
141
+ codeChallengeMethod: input.codeChallengeMethod ?? null,
142
+ nonce: input.nonce ?? null,
143
+ expiresAt: DateTime.now().plus({ seconds: ttl })
144
+ });
145
+ return rawToken;
146
+ }
147
+ };
148
+ //#endregion
149
+ //#region src/controllers/authorize_controller.ts
150
+ /**
151
+ * Handles the OAuth 2.0 Authorization Endpoint (RFC 6749 §3.1).
152
+ *
153
+ * Validates HTTP parameters, delegates business logic to
154
+ * AuthorizeAction, and interprets the result as redirects.
155
+ *
156
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
157
+ */
158
+ var AuthorizeController = class AuthorizeController {
159
+ static validator = vine.create({
160
+ client_id: vine.string(),
161
+ response_type: vine.string(),
162
+ redirect_uri: vine.string(),
163
+ scope: vine.string().optional(),
164
+ state: vine.string().optional(),
165
+ code_challenge: vine.string().optional(),
166
+ code_challenge_method: vine.string().optional(),
167
+ nonce: vine.string().optional()
168
+ });
169
+ /**
170
+ * Validate the authorization request query params, run the
171
+ * authorize action, and redirect based on the result.
172
+ */
173
+ async handle(ctx) {
174
+ const manager = await ctx.containerResolver.make(SesameManager);
175
+ const [error, query] = await AuthorizeController.validator.tryValidate(ctx.request.qs());
176
+ if (error) throw new E_INVALID_REQUEST("Invalid authorization request parameters");
177
+ await ctx.auth.check();
178
+ const user = ctx.auth.user;
179
+ const result = await new AuthorizeAction().execute(manager, {
180
+ clientId: query.client_id,
181
+ responseType: query.response_type,
182
+ redirectUri: query.redirect_uri,
183
+ scope: query.scope,
184
+ state: query.state,
185
+ codeChallenge: query.code_challenge,
186
+ codeChallengeMethod: query.code_challenge_method,
187
+ nonce: query.nonce,
188
+ userId: user ? String(user.id) : void 0
189
+ });
190
+ if (result.type === "redirect_error") return this.#redirectToClient(ctx, {
191
+ issuer: manager.config.issuer,
192
+ redirectUri: query.redirect_uri,
193
+ state: query.state,
194
+ params: {
195
+ error: result.error,
196
+ error_description: result.description
197
+ }
198
+ });
199
+ if (result.type === "login_required") {
200
+ const params = this.#buildDisplayParams(ctx);
201
+ const url = this.#resolvePageUrl(manager.config.loginPage, ctx, params);
202
+ return ctx.response.redirect().toPath(url);
203
+ }
204
+ if (result.type === "consent_required") {
205
+ const params = this.#buildDisplayParams(ctx);
206
+ params.set("auth_token", result.authToken);
207
+ params.set("scope", result.scopes.join(" "));
208
+ const url = this.#resolvePageUrl(manager.config.consentPage, ctx, params);
209
+ return ctx.response.redirect().toPath(url);
210
+ }
211
+ return this.#redirectToClient(ctx, {
212
+ issuer: manager.config.issuer,
213
+ redirectUri: query.redirect_uri,
214
+ state: query.state,
215
+ params: { code: result.code }
216
+ });
217
+ }
218
+ /**
219
+ * Build a redirect URL with OAuth params, state, and issuer,
220
+ * then redirect the user agent to the client's redirect_uri.
221
+ */
222
+ #redirectToClient(ctx, options) {
223
+ const url = new URL(options.redirectUri);
224
+ for (const [key, value] of Object.entries(options.params)) url.searchParams.set(key, value);
225
+ if (options.state) url.searchParams.set("state", options.state);
226
+ url.searchParams.set("iss", options.issuer);
227
+ return ctx.response.redirect().toPath(url.toString());
228
+ }
229
+ /**
230
+ * Forward original authorize query params so the
231
+ * login/consent page can display them.
232
+ */
233
+ #buildDisplayParams(ctx) {
234
+ const params = new URLSearchParams();
235
+ for (const [key, value] of Object.entries(ctx.request.qs())) {
236
+ if (key === "auth_token" || value == null) continue;
237
+ params.set(key, String(value));
238
+ }
239
+ return params;
240
+ }
241
+ /**
242
+ * Resolve a login/consent page URL from either a static
243
+ * string path or a dynamic function.
244
+ */
245
+ #resolvePageUrl(page, ctx, params) {
246
+ if (typeof page === "function") return page(ctx, params);
247
+ return `${page}?${params.toString()}`;
248
+ }
249
+ };
250
+ //#endregion
251
+ export { AuthorizeController as default };
@@ -1,13 +1,19 @@
1
- import "./decorate-BKZEjPRg.js";
2
- import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CnJ3L8tf.js";
3
- import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
1
+ import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CvxtZvjp.js";
2
+ import { t as SesameManager } from "./sesame_manager-CeEmlzba.js";
4
3
  import vine from "@vinejs/vine";
4
+ //#region src/controllers/client_info_controller.ts
5
+ /**
6
+ * Returns public information about an OAuth client.
7
+ * Used by the consent page to display the client's name
8
+ * from server-side data rather than query parameters
9
+ * (RFC 6819 §4.4.1.4 — prevent client identity spoofing).
10
+ */
5
11
  var ClientInfoController = class ClientInfoController {
6
12
  static validator = vine.create({ client_id: vine.string() });
7
13
  async handle(ctx) {
8
14
  const [error, query] = await ClientInfoController.validator.tryValidate(ctx.request.qs());
9
15
  if (error) throw new E_INVALID_REQUEST("Missing client_id");
10
- const client = await OAuthClient.query().where("clientId", query.client_id).first();
16
+ const client = await (await ctx.containerResolver.make(SesameManager)).store.findClient(query.client_id);
11
17
  if (!client) throw new E_INVALID_CLIENT("Client not found");
12
18
  return {
13
19
  client_id: client.clientId,
@@ -15,4 +21,5 @@ var ClientInfoController = class ClientInfoController {
15
21
  };
16
22
  }
17
23
  };
24
+ //#endregion
18
25
  export { ClientInfoController as default };
@@ -0,0 +1,20 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ import type { CommandOptions } from '@adonisjs/core/types/ace';
3
+ /**
4
+ * Interactively create a new OAuth client.
5
+ *
6
+ * Prompts for name, redirect URIs, client type, and scopes,
7
+ * then outputs the generated client_id and client_secret.
8
+ */
9
+ export default class SesameClient extends BaseCommand {
10
+ static commandName: string;
11
+ static description: string;
12
+ static options: CommandOptions;
13
+ public: boolean;
14
+ name: string;
15
+ redirectUris: string[];
16
+ scopes: string[];
17
+ grantTypes: string[];
18
+ userId: string;
19
+ run(): Promise<void>;
20
+ }
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ /**
3
+ * Generate an RSA JWK key pair for signing OIDC ID tokens.
4
+ */
5
+ export default class SesameKey extends BaseCommand {
6
+ #private;
7
+ static commandName: string;
8
+ static description: string;
9
+ raw: boolean;
10
+ writeEnv: boolean;
11
+ run(): Promise<void | boolean>;
12
+ }
@@ -7,8 +7,6 @@ import type { CommandOptions } from '@adonisjs/core/types/ace';
7
7
  * or `--expired` to target only one category. Expired tokens are
8
8
  * retained for a configurable period (default 168h / 7 days) to
9
9
  * allow for debugging and audit trails.
10
- *
11
- * @see https://datatracker.ietf.org/doc/html/rfc6749
12
10
  */
13
11
  export default class SesamePurge extends BaseCommand {
14
12
  static commandName: string;
@@ -1,7 +1,15 @@
1
- import { t as SesameManager } from "../sesame_manager-DwDZy5Vy.js";
2
- import { t as __decorate } from "../decorate-BKZEjPRg.js";
3
- import "../oauth_access_token-bsoM5KeU.js";
1
+ import { t as SesameManager } from "../sesame_manager-CeEmlzba.js";
2
+ import { t as __decorate } from "../decorate-RQD1h28J.js";
4
3
  import { BaseCommand, flags } from "@adonisjs/core/ace";
4
+ //#region commands/sesame_purge.ts
5
+ /**
6
+ * Purge revoked and/or expired OAuth tokens and authorization codes.
7
+ *
8
+ * By default, purges both revoked and expired records. Use `--revoked`
9
+ * or `--expired` to target only one category. Expired tokens are
10
+ * retained for a configurable period (default 168h / 7 days) to
11
+ * allow for debugging and audit trails.
12
+ */
5
13
  var SesamePurge = class extends BaseCommand {
6
14
  static commandName = "sesame:purge";
7
15
  static description = "Purge revoked and/or expired tokens and authorization codes";
@@ -25,4 +33,5 @@ __decorate([flags.number({
25
33
  description: "Number of hours to retain expired tokens (default: 168 = 7 days)",
26
34
  default: 168
27
35
  })], SesamePurge.prototype, "hours", void 0);
36
+ //#endregion
28
37
  export { SesamePurge as default };
@@ -0,0 +1,31 @@
1
+ import { join } from "node:path";
2
+ //#region configure.ts
3
+ async function configure(command) {
4
+ const codemods = await command.createCodemods();
5
+ const stubsRoot = join(import.meta.dirname, "stubs");
6
+ const store = command.parsedFlags.store ?? "lucid";
7
+ if (store !== "lucid" && store !== "kysely") throw new Error("Invalid Sesame store. Use --store=lucid or --store=kysely");
8
+ const configStub = store === "kysely" ? "config/sesame_kysely.stub" : "config/sesame.stub";
9
+ await codemods.makeUsingStub(stubsRoot, configStub, {});
10
+ const migrationStubs = store === "kysely" ? ["migrations/kysely/create_oauth_tables.stub"] : [
11
+ "migrations/create_oauth_clients_table.stub",
12
+ "migrations/create_oauth_authorization_codes_table.stub",
13
+ "migrations/create_oauth_access_tokens_table.stub",
14
+ "migrations/create_oauth_refresh_tokens_table.stub",
15
+ "migrations/create_oauth_consents_table.stub",
16
+ "migrations/create_oauth_pending_authorization_requests_table.stub"
17
+ ];
18
+ for (const stub of migrationStubs) await codemods.makeUsingStub(stubsRoot, stub, {});
19
+ await codemods.updateRcFile((rcFile) => {
20
+ rcFile.addProvider("@julr/sesame/sesame_provider").addCommand("@julr/sesame/commands");
21
+ });
22
+ await codemods.registerMiddleware("named", [{
23
+ name: "scopes",
24
+ path: "@julr/sesame/scope_middleware"
25
+ }, {
26
+ name: "anyScope",
27
+ path: "@julr/sesame/any_scope_middleware"
28
+ }]);
29
+ }
30
+ //#endregion
31
+ export { configure as t };
@@ -1,25 +1,2 @@
1
- import { join } from "node:path";
2
- async function configure(command) {
3
- const codemods = await command.createCodemods();
4
- const stubsRoot = join(import.meta.url, "./stubs");
5
- await codemods.makeUsingStub(stubsRoot, "config/sesame.stub", {});
6
- for (const stub of [
7
- "migrations/create_oauth_clients_table.stub",
8
- "migrations/create_oauth_authorization_codes_table.stub",
9
- "migrations/create_oauth_access_tokens_table.stub",
10
- "migrations/create_oauth_refresh_tokens_table.stub",
11
- "migrations/create_oauth_consents_table.stub",
12
- "migrations/create_oauth_pending_authorization_requests_table.stub"
13
- ]) await codemods.makeUsingStub(stubsRoot, stub, {});
14
- await codemods.updateRcFile((rcFile) => {
15
- rcFile.addProvider("@julr/sesame/sesame_provider").addCommand("@julr/sesame/commands");
16
- });
17
- await codemods.registerMiddleware("named", [{
18
- name: "scopes",
19
- path: "@julr/sesame/scope_middleware"
20
- }, {
21
- name: "anyScope",
22
- path: "@julr/sesame/any_scope_middleware"
23
- }]);
24
- }
1
+ import { t as configure } from "./configure-t9q3KonY.js";
25
2
  export { configure };
@@ -0,0 +1,96 @@
1
+ import { a as E_INVALID_GRANT, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CvxtZvjp.js";
2
+ import { t as SesameManager } from "./sesame_manager-CeEmlzba.js";
3
+ import { t as TokenService } from "./token_service-DDQ3Dxaj.js";
4
+ import { t as IssueAuthorizationCodeAction } from "./issue_authorization_code-Dr4-bbMp.js";
5
+ import { DateTime } from "luxon";
6
+ import vine from "@vinejs/vine";
7
+ //#region src/controllers/consent_controller.ts
8
+ /**
9
+ * Handles user consent submission for the OAuth authorization flow.
10
+ *
11
+ * When a user approves access, this controller stores or updates
12
+ * the consent record and issues an authorization code via redirect.
13
+ * When the user denies access, it redirects back to the client
14
+ * with an `access_denied` error.
15
+ *
16
+ * Consent records are persisted so that returning users are not
17
+ * prompted again for previously approved scopes.
18
+ *
19
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
20
+ */
21
+ var ConsentController = class ConsentController {
22
+ static validator = vine.create({ auth_token: vine.string() });
23
+ /**
24
+ * Validate the consent submission, consume the pending
25
+ * request, and redirect back to the client with either
26
+ * an authorization code or an access_denied error.
27
+ */
28
+ async handle(ctx) {
29
+ const manager = await ctx.containerResolver.make(SesameManager);
30
+ await ctx.auth.check();
31
+ const user = ctx.auth.user;
32
+ if (!user) throw new E_INVALID_REQUEST("User must be authenticated");
33
+ const [error, body] = await ConsentController.validator.tryValidate(ctx.request.body());
34
+ if (error) throw new E_INVALID_REQUEST("Missing required parameter: auth_token");
35
+ const userId = String(user.id);
36
+ const hashedToken = new TokenService(manager).hashToken(body.auth_token);
37
+ const pendingRequest = await this.#consumePendingRequest(manager, hashedToken, userId);
38
+ if (!pendingRequest) throw new E_INVALID_GRANT("Authorization request not found or expired");
39
+ const client = await manager.store.findClient(pendingRequest.clientId);
40
+ if (!client) throw new E_INVALID_CLIENT("Client not found");
41
+ if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
42
+ if (!client.redirectUris.includes(pendingRequest.redirectUri)) throw new E_INVALID_REQUEST("Invalid redirect_uri");
43
+ if (!ctx.request.body().accept) return this.#redirectWithDenied(ctx, manager, pendingRequest);
44
+ await this.#persistConsent(manager, client.clientId, userId, pendingRequest.scopes);
45
+ const code = await new IssueAuthorizationCodeAction().execute(manager, {
46
+ client,
47
+ userId,
48
+ scopes: pendingRequest.scopes,
49
+ redirectUri: pendingRequest.redirectUri,
50
+ codeChallenge: pendingRequest.codeChallenge ?? void 0,
51
+ codeChallengeMethod: pendingRequest.codeChallengeMethod ?? void 0,
52
+ nonce: pendingRequest.nonce ?? void 0
53
+ });
54
+ const url = new URL(pendingRequest.redirectUri);
55
+ url.searchParams.set("code", code);
56
+ if (pendingRequest.state) url.searchParams.set("state", pendingRequest.state);
57
+ url.searchParams.set("iss", manager.config.issuer);
58
+ return ctx.response.redirect().toPath(url.toString());
59
+ }
60
+ /**
61
+ * Atomically consume a pending authorization request so concurrent
62
+ * consent submissions cannot produce two authorization codes.
63
+ */
64
+ async #consumePendingRequest(manager, hashedToken, userId) {
65
+ return manager.store.consumePendingAuthorizationRequest({
66
+ token: hashedToken,
67
+ userId,
68
+ now: DateTime.now()
69
+ });
70
+ }
71
+ /**
72
+ * Persist or merge consent so future authorization requests
73
+ * for the same client skip the consent screen.
74
+ */
75
+ async #persistConsent(manager, clientId, userId, scopes) {
76
+ await manager.store.grantConsent({
77
+ clientId,
78
+ userId,
79
+ scopes
80
+ });
81
+ }
82
+ /**
83
+ * Redirect back to the client with an access_denied error
84
+ * when the user denies the authorization request.
85
+ */
86
+ #redirectWithDenied(ctx, manager, pendingRequest) {
87
+ const url = new URL(pendingRequest.redirectUri);
88
+ url.searchParams.set("error", "access_denied");
89
+ url.searchParams.set("error_description", "The user denied the authorization request");
90
+ if (pendingRequest.state) url.searchParams.set("state", pendingRequest.state);
91
+ url.searchParams.set("iss", manager.config.issuer);
92
+ return ctx.response.redirect().toPath(url.toString());
93
+ }
94
+ };
95
+ //#endregion
96
+ export { ConsentController as default };
@@ -0,0 +1,41 @@
1
+ //#region src/storage/consent_retry.ts
2
+ const RETRYABLE_CODES = /* @__PURE__ */ new Set([
3
+ "23505",
4
+ "40001",
5
+ "40P01",
6
+ "ER_DUP_ENTRY",
7
+ "ER_LOCK_DEADLOCK",
8
+ "ER_LOCK_WAIT_TIMEOUT",
9
+ "SQLITE_BUSY",
10
+ "SQLITE_BUSY_SNAPSHOT",
11
+ "SQLITE_CONSTRAINT_UNIQUE"
12
+ ]);
13
+ const RETRYABLE_ERRNOS = /* @__PURE__ */ new Set([
14
+ 1062,
15
+ 1205,
16
+ 1213
17
+ ]);
18
+ /**
19
+ * Identify database conflicts that can be retried in a fresh transaction.
20
+ */
21
+ function isRetryableConsentConflict(error) {
22
+ if (!error || typeof error !== "object") return false;
23
+ const candidate = error;
24
+ if (typeof candidate.code === "string" && RETRYABLE_CODES.has(candidate.code)) return true;
25
+ if (typeof candidate.errno === "number" && RETRYABLE_ERRNOS.has(candidate.errno)) return true;
26
+ return candidate.cause ? isRetryableConsentConflict(candidate.cause) : false;
27
+ }
28
+ /**
29
+ * Retry consent writes after uniqueness, serialization, or lock conflicts.
30
+ */
31
+ async function retryConsentConflict(operation) {
32
+ for (let attempt = 1; attempt <= 3; attempt++) try {
33
+ return await operation();
34
+ } catch (error) {
35
+ if (attempt === 3 || !isRetryableConsentConflict(error)) throw error;
36
+ await new Promise((resolve) => setTimeout(resolve, attempt * 5));
37
+ }
38
+ throw new Error("Consent retry limit exceeded");
39
+ }
40
+ //#endregion
41
+ export { retryConsentConflict as t };
@@ -1,15 +1,9 @@
1
- import { column } from "@adonisjs/lucid/orm";
2
- function json(options) {
3
- return column({
4
- ...options,
5
- prepare: (value) => value == null ? null : JSON.stringify(value),
6
- consume: (value) => value == null ? null : typeof value === "string" ? JSON.parse(value) : value
7
- });
8
- }
1
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
9
2
  function __decorate(decorators, target, key, desc) {
10
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
11
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
12
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
13
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
14
7
  }
15
- export { json as n, __decorate as t };
8
+ //#endregion
9
+ export { __decorate as t };
@@ -0,0 +1,54 @@
1
+ import { r as RESERVED_OIDC_CLAIMS } from "./types-DEtn2Zdb.js";
2
+ import { createHash } from "node:crypto";
3
+ import string from "@adonisjs/core/helpers/string";
4
+ //#region src/services/id_token_service.ts
5
+ /**
6
+ * Builds and signs OIDC `id_token` JWTs.
7
+ */
8
+ var IdTokenService = class IdTokenService {
9
+ #manager;
10
+ constructor(manager) {
11
+ this.#manager = manager;
12
+ }
13
+ /**
14
+ * Compute `at_hash` — left half of SHA-256 hash of the access token, base64url-encoded.
15
+ * @see OIDC Core §3.1.3.6
16
+ */
17
+ static computeAtHash(accessToken) {
18
+ const hash = createHash("sha256").update(accessToken).digest();
19
+ return hash.subarray(0, hash.length / 2).toString("base64url");
20
+ }
21
+ /**
22
+ * Filter out reserved claims that the server owns.
23
+ */
24
+ static filterReservedClaims(claims) {
25
+ const filtered = {};
26
+ for (const [key, value] of Object.entries(claims)) if (!RESERVED_OIDC_CLAIMS.has(key)) filtered[key] = value;
27
+ return filtered;
28
+ }
29
+ /**
30
+ * Resolve user OIDC claims by calling `getOidcClaims` if present,
31
+ * then filtering out reserved protocol claims.
32
+ */
33
+ static async resolveUserClaims(user, scopes) {
34
+ const rawClaims = typeof user?.getOidcClaims === "function" ? await user.getOidcClaims(scopes) : {};
35
+ return IdTokenService.filterReservedClaims(rawClaims);
36
+ }
37
+ async sign(options) {
38
+ const now = Math.floor(Date.now() / 1e3);
39
+ const ttlSeconds = string.seconds.parse(this.#manager.config.idTokenTtl);
40
+ const payload = {
41
+ ...await IdTokenService.resolveUserClaims(options.user, options.scopes),
42
+ iss: this.#manager.config.issuer,
43
+ sub: String(options.sub),
44
+ aud: options.clientId,
45
+ iat: now,
46
+ exp: now + ttlSeconds,
47
+ at_hash: IdTokenService.computeAtHash(options.accessToken)
48
+ };
49
+ if (options.nonce) payload.nonce = options.nonce;
50
+ return this.#manager.keyService.sign(payload);
51
+ }
52
+ };
53
+ //#endregion
54
+ export { IdTokenService as t };
package/build/index.d.ts CHANGED
@@ -1,12 +1,6 @@
1
1
  export { configure } from './configure.ts';
2
2
  export { defineConfig } from './src/define_config.ts';
3
+ export { stores } from './src/stores.ts';
3
4
  export { SesameManager } from './src/sesame_manager.ts';
4
- export { OAuthError, E_INVALID_REQUEST, E_INVALID_CLIENT, E_INVALID_GRANT, E_INVALID_SCOPE, E_INVALID_TOKEN, E_UNSUPPORTED_GRANT_TYPE, E_UNSUPPORTED_RESPONSE_TYPE, E_ACCESS_DENIED, E_INVALID_CLIENT_METADATA, E_SERVER_ERROR, E_INSUFFICIENT_SCOPE, } from './src/oauth_error.ts';
5
- export { OAuthClient } from './src/models/oauth_client.ts';
6
- export { OAuthAccessToken } from './src/models/oauth_access_token.ts';
7
- export { OAuthRefreshToken } from './src/models/oauth_refresh_token.ts';
8
- export { OAuthAuthorizationCode } from './src/models/oauth_authorization_code.ts';
9
- export { OAuthConsent } from './src/models/oauth_consent.ts';
10
5
  export { OAuthGuard } from './src/guard/guard.ts';
11
- export { OAuthLucidUserProvider } from './src/guard/user_provider.ts';
12
- export { oauthGuard, oauthUserProvider } from './src/guard/main.ts';
6
+ export { oauthGuard } from './src/guard/main.ts';