@webpieces/http-routing 0.4.863 → 0.4.865

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 (52) hide show
  1. package/README.md +3 -1
  2. package/package.json +4 -4
  3. package/src/ApiClientFactory.js +1 -1
  4. package/src/ApiClientFactory.js.map +1 -1
  5. package/src/ApiFactory.d.ts +3 -0
  6. package/src/ApiFactory.js.map +1 -1
  7. package/src/ApiRoutingFactory.js +3 -3
  8. package/src/ApiRoutingFactory.js.map +1 -1
  9. package/src/AuthConfig.d.ts +15 -8
  10. package/src/AuthConfig.js +20 -8
  11. package/src/AuthConfig.js.map +1 -1
  12. package/src/AuthHooks.d.ts +13 -61
  13. package/src/AuthHooks.js +7 -44
  14. package/src/AuthHooks.js.map +1 -1
  15. package/src/AuthenticatedCallerContext.d.ts +6 -0
  16. package/src/AuthenticatedCallerContext.js +44 -0
  17. package/src/AuthenticatedCallerContext.js.map +1 -1
  18. package/src/AuthorizationHook.d.ts +34 -0
  19. package/src/AuthorizationHook.js +121 -0
  20. package/src/AuthorizationHook.js.map +1 -0
  21. package/src/AuthorizedApiDocument.d.ts +18 -0
  22. package/src/AuthorizedApiDocument.js +137 -0
  23. package/src/AuthorizedApiDocument.js.map +1 -0
  24. package/src/DefaultJwtHook.d.ts +4 -4
  25. package/src/DefaultJwtHook.js +10 -7
  26. package/src/DefaultJwtHook.js.map +1 -1
  27. package/src/DefaultOidcVerifier.d.ts +4 -3
  28. package/src/DefaultOidcVerifier.js +6 -4
  29. package/src/DefaultOidcVerifier.js.map +1 -1
  30. package/src/InvocationAuthentication.d.ts +7 -7
  31. package/src/InvocationAuthentication.js +18 -17
  32. package/src/InvocationAuthentication.js.map +1 -1
  33. package/src/JwtHookCompileAssertions.d.ts +1 -1
  34. package/src/JwtHookCompileAssertions.js +2 -5
  35. package/src/JwtHookCompileAssertions.js.map +1 -1
  36. package/src/RouteBuilderImpl.js +7 -0
  37. package/src/RouteBuilderImpl.js.map +1 -1
  38. package/src/WebpiecesRouter.d.ts +2 -0
  39. package/src/WebpiecesRouter.js +6 -0
  40. package/src/WebpiecesRouter.js.map +1 -1
  41. package/src/filters/AuthFilter.d.ts +11 -22
  42. package/src/filters/AuthFilter.js +75 -80
  43. package/src/filters/AuthFilter.js.map +1 -1
  44. package/src/filters/AuthorizationFilter.d.ts +10 -0
  45. package/src/filters/AuthorizationFilter.js +34 -0
  46. package/src/filters/AuthorizationFilter.js.map +1 -0
  47. package/src/index.d.ts +5 -2
  48. package/src/index.js +25 -10
  49. package/src/index.js.map +1 -1
  50. package/src/setupRuntime.d.ts +2 -2
  51. package/src/setupRuntime.js +2 -2
  52. package/src/setupRuntime.js.map +1 -1
@@ -13,7 +13,7 @@ import { DefaultOidcVerifier } from '../DefaultOidcVerifier';
13
13
  * default:
14
14
  * - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). No
15
15
  * AuthConfig bound → no accepted secret → fail fast (401).
16
- * - jwt → the bound {@link JwtHook} (`parseJwt` + `authorizeJwt`, both awaited — an app's
16
+ * - jwt → the bound {@link JwtHook} (`parseJwt`, awaited — an app's
17
17
  * strategy may reach a JWKS or a datastore). No JwtHook bound → "not enabled"
18
18
  * (401): JWT needs an app secret + payload shape.
19
19
  * - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}
@@ -41,23 +41,25 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
41
41
  private readonly callerContext;
42
42
  constructor(oidcVerifier: DefaultOidcVerifier, authConfig?: AuthConfig | undefined, jwtHook?: JwtHook<never> | undefined, oidcHook?: OidcHook | undefined, webhookAuthCallback?: WebhookAuthCallback | undefined, apiKeyHook?: ApiKeyHook | undefined);
43
43
  filter(meta: MethodMeta, nextFilter: Service<MethodMeta, WpResponse<unknown>>): Promise<WpResponse<unknown>>;
44
+ private authenticateAlternatives;
45
+ private authenticateMethod;
44
46
  /**
45
- * Stamp WHICH KIND OF CALLER this is, from the auth mode that matched — `@WpAuthJwt` is a browser
46
- * GUI, `@WpAuthApiKey` is an external partner. See {@link Surface}.
47
+ * Stamp WHICH KIND OF CALLER this is, from the auth mode that matched — `jwt()` is a browser
48
+ * GUI, `apiKey(...)` is an external partner. See {@link Surface}.
47
49
  *
48
50
  * SET AT THE EDGE ONLY. The surface belongs to the ORIGINAL caller, so a hop that already has one
49
51
  * INHERITS it unchanged and this returns immediately. Two different callers put a value there
50
52
  * before this runs, and neither may be overwritten:
51
53
  *
52
- * - the MCP bridge, which stamps `llm` before invoking a `@WpMcpAuthJwt` tool through `ApiFactory`
54
+ * - the MCP bridge, which stamps `llm` before invoking a `@WpAuthorization` tool through `ApiFactory`
53
55
  * — that in-process hop still runs this filter, and its HTTP auth mode says nothing about the
54
56
  * model on the other end;
55
- * - a `@WpAuthOidc` service-to-service hop, whose caller's surface arrived on the wire. OIDC is an
57
+ * - a `oidc(...)` service-to-service hop, whose caller's surface arrived on the wire. OIDC is an
56
58
  * internal hop, NOT a surface of its own, so it deliberately maps to `undefined` below and the
57
59
  * propagated value survives.
58
60
  *
59
61
  * Running BEFORE {@link reconcileWireTrust} is what makes the trusted-header rule work for this
60
- * key: on a `@WpAuthJwt` edge a caller who also SENDS `x-wp-surface` must match the value the
62
+ * key: on a `jwt()` edge a caller who also SENDS `x-wp-surface` must match the value the
61
63
  * framework just derived, and on a public route nothing vouches for it at all, so it is a 401.
62
64
  */
63
65
  private applySurface;
@@ -75,7 +77,7 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
75
77
  */
76
78
  private static surfaceFor;
77
79
  /**
78
- * `@WpAuthWebhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the
80
+ * `webhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the
79
81
  * VENDOR's own validator. Three ways to fail, all 401, all before the controller is entered:
80
82
  *
81
83
  * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior;
@@ -99,7 +101,7 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
99
101
  */
100
102
  private hasRawBytes;
101
103
  /**
102
- * `@WpAuthApiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound
104
+ * `apiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound
103
105
  * headers and let it look the CUSTOMER's key up. The declared `credentials` are NOT read here — they
104
106
  * describe the contract for generators; the hook owns extraction. Three ways to fail, all 401, all
105
107
  * before the controller:
@@ -115,19 +117,6 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
115
117
  * what puts the resolved organization into `RequestContext` for every downstream repository call.
116
118
  */
117
119
  private enforceApiKey;
118
- /**
119
- * A body that failed to parse is held on the {@link RawRequest} and surfaces HERE, after auth, as
120
- * the 400 it always was — never before it.
121
- *
122
- * The order is the whole point. A malformed body from an unauthenticated caller must answer 401,
123
- * because "your JSON was bad" also says "I got past auth", and on a webhook endpoint — whose url
124
- * is public by construction — that is a free oracle for anyone probing. Parsing first made the
125
- * framework hand that out for nothing.
126
- *
127
- * Only routes that retain raw bytes can defer at all; every other route still fails at parse time
128
- * in the transport, exactly as before.
129
- */
130
- private rethrowDeferredBodyError;
131
120
  /**
132
121
  * Does this mode authenticate the CALLER ITSELF (as opposed to a user, or nobody)? The INBOUND
133
122
  * twin of {@link DestinationTrust.forAuthMode}, and deliberately the same question: the client
@@ -153,7 +142,7 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
153
142
  */
154
143
  private static verifiesCaller;
155
144
  /**
156
- * `@WpAuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the
145
+ * `@WpLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the
157
146
  * endpoint did not exist.
158
147
  *
159
148
  * WHY 404 AND NOT THE 403 APPS HAND-ROLLED. Off-local the route is not registered at all
@@ -11,6 +11,7 @@ const core_util_2 = require("@webpieces/core-util");
11
11
  const AuthConfig_1 = require("../AuthConfig");
12
12
  const AuthHooks_1 = require("../AuthHooks");
13
13
  const AuthenticatedCallerContext_1 = require("../AuthenticatedCallerContext");
14
+ const AuthorizationHook_1 = require("../AuthorizationHook");
14
15
  const DefaultOidcVerifier_1 = require("../DefaultOidcVerifier");
15
16
  const log = core_util_1.LogManager.getLogger('AuthFilter');
16
17
  /**
@@ -26,7 +27,7 @@ const AUTHORIZATION_HEADER = 'authorization';
26
27
  * can never be mistaken for a token, nor accepted where the other was expected:
27
28
  *
28
29
  * Authorization: Bearer <user JWT | service OIDC token>
29
- * Authorization: Webpieces <@WpAuthSharedSecret value>
30
+ * Authorization: Webpieces <sharedSecret(...) value>
30
31
  *
31
32
  * The scheme is REQUIRED. A bare value with no scheme is rejected.
32
33
  */
