@boopkit/cap-oauth-server 0.1.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 (124) hide show
  1. package/capability.json +120 -0
  2. package/dist/authorization-code-service.d.ts +105 -0
  3. package/dist/authorization-code-service.d.ts.map +1 -0
  4. package/dist/authorization-code-service.js +142 -0
  5. package/dist/authorization-code-service.js.map +1 -0
  6. package/dist/bearer.d.ts +146 -0
  7. package/dist/bearer.d.ts.map +1 -0
  8. package/dist/bearer.js +150 -0
  9. package/dist/bearer.js.map +1 -0
  10. package/dist/body-field.d.ts +24 -0
  11. package/dist/body-field.d.ts.map +1 -0
  12. package/dist/body-field.js +43 -0
  13. package/dist/body-field.js.map +1 -0
  14. package/dist/client-registry-service.d.ts +120 -0
  15. package/dist/client-registry-service.d.ts.map +1 -0
  16. package/dist/client-registry-service.js +450 -0
  17. package/dist/client-registry-service.js.map +1 -0
  18. package/dist/config.d.ts +82 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +157 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/connections-controller.d.ts +106 -0
  23. package/dist/connections-controller.d.ts.map +1 -0
  24. package/dist/connections-controller.js +161 -0
  25. package/dist/connections-controller.js.map +1 -0
  26. package/dist/consent-controller.d.ts +197 -0
  27. package/dist/consent-controller.d.ts.map +1 -0
  28. package/dist/consent-controller.js +532 -0
  29. package/dist/consent-controller.js.map +1 -0
  30. package/dist/discovery-controller.d.ts +16 -0
  31. package/dist/discovery-controller.d.ts.map +1 -0
  32. package/dist/discovery-controller.js +52 -0
  33. package/dist/discovery-controller.js.map +1 -0
  34. package/dist/discovery.d.ts +95 -0
  35. package/dist/discovery.d.ts.map +1 -0
  36. package/dist/discovery.js +101 -0
  37. package/dist/discovery.js.map +1 -0
  38. package/dist/errors.d.ts +26 -0
  39. package/dist/errors.d.ts.map +1 -0
  40. package/dist/errors.js +41 -0
  41. package/dist/errors.js.map +1 -0
  42. package/dist/express.d.ts +66 -0
  43. package/dist/express.d.ts.map +1 -0
  44. package/dist/express.js +3 -0
  45. package/dist/express.js.map +1 -0
  46. package/dist/grant-service.d.ts +102 -0
  47. package/dist/grant-service.d.ts.map +1 -0
  48. package/dist/grant-service.js +137 -0
  49. package/dist/grant-service.js.map +1 -0
  50. package/dist/index.d.ts +67 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +148 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/metadata-document.d.ts +60 -0
  55. package/dist/metadata-document.d.ts.map +1 -0
  56. package/dist/metadata-document.js +148 -0
  57. package/dist/metadata-document.js.map +1 -0
  58. package/dist/oauth-error.d.ts +45 -0
  59. package/dist/oauth-error.d.ts.map +1 -0
  60. package/dist/oauth-error.js +51 -0
  61. package/dist/oauth-error.js.map +1 -0
  62. package/dist/pkce.d.ts +29 -0
  63. package/dist/pkce.d.ts.map +1 -0
  64. package/dist/pkce.js +65 -0
  65. package/dist/pkce.js.map +1 -0
  66. package/dist/property-array.d.ts +60 -0
  67. package/dist/property-array.d.ts.map +1 -0
  68. package/dist/property-array.js +31 -0
  69. package/dist/property-array.js.map +1 -0
  70. package/dist/public-id.d.ts +35 -0
  71. package/dist/public-id.d.ts.map +1 -0
  72. package/dist/public-id.js +76 -0
  73. package/dist/public-id.js.map +1 -0
  74. package/dist/rate-limit.d.ts +56 -0
  75. package/dist/rate-limit.d.ts.map +1 -0
  76. package/dist/rate-limit.js +55 -0
  77. package/dist/rate-limit.js.map +1 -0
  78. package/dist/redirect-uri.d.ts +89 -0
  79. package/dist/redirect-uri.d.ts.map +1 -0
  80. package/dist/redirect-uri.js +249 -0
  81. package/dist/redirect-uri.js.map +1 -0
  82. package/dist/registration-controller.d.ts +62 -0
  83. package/dist/registration-controller.d.ts.map +1 -0
  84. package/dist/registration-controller.js +79 -0
  85. package/dist/registration-controller.js.map +1 -0
  86. package/dist/request-client.d.ts +19 -0
  87. package/dist/request-client.d.ts.map +1 -0
  88. package/dist/request-client.js +32 -0
  89. package/dist/request-client.js.map +1 -0
  90. package/dist/revocation-controller.d.ts +28 -0
  91. package/dist/revocation-controller.d.ts.map +1 -0
  92. package/dist/revocation-controller.js +176 -0
  93. package/dist/revocation-controller.js.map +1 -0
  94. package/dist/revocation-service.d.ts +113 -0
  95. package/dist/revocation-service.d.ts.map +1 -0
  96. package/dist/revocation-service.js +121 -0
  97. package/dist/revocation-service.js.map +1 -0
  98. package/dist/token-controller.d.ts +79 -0
  99. package/dist/token-controller.d.ts.map +1 -0
  100. package/dist/token-controller.js +207 -0
  101. package/dist/token-controller.js.map +1 -0
  102. package/dist/token-service.d.ts +175 -0
  103. package/dist/token-service.d.ts.map +1 -0
  104. package/dist/token-service.js +251 -0
  105. package/dist/token-service.js.map +1 -0
  106. package/dist/types.d.ts +130 -0
  107. package/dist/types.d.ts.map +1 -0
  108. package/dist/types.js +13 -0
  109. package/dist/types.js.map +1 -0
  110. package/dist/user-store.d.ts +110 -0
  111. package/dist/user-store.d.ts.map +1 -0
  112. package/dist/user-store.js +40 -0
  113. package/dist/user-store.js.map +1 -0
  114. package/package.json +37 -0
  115. package/scaffold/prisma/schema/oauth-clients.prisma +80 -0
  116. package/scaffold/reference/oauth-server.ref.md +478 -0
  117. package/scaffold/src/middleware/oauth-server.ts +227 -0
  118. package/scaffold/src/scripts/seed-oauth-clients.ts +72 -0
  119. package/scaffold/src/services/oauth-server.service.ts +101 -0
  120. package/scaffold/styles/oauth-server.css +345 -0
  121. package/scaffold/views/oauth-connected-clients.hbs +98 -0
  122. package/scaffold/views/oauth-consent-error.hbs +22 -0
  123. package/scaffold/views/oauth-consent.hbs +138 -0
  124. package/scaffold/views/oauth-disconnect-confirm.hbs +57 -0