@@ -41,7 +42,7 @@ const SHARED_SECRET_SCHEME = 'Webpieces';
41
42
  * default:
42
43
  * - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). No
43
44
  * AuthConfig bound → no accepted secret → fail fast (401).
44
- * - jwt → the bound {@link JwtHook} (`parseJwt` + `authorizeJwt`, both awaited — an app's
45
+ * - jwt → the bound {@link JwtHook} (`parseJwt`, awaited — an app's
45
46
  * strategy may reach a JWKS or a datastore). No JwtHook bound → "not enabled"
46
47
  * (401): JWT needs an app secret + payload shape.
47
48
  * - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}
@@ -78,59 +79,79 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
78
79
  }
79
80
  // webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility
80
81
  async filter(meta, nextFilter) {
81
- const mode = meta.routeMeta.authMeta?.mode;
82
- const authHeader = meta.invocationAuthentication
83
- ? `Bearer ${meta.invocationAuthentication.token}`
84
- : core_context_1.RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);
85
- if (!mode || mode.kind === 'public') {
86
- // Public: best-effort parse so a logged-out page can still know the logged-in user.
87
- await this.bestEffortJwt(authHeader);
88
- this.callerContext.reconcileWireTrust(/*callerVerified*/ false);
89
- this.rethrowDeferredBodyError();
82
+ const auth = meta.routeMeta.authMeta;
83
+ if (!auth)
84
+ throw new core_util_1.ApiImplementationError('Every endpoint requires @WpAuth or @WpAuthPublic.');
85
+ if (meta.routeMeta.localOnly)
86
+ this.enforceLocalOnly(meta);
87
+ if (meta.invocationAuthentication) {
88
+ meta.invocationAuthentication.assertTarget(meta.routeMeta);
90
89
  return nextFilter.invoke(meta);
91
90
  }
91
+ const header = core_context_1.RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);
92
+ if (auth.methods[0].kind === 'public') {
93
+ await this.bestEffortJwt(header);
94
+ this.callerContext.reconcileWireTrust(false);
95
+ return nextFilter.invoke(meta);
96
+ }
97
+ const selected = await this.authenticateAlternatives(auth.methods, header, meta);
98
+ this.applySurface(selected);
99
+ this.callerContext.reconcileWireTrust(AuthFilter_1.verifiesCaller(selected));
100
+ return nextFilter.invoke(meta);
101
+ }
102
+ async authenticateAlternatives(methods, header, meta) {
103
+ for (const method of methods) {
104
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- failed credentials may try another declared authenticator; invariants propagate
105
+ try {
106
+ await this.authenticateMethod(method, header, meta);
107
+ return method;
108
+ }
109
+ catch (err) { // webpieces-disable no-any-unknown -- structured credential rejection at the boundary
110
+ const error = (0, core_util_1.toError)(err);
111
+ if (!(error instanceof core_util_1.ApiUnauthorizedError) || methods.length === 1)
112
+ throw error;
113
+ }
114
+ }
115
+ throw new core_util_1.ApiUnauthorizedError('No declared credential mechanism authenticated this request.');
116
+ }
117
+ async authenticateMethod(mode, header, meta) {
92
118
  switch (mode.kind) {
93
119
  case 'jwt':
94
- await this.enforceJwt(authHeader, mode.requirement, meta.invocationAuthentication);
95
- break;
120
+ await this.enforceJwt(header);
121
+ return;
96
122
  case 'oidc':
97
- await this.enforceOidc(authHeader, mode.callers);
98
- break;
123
+ await this.enforceOidc(header, mode.callers);
124
+ return;
99
125
  case 'shared-secret':
100
- this.enforceSharedSecret(this.credential(authHeader, SHARED_SECRET_SCHEME), mode.secretKey);
101
- break;
126
+ this.enforceSharedSecret(this.credential(header, SHARED_SECRET_SCHEME), mode.secretKey);
127
+ core_context_1.RequestContext.putTrusted(AuthorizationHook_1.VERIFIED_MACHINE_CALLER, new AuthorizationHook_1.VerifiedMachineCaller('shared-secret', mode.secretKey));
128
+ return;
102
129
  case 'webhook':
103
130
  await this.enforceWebhook(mode.name, meta);
104
- break;
131
+ return;
105
132
  case 'apikey':
106
133
  await this.enforceApiKey(mode.regime, meta);
107
- break;
108
- case 'local-only':
109
- this.enforceLocalOnly(meta);
110
- break;
134
+ return;
135
+ case 'public': throw new core_util_1.ApiImplementationError('Public is not a protected auth alternative.');
111
136
  }
112
- this.applySurface(mode);
113
- this.callerContext.reconcileWireTrust(AuthFilter_1.verifiesCaller(mode));
114
- this.rethrowDeferredBodyError();
115
- return nextFilter.invoke(meta);
116
137
  }
117
138
  /**
118
- * Stamp WHICH KIND OF CALLER this is, from the auth mode that matched — `@WpAuthJwt` is a browser
119
- * GUI, `@WpAuthApiKey` is an external partner. See {@link Surface}.
139
+ * Stamp WHICH KIND OF CALLER this is, from the auth mode that matched — `jwt()` is a browser
140
+ * GUI, `apiKey(...)` is an external partner. See {@link Surface}.
120
141
  *
121
142
  * SET AT THE EDGE ONLY. The surface belongs to the ORIGINAL caller, so a hop that already has one
122
143
  * INHERITS it unchanged and this returns immediately. Two different callers put a value there
123
144
  * before this runs, and neither may be overwritten:
124
145
  *
125
- * - the MCP bridge, which stamps `llm` before invoking a `@WpMcpAuthJwt` tool through `ApiFactory`
146
+ * - the MCP bridge, which stamps `llm` before invoking a `@WpAuthorization` tool through `ApiFactory`
126
147
  * — that in-process hop still runs this filter, and its HTTP auth mode says nothing about the
127
148
  * model on the other end;
128
- * - a `@WpAuthOidc` service-to-service hop, whose caller's surface arrived on the wire. OIDC is an
149
+ * - a `oidc(...)` service-to-service hop, whose caller's surface arrived on the wire. OIDC is an
129
150
  * internal hop, NOT a surface of its own, so it deliberately maps to `undefined` below and the
130
151
  * propagated value survives.
131
152
  *
132
153
  * Running BEFORE {@link reconcileWireTrust} is what makes the trusted-header rule work for this
133
- * key: on a `@WpAuthJwt` edge a caller who also SENDS `x-wp-surface` must match the value the
154
+ * key: on a `jwt()` edge a caller who also SENDS `x-wp-surface` must match the value the
134
155
  * framework just derived, and on a public route nothing vouches for it at all, so it is a 401.
135
156
  */
136
157
  applySurface(mode) {
@@ -164,13 +185,12 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
164
185
  case 'oidc':
165
186
  case 'shared-secret':
166
187
  case 'webhook':
167
- case 'local-only':
168
188
  case 'public':
169
189
  return undefined;
170
190
  }
171
191
  }
172
192
  /**
173
- * `@WpAuthWebhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the
193
+ * `webhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the
174
194
  * VENDOR's own validator. Three ways to fail, all 401, all before the controller is entered:
175
195
  *
176
196
  * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior;
@@ -187,13 +207,13 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
187
207
  */