@@ -0,0 +1,532 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SCOPE_DESCRIPTIONS_LOCAL = void 0;
4
+ exports.toScopeViews = toScopeViews;
5
+ exports.scopeDescriptionsFrom = scopeDescriptionsFrom;
6
+ exports.warnUndescribedScopes = warnUndescribedScopes;
7
+ exports.provenanceFor = provenanceFor;
8
+ exports.safeLogoUri = safeLogoUri;
9
+ exports.monogramFor = monogramFor;
10
+ exports.createConsentHandlers = createConsentHandlers;
11
+ exports.createConsentRouter = createConsentRouter;
12
+ const express_1 = require("express");
13
+ const pkce_js_1 = require("./pkce.js");
14
+ const redirect_uri_js_1 = require("./redirect-uri.js");
15
+ require("./express.js");
16
+ /**
17
+ * `GET` and `POST /oauth/authorize` — the consent screen.
18
+ *
19
+ * This is the product of the capability. Everything else is plumbing that makes
20
+ * this page's answer enforceable, and the page belongs to the application it is
21
+ * rendered in: it is one of the app's own pages, met at the moment somebody is
22
+ * deciding whether to trust a third party. It is not a protocol console.
23
+ *
24
+ * ## One page, three variants
25
+ *
26
+ * Authenticated, inline login, inline signup — following both donors, because
27
+ * an authorization request is a long query string and round-tripping it through
28
+ * cap-users' `?next=` would mean URL-encoding a URL into a redirect target,
29
+ * which is fragile and is the classic open-redirect shape. The request stays on
30
+ * the page in hidden fields and posts back here.
31
+ *
32
+ * **Signing in is the application's decision, not ours.** Both faces resolve
33
+ * credentials through injected `signIn` and `register` ports rather than
34
+ * through cap-users' `authService`, so an application whose own login admits
35
+ * somebody this page would refuse cannot end up with two doors that disagree
36
+ * about the same password. See `ConsentSignIn` for the failure that produced
37
+ * the ports.
38
+ *
39
+ * **Inline signup does not approve.** Both donors continue straight from a
40
+ * successful inline registration into the approval path, so one button press
41
+ * creates an account *and* grants a third party access to it. Those are two
42
+ * decisions with different consequences, and the second one is the entire point
43
+ * of the page. After signup the consent card is re-rendered in its
44
+ * authenticated variant and a second, explicit Approve is required. The cost is
45
+ * one extra click on the rarest path.
46
+ *
47
+ * ## Two hostile inputs the page is built against
48
+ *
49
+ * **`logo_uri` belongs to a stranger.** Under open dynamic registration the
50
+ * name, description and icon are supplied by an unauthenticated registrant, and
51
+ * they are the entire evidence base for the only decision on the page. So the
52
+ * icon renders inside a **fixed box** whose size is set in CSS and never by the
53
+ * image, an unusable or absent `logo_uri` falls back to a monogram, and the
54
+ * provenance line says in words where the client's claims came from — an
55
+ * operator-configured client can say so, a metadata-document client names the
56
+ * origin its document was fetched from, and a self-registered one says that
57
+ * nothing about it has been verified.
58
+ *
59
+ * **Raw scope strings are not consent.** What `workflows:advance` means is the
60
+ * application's domain knowledge, which the umbrella spec makes explicit — so
61
+ * this capability has no map to ship and no way to invent one. Descriptions
62
+ * come from `app.locals.oauthScopeDescriptions`, published the way
63
+ * cap-oauth-client publishes its provider buttons, and any advertised scope the
64
+ * application did not describe produces a **boot warning naming it**. Asking
65
+ * somebody to approve a string they cannot evaluate is asking a question they
66
+ * are not prepared to answer.
67
+ *
68
+ * ## And the coverage statement
69
+ *
70
+ * The settled data model keys the grant on the *user*, so a client reaches
71
+ * every account that user reaches, present and future. No donor page says
72
+ * anything of the kind, because neither donor had a multi-account model. It
73
+ * goes **before** the Approve button: a person cannot discover afterwards that
74
+ * they approved a connector for accounts they did not have in mind.
75
+ */
76
+ /** The session cookie cap-users sets. Matched so inline login logs in here. */
77
+ const SESSION_COOKIE = 'auth_id';
78
+ const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
79
+ exports.SCOPE_DESCRIPTIONS_LOCAL = 'oauthScopeDescriptions';
80
+ /**
81
+ * Turn a scope list into what a page shows, given the application's vocabulary.
82
+ *
83
+ * Exported because the consent screen and the connected-clients screen must
84
+ * describe the same scope with the same words. A user who approved "See your
85
+ * name and email address" and later reads "profile" on the management screen
86
+ * cannot tell they are looking at the same thing.
87
+ */
88
+ function toScopeViews(scopes, descriptions) {
89
+ return scopes.map((value) => {
90
+ const description = descriptions?.[value];
91
+ const described = typeof description === 'string' && description.trim() !== '';
92
+ return { value, description: described ? description.trim() : value, described };
93
+ });
94
+ }
95
+ /**
96
+ * The application's scope vocabulary, wherever it was published.
97
+ *
98
+ * `app.locals` wins over anything passed at construction, because it is written
99
+ * later and by the application itself. Merging rather than choosing means the
100
+ * two cannot silently disagree: a factory argument that the page ignored used to
101
+ * be enough to silence the boot warning while the page still showed raw strings.
102
+ */
103
+ function scopeDescriptionsFrom(req, defaults) {
104
+ const published = req.app?.locals?.[exports.SCOPE_DESCRIPTIONS_LOCAL];
105
+ if (!published)
106
+ return defaults;
107
+ if (!defaults)
108
+ return published;
109
+ return { ...defaults, ...published };
110
+ }
111
+ /**
112
+ * Warn, once at boot, about every advertised scope the application has not
113
+ * described.
114
+ *
115
+ * Called from the router factory rather than left to the developer to remember,
116
+ * because the failure it prevents is silent: the page renders, the raw string
117
+ * appears where a sentence should be, and nothing anywhere says that a consent
118
+ * screen is asking an unanswerable question.
119
+ */
120
+ function warnUndescribedScopes(scopesSupported, descriptions, warn = (message) => console.warn(`[cap-oauth-server] ${message}`)) {
121
+ const undescribed = scopesSupported.filter((scope) => typeof descriptions?.[scope] !== 'string' || descriptions[scope].trim() === '');
122
+ if (undescribed.length > 0) {
123
+ warn(`no consent-screen description for ${undescribed.join(', ')} — the consent page will show ` +
124
+ `the raw scope string, which asks the user to approve something they cannot evaluate. ` +
125
+ `Set app.locals.${exports.SCOPE_DESCRIPTIONS_LOCAL}.`);
126
+ }
127
+ return undescribed;
128
+ }
129
+ /** What the page says about where this client's claims came from. */
130
+ function provenanceFor(client) {
131
+ const mode = client.mode;
132
+ if (mode === 'static') {
133
+ return 'This application was configured by the people who run this site.';
134
+ }
135
+ if (mode === 'metadata_document') {
136
+ let origin = client.metadataUri ?? client.clientId;
137
+ try {
138
+ origin = new URL(origin).origin;
139
+ }
140
+ catch {
141
+ // Left as the raw string; it is escaped by the template either way.
142
+ }
143
+ return `Its details were published at ${origin}.`;
144
+ }
145
+ return 'This application registered itself. Nothing about it has been verified.';
146
+ }
147
+ /**
148
+ * An icon URL we are willing to put in an `<img src>`, or nothing.
149
+ *
150
+ * `https` only. `http` would break the page's padlock, and `data:` and
151
+ * `javascript:` are a stranger's payload rendered inside our own origin's page.
152
+ * The box's size is fixed in CSS regardless, so this decides only whether there
153
+ * is an image at all — never how much room it gets.
154
+ */
155
+ function safeLogoUri(value) {
156
+ if (typeof value !== 'string' || value.trim() === '')
157
+ return undefined;
158
+ try {
159
+ const url = new URL(value);
160
+ return url.protocol === 'https:' ? url.toString() : undefined;
161
+ }
162
+ catch {
163
+ return undefined;
164
+ }
165
+ }
166
+ /** The letter in the icon box when there is no usable image. */
167
+ function monogramFor(client) {
168
+ // The name first, then the `client_id`, then a question mark. Falling through
169
+ // matters because a registrant supplies the name: `" "` and `"!!!"` are
170
+ // both names a stranger can register, and an empty icon box reads as a broken
171
+ // page rather than as a client with a strange name.
172
+ for (const source of [client.name, client.clientId]) {
173
+ if (typeof source !== 'string')
174
+ continue;
175
+ const first = [...source.trim()].find((ch) => /[\p{L}\p{N}]/u.test(ch));
176
+ if (first)
177
+ return first.toUpperCase();
178
+ }
179
+ return '?';
180
+ }
181
+ /** Errors that must be shown on our own page, because we cannot redirect yet. */
182
+ class ConsentRequestError extends Error {
183
+ constructor(message, status = 400) {
184
+ super(message);
185
+ this.name = 'ConsentRequestError';
186
+ this.status = status;
187
+ }
188
+ }
189
+ /** Errors that RFC 6749 §4.1.2.1 says go back to the client as a redirect. */
190
+ class RedirectableError extends Error {
191
+ constructor(code, message) {
192
+ super(message);
193
+ this.name = 'RedirectableError';
194
+ this.code = code;
195
+ }
196
+ }
197
+ function first(value) {
198
+ if (typeof value === 'string') {
199
+ const trimmed = value.trim();
200
+ return trimmed === '' ? undefined : trimmed;
201
+ }
202
+ // Express hands back an array for a repeated parameter. A repeated
203
+ // `redirect_uri` is not a request we should try to interpret.
204
+ return undefined;
205
+ }
206
+ function parseScopeList(raw) {
207
+ if (!raw)
208
+ return [];
209
+ return [...new Set(raw.split(/\s+/).filter(Boolean))];
210
+ }
211
+ function createConsentHandlers(deps) {
212
+ const { clientRegistry, authorizationCodeService, signIn, register, scopesSupported } = deps;
213
+ const viewName = deps.viewName ?? 'oauth-consent';
214
+ const layout = deps.layout ?? 'main';
215
+ const actionPath = deps.actionPath ?? '/oauth/authorize';
216
+ /**
217
+ * Resolve the client and the redirect URI *before* anything else is judged.
218
+ *
219
+ * The order is RFC 6749 §4.1.2.1's and it is not a style choice: until both
220
+ * are known good there is nowhere safe to send an error, so every other
221
+ * failure has to be reported on our own page. Once they are, every remaining
222
+ * failure goes back to the client as a redirect, because a client left with
223
+ * no answer hangs until its own timeout while the user believes they are done.
224
+ */
225
+ async function resolveTarget(source) {
226
+ const clientId = first(source.client_id);
227
+ if (!clientId)
228
+ throw new ConsentRequestError('client_id is required');
229
+ let client = await clientRegistry.findByClientId(clientId);
230
+ if (!client && /^https:\/\//i.test(clientId)) {
231
+ // A Client ID Metadata Document client is allowed to arrive unseen: the
232
+ // document at its own URL is its registration. A fetch failure here is a
233
+ // refusal rather than a 500, because the URL was supplied by the caller.
234
+ try {
235
+ client = await clientRegistry.resolveMetadataDocument(clientId);
236
+ }
237
+ catch {
238
+ client = null;
239
+ }
240
+ }
241
+ if (!client)
242
+ throw new ConsentRequestError('unknown client_id');
243
+ if (client.status !== 'enabled')
244
+ throw new ConsentRequestError('this client is suspended', 403);
245
+ const redirectUri = first(source.redirect_uri);
246
+ if (!redirectUri)
247
+ throw new ConsentRequestError('redirect_uri is required');
248
+ if (!(0, redirect_uri_js_1.matchesAnyRedirectUri)(client.redirectUris, redirectUri)) {
249
+ // Never redirect to an unregistered URI to report that it is
250
+ // unregistered. That is the open redirect.
251
+ throw new ConsentRequestError('redirect_uri is not registered for this client');
252
+ }
253
+ return { client, redirectUri };
254
+ }
255
+ /** Everything that can be answered with a redirect, once we may redirect. */
256
+ function parseRequest(source, client) {
257
+ const responseType = first(source.response_type);
258
+ if (responseType !== 'code') {
259
+ throw new RedirectableError('unsupported_response_type', 'this server issues authorization codes; response_type must be "code"');
260
+ }
261
+ const codeChallengeMethod = first(source.code_challenge_method) ?? pkce_js_1.CODE_CHALLENGE_METHOD;
262
+ if (codeChallengeMethod !== pkce_js_1.CODE_CHALLENGE_METHOD) {
263
+ throw new RedirectableError('invalid_request', `code_challenge_method must be ${pkce_js_1.CODE_CHALLENGE_METHOD}`);
264
+ }
265
+ const codeChallenge = first(source.code_challenge);
266
+ if (!(0, pkce_js_1.isValidCodeChallenge)(codeChallenge)) {
267
+ // Required, not optional. OAuth 2.1 requires PKCE for public clients and
268
+ // every client this server will ever see is public.
269
+ throw new RedirectableError('invalid_request', 'a valid S256 code_challenge is required');
270
+ }
271
+ const scopes = parseScopeList(first(source.scope));
272
+ const unknown = scopes.filter((scope) => !scopesSupported.includes(scope));
273
+ if (unknown.length > 0) {
274
+ throw new RedirectableError('invalid_scope', `not supported here: ${unknown.join(' ')}`);
275
+ }
276
+ const clientScopes = client.scopes;
277
+ const disallowed = clientScopes.length > 0 ? scopes.filter((scope) => !clientScopes.includes(scope)) : [];
278
+ if (disallowed.length > 0) {
279
+ throw new RedirectableError('invalid_scope', `this client is not registered for: ${disallowed.join(' ')}`);
280
+ }
281
+ return {
282
+ responseType,
283
+ clientId: client.clientId,
284
+ redirectUri: first(source.redirect_uri),
285
+ scopes: scopes.length > 0 ? scopes : [...clientScopes],
286
+ state: first(source.state),
287
+ codeChallenge,
288
+ codeChallengeMethod,
289
+ resource: first(source.resource),
290
+ };
291
+ }
292
+ /** Build the redirect back to the client, error or success. */
293
+ function redirectTo(res, redirectUri, params) {
294
+ const url = new URL(redirectUri);
295
+ for (const [key, value] of Object.entries(params)) {
296
+ if (value !== undefined)
297
+ url.searchParams.set(key, value);
298
+ }
299
+ res.redirect(302, url.toString());
300
+ }
301
+ function scopeViews(scopes, req) {
302
+ return toScopeViews(scopes, scopeDescriptionsFrom(req, deps.scopeDescriptions));
303
+ }
304
+ /** The hidden fields that carry the request across a form post. */
305
+ function hiddenFields(request) {
306
+ const fields = [
307
+ ['response_type', request.responseType],
308
+ ['client_id', request.clientId],
309
+ ['redirect_uri', request.redirectUri],
310
+ ['scope', request.scopes.join(' ')],
311
+ ['state', request.state],
312
+ ['code_challenge', request.codeChallenge],
313
+ ['code_challenge_method', request.codeChallengeMethod],
314
+ ['resource', request.resource],
315
+ ];
316
+ return fields
317
+ .filter((entry) => entry[1] !== undefined && entry[1] !== '')
318
+ .map(([name, value]) => ({ name, value }));
319
+ }
320
+ function render(req, res, client, request, variant, extra = {}) {
321
+ res.render(viewName, {
322
+ layout,
323
+ action: actionPath,
324
+ variant,
325
+ isAuthenticated: variant === 'authenticated',
326
+ isLogin: variant === 'login',
327
+ isSignup: variant === 'signup',
328
+ client: {
329
+ name: client.name ?? client.clientId,
330
+ description: client.description ?? undefined,
331
+ clientUri: client.clientUri ?? undefined,
332
+ policyUri: client.policyUri ?? undefined,
333
+ tosUri: client.tosUri ?? undefined,
334
+ logoUri: safeLogoUri(client.logoUri),
335
+ monogram: monogramFor(client),
336
+ provenance: provenanceFor(client),
337
+ mode: client.mode,
338
+ },
339
+ scopes: scopeViews(request.scopes, req),
340
+ hiddenFields: hiddenFields(request),
341
+ ...extra,
342
+ });
343
+ }
344
+ /** Which face to show. `res.locals.auth` is cap-users' `addAuthContext`. */
345
+ function variantFor(req, res) {
346
+ const tier = res.locals?.auth?.trustTier;
347
+ return req.user && (tier === 'authenticated' || tier === 'verified')
348
+ ? 'authenticated'
349
+ : 'login';
350
+ }
351
+ function reportConsentError(res, err) {
352
+ // Our own page, because there is nowhere safe to send the caller. Rendered
353
+ // rather than JSON: whoever is looking at this is a person in a browser.
354
+ res.status(err.status).render('oauth-consent-error', {
355
+ layout,
356
+ errorMessage: err.message,
357
+ });
358
+ }
359
+ const showConsent = async (req, res, next) => {
360
+ let target;
361
+ try {
362
+ target = await resolveTarget(req.query);
363
+ }
364
+ catch (err) {
365
+ if (err instanceof ConsentRequestError)
366
+ return reportConsentError(res, err);
367
+ return next(err);
368
+ }
369
+ try {
370
+ const request = parseRequest(req.query, target.client);
371
+ render(req, res, target.client, request, variantFor(req, res));
372
+ }
373
+ catch (err) {
374
+ if (err instanceof RedirectableError) {
375
+ return redirectTo(res, target.redirectUri, {
376
+ error: err.code,
377
+ error_description: err.message,
378
+ state: first(req.query.state),
379
+ });
380
+ }
381
+ next(err);
382
+ }
383
+ };
384
+ const submitConsent = async (req, res, next) => {
385
+ const body = (req.body ?? {});
386
+ let target;
387
+ try {
388
+ target = await resolveTarget(body);
389
+ }
390
+ catch (err) {
391
+ if (err instanceof ConsentRequestError)
392
+ return reportConsentError(res, err);
393
+ return next(err);
394
+ }
395
+ let request;
396
+ try {
397
+ request = parseRequest(body, target.client);
398
+ }
399
+ catch (err) {
400
+ if (err instanceof RedirectableError) {
401
+ return redirectTo(res, target.redirectUri, {
402
+ error: err.code,
403
+ error_description: err.message,
404
+ state: first(body.state),
405
+ });
406
+ }
407
+ return next(err);
408
+ }
409
+ const action = first(body.action) ?? 'approve';
410
+ try {
411
+ if (action === 'deny') {
412
+ // A protocol response, not a navigation. If Cancel merely went home,
413
+ // the waiting client would hang until its own timeout while the user
414
+ // believed they had declined. The target is derived from the registry
415
+ // above and never taken from the form, or it is an open redirect.
416
+ return redirectTo(res, request.redirectUri, {
417
+ error: 'access_denied',
418
+ error_description: 'the user declined this request',
419
+ state: request.state,
420
+ });
421
+ }
422
+ if (action === 'login' || action === 'signup') {
423
+ let user;
424
+ let sessionToken;
425
+ try {
426
+ const result = action === 'login'
427
+ ? await signIn({
428
+ email: String(body.email ?? ''),
429
+ password: String(body.password ?? ''),
430
+ sessionToken: req.cookies?.[SESSION_COOKIE],
431
+ })
432
+ : await register({
433
+ email: String(body.email ?? ''),
434
+ password: String(body.password ?? ''),
435
+ passwordConfirmation: String(body.passwordConfirmation ?? ''),
436
+ });
437
+ user = result.user;
438
+ sessionToken = result.sessionToken;
439
+ }
440
+ catch (err) {
441
+ return render(req, res, target.client, request, action, {
442
+ email: typeof body.email === 'string' ? body.email : undefined,
443
+ errorMessage: action === 'login'
444
+ ? 'Invalid email or password'
445
+ : err instanceof Error && err.message
446
+ ? err.message
447
+ : 'We could not create that account',
448
+ });
449
+ }
450
+ res.cookie(SESSION_COOKIE, sessionToken, { httpOnly: true, maxAge: ONE_YEAR_MS });
451
+ req.user = user;
452
+ // Signing in is not consenting. Re-render the card and require a
453
+ // second, explicit Approve — see the header.
454
+ return render(req, res, target.client, request, 'authenticated', {
455
+ justSignedIn: true,
456
+ });
457
+ }
458
+ if (action === 'showSignup' || action === 'showLogin') {
459
+ return render(req, res, target.client, request, action === 'showSignup' ? 'signup' : 'login');
460
+ }
461
+ // Approve.
462
+ const user = req.user;
463
+ if (!user) {
464
+ // Not signed in, so there is nobody to approve on behalf of. Show the
465
+ // login variant rather than redirecting: the request must stay on the
466
+ // page.
467
+ return render(req, res, target.client, request, 'login', {
468
+ errorMessage: 'Please sign in to continue.',
469
+ });
470
+ }
471
+ const { code } = await authorizationCodeService.issue({
472
+ userId: user.id,
473
+ clientId: target.client.clientId,
474
+ clientPublicId: target.client.publicId,
475
+ redirectUri: request.redirectUri,
476
+ codeChallenge: request.codeChallenge,
477
+ codeChallengeMethod: request.codeChallengeMethod,
478
+ scopes: request.scopes,
479
+ resource: request.resource ?? null,
480
+ });
481
+ // `state` is echoed exactly as it arrived and is never interpreted. It is
482
+ // the client's CSRF token, not ours.
483
+ return redirectTo(res, request.redirectUri, { code, state: request.state });
484
+ }
485
+ catch (err) {
486
+ next(err);
487
+ }
488
+ };
489
+ return { showConsent, submitConsent };
490
+ }
491
+ /**
492
+ * `GET` and `POST /oauth/authorize`.
493
+ *
494
+ * ## Why the undescribed-scope warning fires on the first request
495
+ *
496
+ * It used to fire here, at construction, on the argument that a warning a
497
+ * developer only sees when somebody reaches the consent screen is a warning they
498
+ * see in production. That reasoning is right and the placement was still wrong,
499
+ * because at construction there is nothing true to judge: the descriptions live
500
+ * on `app.locals`, an application publishes them after this router is built —
501
+ * boop injects every capability's mount above an application's own
502
+ * `app.locals` assignments — and the factory argument is one the shipped
503
+ * scaffold does not pass. So the check ran against `undefined` and named every
504
+ * advertised scope, on every boot, however carefully the application had
505
+ * described them. The wishfleet integration met exactly that: three scopes
506
+ * described, three scopes reported missing (boop-2vdy.8.5).
507
+ *
508
+ * A warning that always fires is worse than none, because it trains a developer
509
+ * to scroll past the line that would have told them their consent screen was
510
+ * asking an unanswerable question.
511
+ *
512
+ * So it fires on the first request this router serves, which is the earliest
513
+ * moment the value it must judge exists — and, since the router sits on the
514
+ * application's own mount, that is the first request to the site rather than the
515
+ * first one to reach the consent screen. Once per router, not once per request.
516
+ */
517
+ function createConsentRouter(options) {
518
+ const handlers = createConsentHandlers(options);
519
+ const router = (0, express_1.Router)();
520
+ let warned = false;
521
+ router.use((req, _res, next) => {
522
+ if (!warned) {
523
+ warned = true;
524
+ warnUndescribedScopes(options.scopesSupported, scopeDescriptionsFrom(req, options.scopeDescriptions), options.warn);
525
+ }
526
+ next();
527
+ });
528
+ router.get('/oauth/authorize', handlers.showConsent);
529
+ router.post('/oauth/authorize', (0, express_1.urlencoded)({ extended: false, limit: options.bodyLimit ?? '16kb' }), handlers.submitConsent);
530
+ return router;
531
+ }
532
+ //# sourceMappingURL=consent-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent-controller.js","sourceRoot":"","sources":["../src/consent-controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { Router, type RequestHandler } from 'express';
2
+ import { type DiscoveryConfig } from './discovery.js';
3
+ export interface DiscoveryHandlers {
4
+ authorizationServerMetadata: RequestHandler;
5
+ protectedResourceMetadata: RequestHandler;
6
+ }
7
+ export declare function createDiscoveryHandlers(config: DiscoveryConfig): DiscoveryHandlers;
8
+ /**
9
+ * A router carrying both documents at their specified paths.
10
+ *
11
+ * Mounted at the application root — `app.use(oauthDiscoveryRoutes)` — because
12
+ * RFC 8414 and RFC 9728 fix these paths at the origin, and a client will not
13
+ * look for them anywhere else.
14
+ */
15
+ export declare function createDiscoveryRouter(config: DiscoveryConfig): Router;
16
+ //# sourceMappingURL=discovery-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery-controller.d.ts","sourceRoot":"","sources":["../src/discovery-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,cAAc,EAAiB,MAAM,SAAS,CAAC;AAErE,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAwBxB,MAAM,WAAW,iBAAiB;IAChC,2BAA2B,EAAE,cAAc,CAAC;IAC5C,yBAAyB,EAAE,cAAc,CAAC;CAC3C;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,iBAAiB,CAWlF;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAQrE"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDiscoveryHandlers = createDiscoveryHandlers;
4
+ exports.createDiscoveryRouter = createDiscoveryRouter;
5
+ const express_1 = require("express");
6
+ const discovery_js_1 = require("./discovery.js");
7
+ /**
8
+ * The `.well-known` routes.
9
+ *
10
+ * Both mount **outside `/api`** and are reachable with no authentication —
11
+ * they are what a client reads *before* it has any credential at all, and a 401
12
+ * here is a connect failure the client cannot recover from.
13
+ *
14
+ * There is no `:resource` parameter and no filesystem read. Two fixed paths,
15
+ * two computed bodies. See the header of `discovery.ts` for why.
16
+ */
17
+ /** One place the two handlers agree on headers, so they cannot drift. */
18
+ function sendDocument(res, body) {
19
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
20
+ // Public, because the documents carry no secret and every client fetches
21
+ // them on every connect. An hour is the donor's number and the ecosystem's
22
+ // habit; it is short enough that a scope or endpoint change propagates the
23
+ // same working day.
24
+ res.setHeader('Cache-Control', `public, max-age=${discovery_js_1.DISCOVERY_MAX_AGE_SECONDS}`);
25
+ res.status(200).json(body);
26
+ }
27
+ function createDiscoveryHandlers(config) {
28
+ // Built once at construction rather than per request: the configuration is
29
+ // read at boot, so a running process has one answer and cannot serve two
30
+ // different documents to two clients.
31
+ const authorizationServer = (0, discovery_js_1.buildAuthorizationServerMetadata)(config);
32
+ const protectedResource = (0, discovery_js_1.buildProtectedResourceMetadata)(config);
33
+ return {
34
+ authorizationServerMetadata: (_req, res) => sendDocument(res, authorizationServer),
35
+ protectedResourceMetadata: (_req, res) => sendDocument(res, protectedResource),
36
+ };
37
+ }
38
+ /**
39
+ * A router carrying both documents at their specified paths.
40
+ *
41
+ * Mounted at the application root — `app.use(oauthDiscoveryRoutes)` — because
42
+ * RFC 8414 and RFC 9728 fix these paths at the origin, and a client will not
43
+ * look for them anywhere else.
44
+ */
45
+ function createDiscoveryRouter(config) {
46
+ const handlers = createDiscoveryHandlers(config);
47
+ const router = (0, express_1.Router)();
48
+ router.get('/.well-known/oauth-authorization-server', handlers.authorizationServerMetadata);
49
+ router.get('/.well-known/oauth-protected-resource', handlers.protectedResourceMetadata);
50
+ return router;
51
+ }
52
+ //# sourceMappingURL=discovery-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery-controller.js","sourceRoot":"","sources":["../src/discovery-controller.ts"],"names":[],"mappings":";;AAoCA,0DAWC;AASD,sDAQC;AAhED,qCAAqE;AAErE,iDAKwB;AAExB;;;;;;;;;GASG;AAEH,yEAAyE;AACzE,SAAS,YAAY,CAAC,GAAa,EAAE,IAAa;IAChD,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,iCAAiC,CAAC,CAAC;IACjE,yEAAyE;IACzE,2EAA2E;IAC3E,2EAA2E;IAC3E,oBAAoB;IACpB,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,mBAAmB,wCAAyB,EAAE,CAAC,CAAC;IAC/E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC;AAOD,SAAgB,uBAAuB,CAAC,MAAuB;IAC7D,2EAA2E;IAC3E,yEAAyE;IACzE,sCAAsC;IACtC,MAAM,mBAAmB,GAAG,IAAA,+CAAgC,EAAC,MAAM,CAAC,CAAC;IACrE,MAAM,iBAAiB,GAAG,IAAA,6CAA8B,EAAC,MAAM,CAAC,CAAC;IAEjE,OAAO;QACL,2BAA2B,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,mBAAmB,CAAC;QAClF,yBAAyB,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,iBAAiB,CAAC;KAC/E,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,qBAAqB,CAAC,MAAuB;IAC3D,MAAM,QAAQ,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IAExB,MAAM,CAAC,GAAG,CAAC,yCAAyC,EAAE,QAAQ,CAAC,2BAA2B,CAAC,CAAC;IAC5F,MAAM,CAAC,GAAG,CAAC,uCAAuC,EAAE,QAAQ,CAAC,yBAAyB,CAAC,CAAC;IAExF,OAAO,MAAM,CAAC;AAChB,CAAC"}