188
208
  async enforceWebhook(name, meta) {
189
209
  if (!this.webhookAuthCallback) {
190
- log.warn(`Refusing @WpAuthWebhook('${name}') endpoint ${meta.routeMeta.path}: no WebhookAuthCallback is bound. ` +
210
+ log.warn(`Refusing webhook('${name}') endpoint ${meta.routeMeta.path}: no WebhookAuthCallback is bound. ` +
191
211
  `Bind one (options.bind(WEBHOOK_AUTH_CALLBACK).to(YourWebhookAuthCallback)) to enable webhook verification.`);
192
212
  throw new core_util_1.ApiUnauthorizedError('Webhook auth is not enabled on this server');
193
213
  }
194
214
  const request = core_context_1.RequestContext.getRequest();
195
215
  if (!this.hasRawBytes(request)) {
196
- log.warn(`Refusing @WpAuthWebhook('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +
216
+ log.warn(`Refusing webhook('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +
197
217
  `no raw bytes. Declare @Endpoint(POST, path, WRITE, EXTERNAL, { calledBy: '${name}', rawBody: true }); a ` +
198
218
  `spec driving this route in-process must publish an HttpRequest built with a RawRequest.`);
199
219
  throw new core_util_1.ApiUnauthorizedError('Webhook signature cannot be verified: no raw request was retained');
@@ -213,7 +233,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
213
233
  return request?.raw !== undefined;
214
234
  }
215
235
  /**
216
- * `@WpAuthApiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound
236
+ * `apiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound
217
237
  * headers and let it look the CUSTOMER's key up. The declared `credentials` are NOT read here — they
218
238
  * describe the contract for generators; the hook owns extraction. Three ways to fail, all 401, all
219
239
  * before the controller:
@@ -230,13 +250,13 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
230
250
  */
231
251
  async enforceApiKey(regime, meta) {
232
252
  if (!this.apiKeyHook) {
233
- log.warn(`Refusing @WpAuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no ApiKeyHook is bound. ` +
253
+ log.warn(`Refusing apiKey('${regime}') endpoint ${meta.routeMeta.path}: no ApiKeyHook is bound. ` +
234
254
  `Bind one (options.bind(API_KEY_HOOK).to(YourApiKeyHook)) to enable api-key verification.`);
235
255
  throw new core_util_1.ApiUnauthorizedError('API-key auth is not enabled on this server');
236
256
  }
237
257
  const request = core_context_1.RequestContext.getRequest();
238
258
  if (!request) {
239
- log.warn(`Refusing @WpAuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no inbound HttpRequest is ` +
259
+ log.warn(`Refusing apiKey('${regime}') endpoint ${meta.routeMeta.path}: no inbound HttpRequest is ` +
240
260
  `in scope, so the hook has no headers to read. A spec driving this route in-process must ` +
241
261
  `publish an HttpRequest carrying the api-key headers.`);
242
262
  throw new core_util_1.ApiUnauthorizedError('API key cannot be verified: no inbound request was published');
@@ -246,24 +266,6 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
246
266
  const caller = await this.apiKeyHook.verifyApiKey(regime, request);
247
267
  this.callerContext.publish(caller);
248
268
  }
249
- /**
250
- * A body that failed to parse is held on the {@link RawRequest} and surfaces HERE, after auth, as
251
- * the 400 it always was — never before it.
252
- *
253
- * The order is the whole point. A malformed body from an unauthenticated caller must answer 401,
254
- * because "your JSON was bad" also says "I got past auth", and on a webhook endpoint — whose url
255
- * is public by construction — that is a free oracle for anyone probing. Parsing first made the
256
- * framework hand that out for nothing.
257
- *
258
- * Only routes that retain raw bytes can defer at all; every other route still fails at parse time
259
- * in the transport, exactly as before.
260
- */
261
- rethrowDeferredBodyError() {
262
- const parseError = core_context_1.RequestContext.getRequest()?.raw?.bodyParseError;
263
- if (parseError) {
264
- throw new core_util_1.ApiBadRequestError('Request body is not valid JSON', undefined, undefined, parseError);
265
- }
266
- }
267
269
  /**
268
270
  * Does this mode authenticate the CALLER ITSELF (as opposed to a user, or nobody)? The INBOUND
269
271
  * twin of {@link DestinationTrust.forAuthMode}, and deliberately the same question: the client
@@ -307,12 +309,11 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
307
309
  // The hook's OWN derived entries still land (applyAuthenticatedCaller), and reconcileWireTrust then
308
310
  // admits an inbound trusted header only when the hook independently derived the same value.
309
311
  case 'apikey':
310
- case 'local-only':
311
312
  return false;
312
313
  }
313
314
  }
314
315
  /**
315
- * `@WpAuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the
316
+ * `@WpLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the
316
317
  * endpoint did not exist.
317
318
  *
318
319
  * WHY 404 AND NOT THE 403 APPS HAND-ROLLED. Off-local the route is not registered at all
@@ -331,7 +332,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
331
332
  if (core_util_1.RuntimeLocality.isLocalDevelopment()) {
332
333
  return;
333
334
  }
334
- log.warn(`Refusing @WpAuthLocalOnly endpoint ${meta.routeMeta.path}: ` +
335
+ log.warn(`Refusing @WpLocalOnly endpoint ${meta.routeMeta.path}: ` +
335
336
  (core_util_1.RuntimeLocality.isDeclared()
336
337
  ? 'this process declared itself DEPLOYED.'
337
338
  : 'no startup declared a RuntimeLocality, so this process is treated as DEPLOYED. ' +
@@ -339,38 +340,30 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
339
340
  // Same shape as an unregistered route — see the method doc for why this is not a 403.
340
341
  throw new core_util_1.ApiEndpointNotFoundError(`No endpoint at ${meta.routeMeta.path}`);
341
342
  }
342
- async enforceJwt(header, requirement, invocation) {
343
+ async enforceJwt(header) {
343
344
  const token = this.credential(header, BEARER_SCHEME);
344
- if (!token) {
345
+ if (!token)
345
346
  throw new core_util_1.ApiUnauthorizedError('Authentication required');
346
- }
347
- if (!this.jwtHook) {
347
+ if (!this.jwtHook)
348
348
  throw new core_util_1.ApiUnauthorizedError('User-JWT auth is not enabled on this server');
349
- }
350
- const caller = await this.jwtHook.parseJwt(token); // AUTHENTICATE — throws ApiUnauthorizedError if invalid
351
- invocation?.assertCaller(caller);
352
- if (!invocation)
353
- this.callerContext.publish(caller);
354
- await this.jwtHook.authorizeJwt(caller, requirement); // AUTHORIZE — app policy; throws ApiForbiddenError to deny
349
+ this.callerContext.publish(await this.jwtHook.parseJwt(token));
355
350
  }
356
351
  async enforceOidc(header, callers) {
357
352
  const token = this.credential(header, BEARER_SCHEME);
358
- if (!token) {
359
- throw new core_util_1.ApiUnauthorizedError('Missing OIDC bearer token for @WpAuthOidc endpoint');
360
- }
361
- // App-bound OidcHook overrides the caller policy; otherwise the framework default runs directly.
362
- if (this.oidcHook) {
363
- await this.oidcHook.verifyOidc(token, callers);
364
- }
365
- else {
366
- await this.oidcVerifier.verify(token, callers);
367
- }
353
+ if (!token)
354
+ throw new core_util_1.ApiUnauthorizedError('Missing OIDC bearer token');
355
+ const identity = this.oidcHook
356
+ ? await this.oidcHook.verifyOidc(token, [...callers])
357
+ : await this.oidcVerifier.verify(token, [...callers]);
358
+ if (!identity)
359
+ throw new core_util_1.ApiImplementationError('OIDC verifier must return its verified machine identity.');
360
+ core_context_1.RequestContext.putTrusted(AuthorizationHook_1.VERIFIED_MACHINE_CALLER, new AuthorizationHook_1.VerifiedMachineCaller(identity.mechanism, identity.identity));
368
361
  }
369
362
  /** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */
370
363
  enforceSharedSecret(provided, secretKey) {
371
364
  const accepted = this.authConfig?.sharedSecrets[secretKey];
372
365
  if (!accepted || !provided || !this.matchesEither(provided, accepted)) {
373
- throw new core_util_1.ApiUnauthorizedError('Invalid shared secret for @WpAuthSharedSecret endpoint');
366
+ throw new core_util_1.ApiUnauthorizedError('Invalid shared secret for sharedSecret(...) endpoint');
374
367
  }
375
368
  }
376
369
  /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */
@@ -390,6 +383,8 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
390
383
  }
391
384
  catch (err) {
392
385
  const error = (0, core_util_1.toError)(err);
386
+ if (!(error instanceof core_util_1.ApiUnauthorizedError))
387
+ throw error;
393
388
  log.debug('Best-effort JWT parse on a public endpoint failed (treating as anonymous): ', error);
394
389
  }
395
390
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AuthFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-routing/src/filters/AuthFilter.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport {\n provideFrameworkSingleton,\n HttpRequest,\n RawHttpRequest,\n RequestContext,\n} from '@webpieces/core-context';\nimport {\n AuthMode,\n ApiEndpointNotFoundError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n JwtRequirement,\n LogManager,\n RuntimeLocality,\n Surface,\n toError,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { Filter, Service } from '@webpieces/core-util';\nimport { WpResponse } from '../WpResponse';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AUTH_CONFIG, SharedSecrets } from '../AuthConfig';\nimport {\n ApiKeyHook,\n API_KEY_HOOK,\n JwtHook,\n JWT_HOOK,\n OidcHook,\n OIDC_HOOK,\n WebhookAuthCallback,\n WEBHOOK_AUTH_CALLBACK,\n} from '../AuthHooks';\nimport { AuthenticatedCallerContext } from '../AuthenticatedCallerContext';\nimport { InvocationAuthentication } from '../InvocationAuthentication';\nimport { DefaultOidcVerifier } from '../DefaultOidcVerifier';\n\nconst log = LogManager.getLogger('AuthFilter');\n\n/**\n * The ONE credential header, read straight off the inbound HttpRequest.\n *\n * Deliberately NOT a ContextKey: a ContextKey with an httpHeader is a TRANSFERRED key, which would\n * put the caller's credential into RequestContext and hence onto every outbound call this service\n * makes, and onto every Cloud Task it enqueues. A credential belongs to ONE request hop.\n */\nconst AUTHORIZATION_HEADER = 'authorization';\n\n/**\n * The scheme (first word of the Authorization value) names WHICH credential follows, so a secret\n * can never be mistaken for a token, nor accepted where the other was expected:\n *\n * Authorization: Bearer <user JWT | service OIDC token>\n * Authorization: Webpieces <@WpAuthSharedSecret value>\n *\n * The scheme is REQUIRED. A bare value with no scheme is rejected.\n */\nconst BEARER_SCHEME = 'Bearer';\nconst SHARED_SECRET_SCHEME = 'Webpieces';\n\n/**\n * AuthFilter - the ONE framework auth filter, auto-installed just below the error filter on every\n * route. It is TRANSPORT-NEUTRAL: it reads the raw credential from the {@link HttpRequest} in\n * RequestContext (never express), so the SAME check runs over HTTP and via createApiClient.\n *\n * It enforces the endpoint's AuthMode from separately-bound pieces, each OPTIONAL except the OIDC\n * default:\n * - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). No\n * AuthConfig bound → no accepted secret → fail fast (401).\n * - jwt → the bound {@link JwtHook} (`parseJwt` + `authorizeJwt`, both awaited — an app's\n * strategy may reach a JWKS or a datastore). No JwtHook bound → \"not enabled\"\n * (401): JWT needs an app secret + payload shape.\n * - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}\n * run DIRECTLY — so a server that wires NOTHING still verifies Google OIDC.\n * - webhook → the bound {@link WebhookAuthCallback} verifies the VENDOR's signature over the retained\n * raw request. No WebhookAuthCallback bound → 401, like jwt: an unverified webhook is\n * never waved through because wiring was forgotten.\n * - apikey → the bound {@link ApiKeyHook} looks the CUSTOMER's key up (async, over the whole\n * header set) and returns the context to seed. No ApiKeyHook bound → 401, like jwt.\n * - public → BEST-EFFORT jwt parse (only if a JwtHook is bound): stamp the user's context so\n * a logged-out page still knows who is logged in; never fails.\n * - local-only → serve only when {@link RuntimeLocality} says this process is a developer's\n * machine; otherwise 404, indistinguishable from the route not existing (which,\n * off-local, it does not — `ApiRoutingFactory` never registered it).\n *\n * Zero wiring = OIDC just works; an app only binds the hooks it actually uses.\n */\n@provideFrameworkSingleton()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\nexport class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n private readonly callerContext = new AuthenticatedCallerContext();\n\n constructor(\n // Framework default, always available — verifies Google OIDC with zero app wiring.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- AuthFilter is DI-resolved via the esbuild/vitest path, which elides type-only imports (no design:paramtypes), so every param needs its explicit token\n @inject(DefaultOidcVerifier) private readonly oidcVerifier: DefaultOidcVerifier,\n // @optional: only bind an AuthConfig to enable @WpAuthSharedSecret endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(AUTH_CONFIG) private readonly authConfig?: AuthConfig,\n // @optional: only bind a JwtHook to enable @WpAuthJwt endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(JWT_HOOK) private readonly jwtHook?: JwtHook<never>,\n // @optional: only bind an OidcHook to OVERRIDE the DefaultOidcVerifier caller policy.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(OIDC_HOOK) private readonly oidcHook?: OidcHook,\n // @optional: only bind a WebhookAuthCallback to enable @WpAuthWebhook endpoints. Unbound = every such\n // endpoint 401s, which is the ONE default that must not be the other way round.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional()\n @inject(WEBHOOK_AUTH_CALLBACK)\n private readonly webhookAuthCallback?: WebhookAuthCallback,\n // @optional: only bind an ApiKeyHook to enable @WpAuthApiKey endpoints. Unbound = every such\n // endpoint 401s, for the same reason as the webhook hook above.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(API_KEY_HOOK) private readonly apiKeyHook?: ApiKeyHook,\n ) {\n super();\n }\n\n // webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\n override async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n const mode = meta.routeMeta.authMeta?.mode;\n const authHeader = meta.invocationAuthentication\n ? `Bearer ${meta.invocationAuthentication.token}`\n : RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);\n\n if (!mode || mode.kind === 'public') {\n // Public: best-effort parse so a logged-out page can still know the logged-in user.\n await this.bestEffortJwt(authHeader);\n this.callerContext.reconcileWireTrust(/*callerVerified*/ false);\n this.rethrowDeferredBodyError();\n return nextFilter.invoke(meta);\n }\n\n switch (mode.kind) {\n case 'jwt':\n await this.enforceJwt(authHeader, mode.requirement, meta.invocationAuthentication);\n break;\n case 'oidc':\n await this.enforceOidc(authHeader, mode.callers);\n break;\n case 'shared-secret':\n this.enforceSharedSecret(\n this.credential(authHeader, SHARED_SECRET_SCHEME),\n mode.secretKey,\n );\n break;\n case 'webhook':\n await this.enforceWebhook(mode.name, meta);\n break;\n case 'apikey':\n await this.enforceApiKey(mode.regime, meta);\n break;\n case 'local-only':\n this.enforceLocalOnly(meta);\n break;\n }\n this.applySurface(mode);\n this.callerContext.reconcileWireTrust(AuthFilter.verifiesCaller(mode));\n this.rethrowDeferredBodyError();\n return nextFilter.invoke(meta);\n }\n\n /**\n * Stamp WHICH KIND OF CALLER this is, from the auth mode that matched — `@WpAuthJwt` is a browser\n * GUI, `@WpAuthApiKey` is an external partner. See {@link Surface}.\n *\n * SET AT THE EDGE ONLY. The surface belongs to the ORIGINAL caller, so a hop that already has one\n * INHERITS it unchanged and this returns immediately. Two different callers put a value there\n * before this runs, and neither may be overwritten:\n *\n * - the MCP bridge, which stamps `llm` before invoking a `@WpMcpAuthJwt` tool through `ApiFactory`\n * — that in-process hop still runs this filter, and its HTTP auth mode says nothing about the\n * model on the other end;\n * - a `@WpAuthOidc` service-to-service hop, whose caller's surface arrived on the wire. OIDC is an\n * internal hop, NOT a surface of its own, so it deliberately maps to `undefined` below and the\n * propagated value survives.\n *\n * Running BEFORE {@link reconcileWireTrust} is what makes the trusted-header rule work for this\n * key: on a `@WpAuthJwt` edge a caller who also SENDS `x-wp-surface` must match the value the\n * framework just derived, and on a public route nothing vouches for it at all, so it is a 401.\n */\n private applySurface(mode: AuthMode): void {\n if (RequestContext.getTrusted(WebpiecesCoreHeaders.SURFACE) !== undefined) {\n return;\n }\n const surface = AuthFilter.surfaceFor(mode);\n if (surface !== undefined) {\n RequestContext.putTrusted(WebpiecesCoreHeaders.SURFACE, surface);\n }\n }\n\n /**\n * The auth mode -> {@link Surface} table, and the ONE place it is written down.\n *\n * `undefined` means \"this mode is not a surface\", which is a different statement from \"unknown\":\n * `oidc` and `shared-secret` are internal hops that RELAY somebody else's surface, `webhook`'s\n * sender is a vendor with no webpieces surface at all, and `local-only` is a developer's own box.\n *\n * An exhaustive switch with NO `default`, like {@link verifiesCaller} and\n * `DestinationTrust.forAuthMode`: a new AuthMode kind must fail to compile here rather than\n * silently landing on `gui` (which would publish 266 to a partner) or on `public-api` (which would\n * publish a real 4xx to a GUI).\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors verifiesCaller)\n private static surfaceFor(mode: AuthMode): Surface | undefined {\n switch (mode.kind) {\n case 'jwt':\n return 'gui';\n case 'apikey':\n return 'public-api';\n case 'oidc':\n case 'shared-secret':\n case 'webhook':\n case 'local-only':\n case 'public':\n return undefined;\n }\n }\n\n /**\n * `@WpAuthWebhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the\n * VENDOR's own validator. Three ways to fail, all 401, all before the controller is entered:\n *\n * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior;\n * an unverified webhook must never be waved through because wiring was forgotten.\n * 2. NO raw request — the transport kept no bytes. `assertEveryWebhookEndpointRetainsRawBody`\n * normally makes this a startup error, so reaching it means either a hand-registered route or\n * an in-process caller (a spec) that published an HttpRequest with no {@link RawRequest}. The\n * message says which fix applies rather than leaving a bare 401.\n * 3. The hook threw — the signature did not verify.\n *\n * Case 2 is checked HERE and nowhere else. {@link hasRawBytes} narrows the request to\n * {@link RawHttpRequest} at this one gate, so the hook's signature promises `raw` is present and\n * no vendor implementation ever writes `raw!` or a guard of its own.\n */\n private async enforceWebhook(name: string, meta: MethodMeta): Promise<void> {\n if (!this.webhookAuthCallback) {\n log.warn(\n `Refusing @WpAuthWebhook('${name}') endpoint ${meta.routeMeta.path}: no WebhookAuthCallback is bound. ` +\n `Bind one (options.bind(WEBHOOK_AUTH_CALLBACK).to(YourWebhookAuthCallback)) to enable webhook verification.`,\n );\n throw new ApiUnauthorizedError('Webhook auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n if (!this.hasRawBytes(request)) {\n log.warn(\n `Refusing @WpAuthWebhook('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +\n `no raw bytes. Declare @Endpoint(POST, path, WRITE, EXTERNAL, { calledBy: '${name}', rawBody: true }); a ` +\n `spec driving this route in-process must publish an HttpRequest built with a RawRequest.`,\n );\n throw new ApiUnauthorizedError(\n 'Webhook signature cannot be verified: no raw request was retained',\n );\n }\n // Throws ApiUnauthorizedError to deny. On success the vendor account the signature proved is\n // stamped through the SAME path a jwt or api-key caller takes.\n const caller = await this.webhookAuthCallback.verifyWebhook(name, request);\n this.callerContext.publish(caller);\n }\n\n /**\n * The ONE place the framework decides a request carries the verbatim bytes. A TYPE PREDICATE, so\n * the `true` branch hands {@link enforceWebhook} a {@link RawHttpRequest} with no cast and no\n * non-null assertion — the bad state stops being representable past this line rather than being\n * re-thrown about by every hook.\n */\n private hasRawBytes(request: HttpRequest | undefined): request is RawHttpRequest {\n return request?.raw !== undefined;\n }\n\n /**\n * `@WpAuthApiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound\n * headers and let it look the CUSTOMER's key up. The declared `credentials` are NOT read here — they\n * describe the contract for generators; the hook owns extraction. Three ways to fail, all 401, all\n * before the controller:\n *\n * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior; an\n * unverified partner request must never be waved through because wiring was forgotten.\n * 2. NO inbound request in scope — there are no headers to read, so there is nothing to verify. That\n * means a caller drove this route without publishing an HttpRequest; the message says so rather\n * than leaving a bare 401.\n * 3. The hook threw — the key, or the key/organization pair, did not check out.\n *\n * On success the hook's {@link AuthenticatedCaller} is stamped exactly as a jwt parse's is, which is\n * what puts the resolved organization into `RequestContext` for every downstream repository call.\n */\n private async enforceApiKey(regime: string, meta: MethodMeta): Promise<void> {\n if (!this.apiKeyHook) {\n log.warn(\n `Refusing @WpAuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no ApiKeyHook is bound. ` +\n `Bind one (options.bind(API_KEY_HOOK).to(YourApiKeyHook)) to enable api-key verification.`,\n );\n throw new ApiUnauthorizedError('API-key auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n if (!request) {\n log.warn(\n `Refusing @WpAuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no inbound HttpRequest is ` +\n `in scope, so the hook has no headers to read. A spec driving this route in-process must ` +\n `publish an HttpRequest carrying the api-key headers.`,\n );\n throw new ApiUnauthorizedError(\n 'API key cannot be verified: no inbound request was published',\n );\n }\n // Throws ApiUnauthorizedError to deny. The hook gets the WHOLE request so it can cross-check\n // the key against a second header (the organization the customer is acting for).\n const caller = await this.apiKeyHook.verifyApiKey(regime, request);\n this.callerContext.publish(caller);\n }\n\n /**\n * A body that failed to parse is held on the {@link RawRequest} and surfaces HERE, after auth, as\n * the 400 it always was — never before it.\n *\n * The order is the whole point. A malformed body from an unauthenticated caller must answer 401,\n * because \"your JSON was bad\" also says \"I got past auth\", and on a webhook endpoint — whose url\n * is public by construction — that is a free oracle for anyone probing. Parsing first made the\n * framework hand that out for nothing.\n *\n * Only routes that retain raw bytes can defer at all; every other route still fails at parse time\n * in the transport, exactly as before.\n */\n private rethrowDeferredBodyError(): void {\n const parseError = RequestContext.getRequest()?.raw?.bodyParseError;\n if (parseError) {\n throw new ApiBadRequestError(\n 'Request body is not valid JSON',\n undefined,\n undefined,\n parseError,\n );\n }\n }\n\n /**\n * Does this mode authenticate the CALLER ITSELF (as opposed to a user, or nobody)? The INBOUND\n * twin of {@link DestinationTrust.forAuthMode}, and deliberately the same question: the client\n * omits trusted keys for a destination that cannot verify it, and the server rejects trusted keys\n * on a route that cannot verify the sender. One rule, two ends — if they disagreed, every call\n * would fail with a 401 that looks like a framework bug.\n *\n * - `oidc` / `shared-secret` → TRUE. An internal service is on the other end and the trusted\n * context it forwarded may be believed. This is what makes cross-service identity propagation\n * work.\n * - `jwt` / `public` → FALSE. A user JWT proves who the USER is; the SENDER is still whoever\n * holds the token, i.e. a browser.\n * - `local-only` → FALSE. It verifies WHERE WE ARE RUNNING, not who is calling — anything on\n * localhost reaches it, and it has no authenticator, so nothing can ever vouch for an inbound\n * trusted header. Any such header therefore rejects the request, which is exactly right.\n *\n * - `apikey` → FALSE. See the comment on that branch: the sender is a CUSTOMER.\n *\n * An exhaustive switch with NO `default`, returning on every branch: a new AuthMode kind is a\n * COMPILE error here (TS7030, no ending return) rather than silently landing on one posture. The\n * boolean expression this replaced defaulted every future mode to \"not verified\" — the safe\n * answer, but arrived at by accident rather than by decision.\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors DestinationTrust.forAuthMode)\n private static verifiesCaller(mode: AuthMode): boolean {\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return true;\n case 'jwt':\n case 'public':\n // `webhook` DOES authenticate its sender — but the sender is an outside VENDOR, not a\n // peer in this repo, and a vendor neither speaks nor forwards webpieces context headers.\n // So there is no forwarded identity to believe, and admitting one would mean trusting a\n // key a vendor's payload could carry. Same answer as the OUTBOUND half\n // (DestinationTrust.forAuthMode), which is the invariant that keeps the two ends agreeing.\n case 'webhook':\n // `apikey` authenticates the SENDER — but the sender is a CUSTOMER's codebase, not a peer\n // service in this repo, so its forwarded trusted context is exactly what must NOT be\n // believed: admitting it would let a partner assert another customer's org id on the wire.\n // The hook's OWN derived entries still land (applyAuthenticatedCaller), and reconcileWireTrust then\n // admits an inbound trusted header only when the hook independently derived the same value.\n case 'apikey':\n case 'local-only':\n return false;\n }\n }\n\n /**\n * `@WpAuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the\n * endpoint did not exist.\n *\n * WHY 404 AND NOT THE 403 APPS HAND-ROLLED. Off-local the route is not registered at all\n * (`ApiRoutingFactory` skips it), so the ordinary way to reach this path already answers 404. A\n * 403 from here would be a DIFFERENT answer from the same framework for the same endpoint, and\n * the difference is itself the leak: 403 confirms \"this path exists in production, you merely\n * lack permission\", which is a map of the dev-only surface for anyone probing. A local-only\n * endpoint should not admit it exists. Both gates therefore return the same 404, and this one is\n * the backstop for routes registered by hand through `RouteBuilder` rather than by\n * `ApiRoutingFactory`.\n *\n * The log line names WHICH reason applies, because \"you are deployed\" and \"nobody declared a\n * locality\" have completely different fixes and both look like a bare 404 from outside.\n */\n private enforceLocalOnly(meta: MethodMeta): void {\n if (RuntimeLocality.isLocalDevelopment()) {\n return;\n }\n log.warn(\n `Refusing @WpAuthLocalOnly endpoint ${meta.routeMeta.path}: ` +\n (RuntimeLocality.isDeclared()\n ? 'this process declared itself DEPLOYED.'\n : 'no startup declared a RuntimeLocality, so this process is treated as DEPLOYED. ' +\n 'Pass the locality into RuntimeSetupOptions if this really is a developer machine.'),\n );\n // Same shape as an unregistered route — see the method doc for why this is not a 403.\n throw new ApiEndpointNotFoundError(`No endpoint at ${meta.routeMeta.path}`);\n }\n\n private async enforceJwt(\n header: string | undefined,\n requirement: JwtRequirement,\n invocation?: InvocationAuthentication,\n ): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new ApiUnauthorizedError('Authentication required');\n }\n if (!this.jwtHook) {\n throw new ApiUnauthorizedError('User-JWT auth is not enabled on this server');\n }\n const caller = await this.jwtHook.parseJwt(token); // AUTHENTICATE — throws ApiUnauthorizedError if invalid\n invocation?.assertCaller(caller);\n if (!invocation) this.callerContext.publish(caller);\n await this.jwtHook.authorizeJwt(caller, requirement); // AUTHORIZE — app policy; throws ApiForbiddenError to deny\n }\n\n private async enforceOidc(header: string | undefined, callers: string[]): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new ApiUnauthorizedError('Missing OIDC bearer token for @WpAuthOidc endpoint');\n }\n // App-bound OidcHook overrides the caller policy; otherwise the framework default runs directly.\n if (this.oidcHook) {\n await this.oidcHook.verifyOidc(token, callers);\n } else {\n await this.oidcVerifier.verify(token, callers);\n }\n }\n\n /** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */\n private enforceSharedSecret(provided: string | undefined, secretKey: string): void {\n const accepted = this.authConfig?.sharedSecrets[secretKey];\n if (!accepted || !provided || !this.matchesEither(provided, accepted)) {\n throw new ApiUnauthorizedError(\n 'Invalid shared secret for @WpAuthSharedSecret endpoint',\n );\n }\n }\n\n /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */\n private matchesEither(provided: string, accepted: SharedSecrets): boolean {\n return (\n (accepted.secret1 !== '' && this.constantTimeEquals(provided, accepted.secret1)) ||\n (accepted.secret2 !== '' && this.constantTimeEquals(provided, accepted.secret2))\n );\n }\n\n /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */\n private async bestEffortJwt(header: string | undefined): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!this.jwtHook || !token) {\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort on a public route: a bad/absent token just means \"not logged in\", must not fail the request\n try {\n this.callerContext.publish(await this.jwtHook.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(\n 'Best-effort JWT parse on a public endpoint failed (treating as anonymous): ',\n error,\n );\n }\n }\n\n /**\n * The credential value IF the header carries the expected scheme, else undefined.\n *\n * Strict: a bare value with no scheme, or a value under the WRONG scheme (a shared secret sent\n * where a JWT is expected), yields undefined and the caller 401s.\n */\n private credential(header: string | undefined, scheme: string): string | undefined {\n if (!header) {\n return undefined;\n }\n const prefix = `${scheme} `;\n return header.startsWith(prefix) ? header.substring(prefix.length) : undefined;\n }\n\n private constantTimeEquals(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'utf8');\n const bufB = Buffer.from(b, 'utf8');\n if (bufA.length !== bufB.length) {\n return false;\n }\n return timingSafeEqual(bufA, bufB);\n }\n}\n"]}
1
+ {"version":3,"file":"AuthFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-routing/src/filters/AuthFilter.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport {\n provideFrameworkSingleton,\n HttpRequest,\n RawHttpRequest,\n RequestContext,\n} from '@webpieces/core-context';\nimport {\n AuthMode,\n ApiEndpointNotFoundError,\n ApiUnauthorizedError,\n ApiImplementationError,\n LogManager,\n RuntimeLocality,\n Surface,\n toError,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { Filter, Service } from '@webpieces/core-util';\nimport { WpResponse } from '../WpResponse';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AUTH_CONFIG, SharedSecrets } from '../AuthConfig';\nimport {\n ApiKeyHook,\n API_KEY_HOOK,\n JwtHook,\n JWT_HOOK,\n OidcHook,\n OIDC_HOOK,\n WebhookAuthCallback,\n WEBHOOK_AUTH_CALLBACK,\n} from '../AuthHooks';\nimport { AuthenticatedCallerContext } from '../AuthenticatedCallerContext';\nimport { VERIFIED_MACHINE_CALLER, VerifiedMachineCaller } from '../AuthorizationHook';\nimport { DefaultOidcVerifier } from '../DefaultOidcVerifier';\n\nconst log = LogManager.getLogger('AuthFilter');\n\n/**\n * The ONE credential header, read straight off the inbound HttpRequest.\n *\n * Deliberately NOT a ContextKey: a ContextKey with an httpHeader is a TRANSFERRED key, which would\n * put the caller's credential into RequestContext and hence onto every outbound call this service\n * makes, and onto every Cloud Task it enqueues. A credential belongs to ONE request hop.\n */\nconst AUTHORIZATION_HEADER = 'authorization';\n\n/**\n * The scheme (first word of the Authorization value) names WHICH credential follows, so a secret\n * can never be mistaken for a token, nor accepted where the other was expected:\n *\n * Authorization: Bearer <user JWT | service OIDC token>\n * Authorization: Webpieces <sharedSecret(...) value>\n *\n * The scheme is REQUIRED. A bare value with no scheme is rejected.\n */\nconst BEARER_SCHEME = 'Bearer';\nconst SHARED_SECRET_SCHEME = 'Webpieces';\n\n/**\n * AuthFilter - the ONE framework auth filter, auto-installed just below the error filter on every\n * route. It is TRANSPORT-NEUTRAL: it reads the raw credential from the {@link HttpRequest} in\n * RequestContext (never express), so the SAME check runs over HTTP and via createApiClient.\n *\n * It enforces the endpoint's AuthMode from separately-bound pieces, each OPTIONAL except the OIDC\n * default:\n * - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). No\n * AuthConfig bound → no accepted secret → fail fast (401).\n * - jwt → the bound {@link JwtHook} (`parseJwt`, awaited — an app's\n * strategy may reach a JWKS or a datastore). No JwtHook bound → \"not enabled\"\n * (401): JWT needs an app secret + payload shape.\n * - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}\n * run DIRECTLY — so a server that wires NOTHING still verifies Google OIDC.\n * - webhook → the bound {@link WebhookAuthCallback} verifies the VENDOR's signature over the retained\n * raw request. No WebhookAuthCallback bound → 401, like jwt: an unverified webhook is\n * never waved through because wiring was forgotten.\n * - apikey → the bound {@link ApiKeyHook} looks the CUSTOMER's key up (async, over the whole\n * header set) and returns the context to seed. No ApiKeyHook bound → 401, like jwt.\n * - public → BEST-EFFORT jwt parse (only if a JwtHook is bound): stamp the user's context so\n * a logged-out page still knows who is logged in; never fails.\n * - local-only → serve only when {@link RuntimeLocality} says this process is a developer's\n * machine; otherwise 404, indistinguishable from the route not existing (which,\n * off-local, it does not — `ApiRoutingFactory` never registered it).\n *\n * Zero wiring = OIDC just works; an app only binds the hooks it actually uses.\n */\n@provideFrameworkSingleton()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\nexport class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n private readonly callerContext = new AuthenticatedCallerContext();\n\n constructor(\n // Framework default, always available — verifies Google OIDC with zero app wiring.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- AuthFilter is DI-resolved via the esbuild/vitest path, which elides type-only imports (no design:paramtypes), so every param needs its explicit token\n @inject(DefaultOidcVerifier) private readonly oidcVerifier: DefaultOidcVerifier,\n // @optional: only bind an AuthConfig to enable sharedSecret(...) endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(AUTH_CONFIG) private readonly authConfig?: AuthConfig,\n // @optional: only bind a JwtHook to enable jwt() endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(JWT_HOOK) private readonly jwtHook?: JwtHook<never>,\n // @optional: only bind an OidcHook to OVERRIDE the DefaultOidcVerifier caller policy.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(OIDC_HOOK) private readonly oidcHook?: OidcHook,\n // @optional: only bind a WebhookAuthCallback to enable webhook(...) endpoints. Unbound = every such\n // endpoint 401s, which is the ONE default that must not be the other way round.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional()\n @inject(WEBHOOK_AUTH_CALLBACK)\n private readonly webhookAuthCallback?: WebhookAuthCallback,\n // @optional: only bind an ApiKeyHook to enable apiKey(...) endpoints. Unbound = every such\n // endpoint 401s, for the same reason as the webhook hook above.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(API_KEY_HOOK) private readonly apiKeyHook?: ApiKeyHook,\n ) {\n super();\n }\n\n // webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\n override async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n const auth = meta.routeMeta.authMeta;\n if (!auth) throw new ApiImplementationError('Every endpoint requires @WpAuth or @WpAuthPublic.');\n if (meta.routeMeta.localOnly) this.enforceLocalOnly(meta);\n if (meta.invocationAuthentication) {\n meta.invocationAuthentication.assertTarget(meta.routeMeta);\n return nextFilter.invoke(meta);\n }\n const header = RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);\n if (auth.methods[0].kind === 'public') {\n await this.bestEffortJwt(header);\n this.callerContext.reconcileWireTrust(false);\n return nextFilter.invoke(meta);\n }\n const selected = await this.authenticateAlternatives(auth.methods, header, meta);\n this.applySurface(selected);\n this.callerContext.reconcileWireTrust(AuthFilter.verifiesCaller(selected));\n return nextFilter.invoke(meta);\n }\n\n private async authenticateAlternatives(methods: readonly AuthMode[], header: string | undefined, meta: MethodMeta): Promise<AuthMode> {\n for (const method of methods) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- failed credentials may try another declared authenticator; invariants propagate\n try {\n await this.authenticateMethod(method, header, meta);\n return method;\n } catch (err: unknown) { // webpieces-disable no-any-unknown -- structured credential rejection at the boundary\n const error = toError(err);\n if (!(error instanceof ApiUnauthorizedError) || methods.length === 1) throw error;\n }\n }\n throw new ApiUnauthorizedError('No declared credential mechanism authenticated this request.');\n }\n\n private async authenticateMethod(mode: AuthMode, header: string | undefined, meta: MethodMeta): Promise<void> {\n switch (mode.kind) {\n case 'jwt': await this.enforceJwt(header); return;\n case 'oidc': await this.enforceOidc(header, mode.callers); return;\n case 'shared-secret':\n this.enforceSharedSecret(this.credential(header, SHARED_SECRET_SCHEME), mode.secretKey);\n RequestContext.putTrusted(VERIFIED_MACHINE_CALLER, new VerifiedMachineCaller('shared-secret', mode.secretKey));\n return;\n case 'webhook': await this.enforceWebhook(mode.name, meta); return;\n case 'apikey': await this.enforceApiKey(mode.regime, meta); return;\n case 'public': throw new ApiImplementationError('Public is not a protected auth alternative.');\n }\n }\n\n /**\n * Stamp WHICH KIND OF CALLER this is, from the auth mode that matched — `jwt()` is a browser\n * GUI, `apiKey(...)` is an external partner. See {@link Surface}.\n *\n * SET AT THE EDGE ONLY. The surface belongs to the ORIGINAL caller, so a hop that already has one\n * INHERITS it unchanged and this returns immediately. Two different callers put a value there\n * before this runs, and neither may be overwritten:\n *\n * - the MCP bridge, which stamps `llm` before invoking a `@WpAuthorization` tool through `ApiFactory`\n * — that in-process hop still runs this filter, and its HTTP auth mode says nothing about the\n * model on the other end;\n * - a `oidc(...)` service-to-service hop, whose caller's surface arrived on the wire. OIDC is an\n * internal hop, NOT a surface of its own, so it deliberately maps to `undefined` below and the\n * propagated value survives.\n *\n * Running BEFORE {@link reconcileWireTrust} is what makes the trusted-header rule work for this\n * key: on a `jwt()` edge a caller who also SENDS `x-wp-surface` must match the value the\n * framework just derived, and on a public route nothing vouches for it at all, so it is a 401.\n */\n private applySurface(mode: AuthMode): void {\n if (RequestContext.getTrusted(WebpiecesCoreHeaders.SURFACE) !== undefined) {\n return;\n }\n const surface = AuthFilter.surfaceFor(mode);\n if (surface !== undefined) {\n RequestContext.putTrusted(WebpiecesCoreHeaders.SURFACE, surface);\n }\n }\n\n /**\n * The auth mode -> {@link Surface} table, and the ONE place it is written down.\n *\n * `undefined` means \"this mode is not a surface\", which is a different statement from \"unknown\":\n * `oidc` and `shared-secret` are internal hops that RELAY somebody else's surface, `webhook`'s\n * sender is a vendor with no webpieces surface at all, and `local-only` is a developer's own box.\n *\n * An exhaustive switch with NO `default`, like {@link verifiesCaller} and\n * `DestinationTrust.forAuthMode`: a new AuthMode kind must fail to compile here rather than\n * silently landing on `gui` (which would publish 266 to a partner) or on `public-api` (which would\n * publish a real 4xx to a GUI).\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors verifiesCaller)\n private static surfaceFor(mode: AuthMode): Surface | undefined {\n switch (mode.kind) {\n case 'jwt':\n return 'gui';\n case 'apikey':\n return 'public-api';\n case 'oidc':\n case 'shared-secret':\n case 'webhook':\n case 'public':\n return undefined;\n }\n }\n\n /**\n * `webhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the\n * VENDOR's own validator. Three ways to fail, all 401, all before the controller is entered:\n *\n * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior;\n * an unverified webhook must never be waved through because wiring was forgotten.\n * 2. NO raw request — the transport kept no bytes. `assertEveryWebhookEndpointRetainsRawBody`\n * normally makes this a startup error, so reaching it means either a hand-registered route or\n * an in-process caller (a spec) that published an HttpRequest with no {@link RawRequest}. The\n * message says which fix applies rather than leaving a bare 401.\n * 3. The hook threw — the signature did not verify.\n *\n * Case 2 is checked HERE and nowhere else. {@link hasRawBytes} narrows the request to\n * {@link RawHttpRequest} at this one gate, so the hook's signature promises `raw` is present and\n * no vendor implementation ever writes `raw!` or a guard of its own.\n */\n private async enforceWebhook(name: string, meta: MethodMeta): Promise<void> {\n if (!this.webhookAuthCallback) {\n log.warn(\n `Refusing webhook('${name}') endpoint ${meta.routeMeta.path}: no WebhookAuthCallback is bound. ` +\n `Bind one (options.bind(WEBHOOK_AUTH_CALLBACK).to(YourWebhookAuthCallback)) to enable webhook verification.`,\n );\n throw new ApiUnauthorizedError('Webhook auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n if (!this.hasRawBytes(request)) {\n log.warn(\n `Refusing webhook('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +\n `no raw bytes. Declare @Endpoint(POST, path, WRITE, EXTERNAL, { calledBy: '${name}', rawBody: true }); a ` +\n `spec driving this route in-process must publish an HttpRequest built with a RawRequest.`,\n );\n throw new ApiUnauthorizedError(\n 'Webhook signature cannot be verified: no raw request was retained',\n );\n }\n // Throws ApiUnauthorizedError to deny. On success the vendor account the signature proved is\n // stamped through the SAME path a jwt or api-key caller takes.\n const caller = await this.webhookAuthCallback.verifyWebhook(name, request);\n this.callerContext.publish(caller);\n }\n\n /**\n * The ONE place the framework decides a request carries the verbatim bytes. A TYPE PREDICATE, so\n * the `true` branch hands {@link enforceWebhook} a {@link RawHttpRequest} with no cast and no\n * non-null assertion — the bad state stops being representable past this line rather than being\n * re-thrown about by every hook.\n */\n private hasRawBytes(request: HttpRequest | undefined): request is RawHttpRequest {\n return request?.raw !== undefined;\n }\n\n /**\n * `apiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound\n * headers and let it look the CUSTOMER's key up. The declared `credentials` are NOT read here — they\n * describe the contract for generators; the hook owns extraction. Three ways to fail, all 401, all\n * before the controller:\n *\n * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior; an\n * unverified partner request must never be waved through because wiring was forgotten.\n * 2. NO inbound request in scope — there are no headers to read, so there is nothing to verify. That\n * means a caller drove this route without publishing an HttpRequest; the message says so rather\n * than leaving a bare 401.\n * 3. The hook threw — the key, or the key/organization pair, did not check out.\n *\n * On success the hook's {@link AuthenticatedCaller} is stamped exactly as a jwt parse's is, which is\n * what puts the resolved organization into `RequestContext` for every downstream repository call.\n */\n private async enforceApiKey(regime: string, meta: MethodMeta): Promise<void> {\n if (!this.apiKeyHook) {\n log.warn(\n `Refusing apiKey('${regime}') endpoint ${meta.routeMeta.path}: no ApiKeyHook is bound. ` +\n `Bind one (options.bind(API_KEY_HOOK).to(YourApiKeyHook)) to enable api-key verification.`,\n );\n throw new ApiUnauthorizedError('API-key auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n if (!request) {\n log.warn(\n `Refusing apiKey('${regime}') endpoint ${meta.routeMeta.path}: no inbound HttpRequest is ` +\n `in scope, so the hook has no headers to read. A spec driving this route in-process must ` +\n `publish an HttpRequest carrying the api-key headers.`,\n );\n throw new ApiUnauthorizedError(\n 'API key cannot be verified: no inbound request was published',\n );\n }\n // Throws ApiUnauthorizedError to deny. The hook gets the WHOLE request so it can cross-check\n // the key against a second header (the organization the customer is acting for).\n const caller = await this.apiKeyHook.verifyApiKey(regime, request);\n this.callerContext.publish(caller);\n }\n\n /**\n * Does this mode authenticate the CALLER ITSELF (as opposed to a user, or nobody)? The INBOUND\n * twin of {@link DestinationTrust.forAuthMode}, and deliberately the same question: the client\n * omits trusted keys for a destination that cannot verify it, and the server rejects trusted keys\n * on a route that cannot verify the sender. One rule, two ends — if they disagreed, every call\n * would fail with a 401 that looks like a framework bug.\n *\n * - `oidc` / `shared-secret` → TRUE. An internal service is on the other end and the trusted\n * context it forwarded may be believed. This is what makes cross-service identity propagation\n * work.\n * - `jwt` / `public` → FALSE. A user JWT proves who the USER is; the SENDER is still whoever\n * holds the token, i.e. a browser.\n * - `local-only` → FALSE. It verifies WHERE WE ARE RUNNING, not who is calling — anything on\n * localhost reaches it, and it has no authenticator, so nothing can ever vouch for an inbound\n * trusted header. Any such header therefore rejects the request, which is exactly right.\n *\n * - `apikey` → FALSE. See the comment on that branch: the sender is a CUSTOMER.\n *\n * An exhaustive switch with NO `default`, returning on every branch: a new AuthMode kind is a\n * COMPILE error here (TS7030, no ending return) rather than silently landing on one posture. The\n * boolean expression this replaced defaulted every future mode to \"not verified\" — the safe\n * answer, but arrived at by accident rather than by decision.\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors DestinationTrust.forAuthMode)\n private static verifiesCaller(mode: AuthMode): boolean {\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return true;\n case 'jwt':\n case 'public':\n // `webhook` DOES authenticate its sender — but the sender is an outside VENDOR, not a\n // peer in this repo, and a vendor neither speaks nor forwards webpieces context headers.\n // So there is no forwarded identity to believe, and admitting one would mean trusting a\n // key a vendor's payload could carry. Same answer as the OUTBOUND half\n // (DestinationTrust.forAuthMode), which is the invariant that keeps the two ends agreeing.\n case 'webhook':\n // `apikey` authenticates the SENDER — but the sender is a CUSTOMER's codebase, not a peer\n // service in this repo, so its forwarded trusted context is exactly what must NOT be\n // believed: admitting it would let a partner assert another customer's org id on the wire.\n // The hook's OWN derived entries still land (applyAuthenticatedCaller), and reconcileWireTrust then\n // admits an inbound trusted header only when the hook independently derived the same value.\n case 'apikey':\n return false;\n }\n }\n\n /**\n * `@WpLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the\n * endpoint did not exist.\n *\n * WHY 404 AND NOT THE 403 APPS HAND-ROLLED. Off-local the route is not registered at all\n * (`ApiRoutingFactory` skips it), so the ordinary way to reach this path already answers 404. A\n * 403 from here would be a DIFFERENT answer from the same framework for the same endpoint, and\n * the difference is itself the leak: 403 confirms \"this path exists in production, you merely\n * lack permission\", which is a map of the dev-only surface for anyone probing. A local-only\n * endpoint should not admit it exists. Both gates therefore return the same 404, and this one is\n * the backstop for routes registered by hand through `RouteBuilder` rather than by\n * `ApiRoutingFactory`.\n *\n * The log line names WHICH reason applies, because \"you are deployed\" and \"nobody declared a\n * locality\" have completely different fixes and both look like a bare 404 from outside.\n */\n private enforceLocalOnly(meta: MethodMeta): void {\n if (RuntimeLocality.isLocalDevelopment()) {\n return;\n }\n log.warn(\n `Refusing @WpLocalOnly endpoint ${meta.routeMeta.path}: ` +\n (RuntimeLocality.isDeclared()\n ? 'this process declared itself DEPLOYED.'\n : 'no startup declared a RuntimeLocality, so this process is treated as DEPLOYED. ' +\n 'Pass the locality into RuntimeSetupOptions if this really is a developer machine.'),\n );\n // Same shape as an unregistered route — see the method doc for why this is not a 403.\n throw new ApiEndpointNotFoundError(`No endpoint at ${meta.routeMeta.path}`);\n }\n\n private async enforceJwt(header: string | undefined): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) throw new ApiUnauthorizedError('Authentication required');\n if (!this.jwtHook) throw new ApiUnauthorizedError('User-JWT auth is not enabled on this server');\n this.callerContext.publish(await this.jwtHook.parseJwt(token));\n }\n\n private async enforceOidc(header: string | undefined, callers: readonly string[]): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) throw new ApiUnauthorizedError('Missing OIDC bearer token');\n const identity = this.oidcHook\n ? await this.oidcHook.verifyOidc(token, [...callers])\n : await this.oidcVerifier.verify(token, [...callers]);\n if (!identity) throw new ApiImplementationError('OIDC verifier must return its verified machine identity.');\n RequestContext.putTrusted(VERIFIED_MACHINE_CALLER, new VerifiedMachineCaller(identity.mechanism, identity.identity));\n }\n\n /** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */\n private enforceSharedSecret(provided: string | undefined, secretKey: string): void {\n const accepted = this.authConfig?.sharedSecrets[secretKey];\n if (!accepted || !provided || !this.matchesEither(provided, accepted)) {\n throw new ApiUnauthorizedError(\n 'Invalid shared secret for sharedSecret(...) endpoint',\n );\n }\n }\n\n /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */\n private matchesEither(provided: string, accepted: SharedSecrets): boolean {\n return (\n (accepted.secret1 !== '' && this.constantTimeEquals(provided, accepted.secret1)) ||\n (accepted.secret2 !== '' && this.constantTimeEquals(provided, accepted.secret2))\n );\n }\n\n /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */\n private async bestEffortJwt(header: string | undefined): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!this.jwtHook || !token) {\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort on a public route: a bad/absent token just means \"not logged in\", must not fail the request\n try {\n this.callerContext.publish(await this.jwtHook.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n if (!(error instanceof ApiUnauthorizedError)) throw error;\n log.debug(\n 'Best-effort JWT parse on a public endpoint failed (treating as anonymous): ',\n error,\n );\n }\n }\n\n /**\n * The credential value IF the header carries the expected scheme, else undefined.\n *\n * Strict: a bare value with no scheme, or a value under the WRONG scheme (a shared secret sent\n * where a JWT is expected), yields undefined and the caller 401s.\n */\n private credential(header: string | undefined, scheme: string): string | undefined {\n if (!header) {\n return undefined;\n }\n const prefix = `${scheme} `;\n return header.startsWith(prefix) ? header.substring(prefix.length) : undefined;\n }\n\n private constantTimeEquals(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'utf8');\n const bufB = Buffer.from(b, 'utf8');\n if (bufA.length !== bufB.length) {\n return false;\n }\n return timingSafeEqual(bufA, bufB);\n }\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import { Filter, Service } from '@webpieces/core-util';
2
+ import { AuthorizationService } from '../AuthorizationHook';
3
+ import { MethodMeta } from '../MethodMeta';
4
+ import { WpResponse } from '../WpResponse';
5
+ /** Receiving-server enforcement follows successful authentication and trust reconciliation. */
6
+ export declare class AuthorizationFilter extends Filter<MethodMeta, WpResponse<unknown>> {
7
+ private readonly authorization;
8
+ constructor(authorization: AuthorizationService);
9
+ filter(meta: MethodMeta, next: Service<MethodMeta, WpResponse<unknown>>): Promise<WpResponse<unknown>>;
10
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthorizationFilter = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const core_context_1 = require("@webpieces/core-context");
6
+ const core_util_1 = require("@webpieces/core-util");
7
+ const AuthorizationHook_1 = require("../AuthorizationHook");
8
+ /** Receiving-server enforcement follows successful authentication and trust reconciliation. */
9
+ let AuthorizationFilter = class AuthorizationFilter extends core_util_1.Filter {
10
+ authorization;
11
+ constructor(authorization) {
12
+ super();
13
+ this.authorization = authorization;
14
+ }
15
+ // webpieces-disable no-any-unknown -- framework filters accept any endpoint's response DTO
16
+ async filter(meta, next) {
17
+ const policy = meta.routeMeta.authorization;
18
+ if (!policy)
19
+ throw new core_util_1.ApiImplementationError(`Endpoint ${meta.routeMeta.path} requires @WpAuthorization.`);
20
+ await this.authorization.authorize(policy);
21
+ const parseError = core_context_1.RequestContext.getRequest()?.raw?.bodyParseError;
22
+ if (parseError)
23
+ throw new core_util_1.ApiBadRequestError('Request body is not valid JSON', undefined, undefined, parseError);
24
+ return next.invoke(meta);
25
+ }
26
+ };
27
+ exports.AuthorizationFilter = AuthorizationFilter;
28
+ exports.AuthorizationFilter = AuthorizationFilter = tslib_1.__decorate([
29
+ (0, core_context_1.provideFrameworkSingleton)()
30
+ // webpieces-disable no-any-unknown -- response DTO is erased at the framework filter boundary
31
+ ,
32
+ tslib_1.__metadata("design:paramtypes", [AuthorizationHook_1.AuthorizationService])
33
+ ], AuthorizationFilter);
34
+ //# sourceMappingURL=AuthorizationFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthorizationFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-routing/src/filters/AuthorizationFilter.ts"],"names":[],"mappings":";;;;AAAA,0DAAoF;AACpF,oDAAmG;AACnG,4DAA4D;AAI5D,+FAA+F;AAGxF,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,kBAAuC;IAC/C;IAA7B,YAA6B,aAAmC;QAC5D,KAAK,EAAE,CAAC;QADiB,kBAAa,GAAb,aAAa,CAAsB;IAEhE,CAAC;IAED,2FAA2F;IAClF,KAAK,CAAC,MAAM,CAAC,IAAgB,EAAE,IAA8C;QAClF,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC;QAC5C,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,kCAAsB,CAAC,YAAY,IAAI,CAAC,SAAS,CAAC,IAAI,6BAA6B,CAAC,CAAC;QAC5G,MAAM,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC3C,MAAM,UAAU,GAAG,6BAAc,CAAC,UAAU,EAAE,EAAE,GAAG,EAAE,cAAc,CAAC;QACpE,IAAI,UAAU;YAAE,MAAM,IAAI,8BAAkB,CAAC,gCAAgC,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QACjH,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;CACJ,CAAA;AAdY,kDAAmB;8BAAnB,mBAAmB;IAF/B,IAAA,wCAAyB,GAAE;IAC5B,8FAA8F;;6CAE9C,wCAAoB;GADvD,mBAAmB,CAc/B","sourcesContent":["import { provideFrameworkSingleton, RequestContext } from '@webpieces/core-context';\nimport { ApiBadRequestError, ApiImplementationError, Filter, Service } from '@webpieces/core-util';\nimport { AuthorizationService } from '../AuthorizationHook';\nimport { MethodMeta } from '../MethodMeta';\nimport { WpResponse } from '../WpResponse';\n\n/** Receiving-server enforcement follows successful authentication and trust reconciliation. */\n@provideFrameworkSingleton()\n// webpieces-disable no-any-unknown -- response DTO is erased at the framework filter boundary\nexport class AuthorizationFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n constructor(private readonly authorization: AuthorizationService) {\n super();\n }\n\n // webpieces-disable no-any-unknown -- framework filters accept any endpoint's response DTO\n override async filter(meta: MethodMeta, next: Service<MethodMeta, WpResponse<unknown>>): Promise<WpResponse<unknown>> {\n const policy = meta.routeMeta.authorization;\n if (!policy) throw new ApiImplementationError(`Endpoint ${meta.routeMeta.path} requires @WpAuthorization.`);\n await this.authorization.authorize(policy);\n const parseError = RequestContext.getRequest()?.raw?.bodyParseError;\n if (parseError) throw new ApiBadRequestError('Request body is not valid JSON', undefined, undefined, parseError);\n return next.invoke(meta);\n }\n}\n"]}