@webpieces/http-routing 0.4.759 → 0.4.761

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/http-routing",
3
- "version": "0.4.759",
3
+ "version": "0.4.761",
4
4
  "description": "Decorator-based routing with auto-wiring for WebPieces",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -22,9 +22,9 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@inversifyjs/binding-decorators": "1.1.5",
25
- "@webpieces/core-context": "0.4.759",
26
- "@webpieces/core-util": "0.4.759",
27
- "@webpieces/gcp-identity": "0.4.759",
25
+ "@webpieces/core-context": "0.4.761",
26
+ "@webpieces/core-util": "0.4.761",
27
+ "@webpieces/gcp-identity": "0.4.761",
28
28
  "inversify": "7.10.4",
29
29
  "jsonwebtoken": "9.0.2",
30
30
  "minimatch": "10.0.1"
@@ -32,6 +32,7 @@ class ApiRoutingFactory {
32
32
  * @param controllerClass - The controller class that implements the API
33
33
  */
34
34
  constructor(apiMetaClass, controllerClass) {
35
+ (0, core_util_1.assertNotInternalApi)(apiMetaClass, 'ApiRoutingFactory');
35
36
  this.apiMetaClass = apiMetaClass;
36
37
  this.controllerClass = controllerClass;
37
38
  // Validate that apiMetaClass is marked with @ApiPath
@@ -77,13 +78,13 @@ class ApiRoutingFactory {
77
78
  throw new Error(`Endpoint '${methodName}' in ${apiName} has no auth decorator. ` +
78
79
  core_util_1.MISSING_AUTH_DECORATOR_FIX);
79
80
  }
80
- // @AuthLocalOnly: off-local the route is never registered, so the endpoint does not
81
+ // @WpAuthLocalOnly: off-local the route is never registered, so the endpoint does not
81
82
  // exist rather than existing-and-refusing. This is the PRIMARY gate; AuthFilter's 404 is
82
83
  // the backstop for routes added by hand through RouteBuilder. One decorator drives both
83
84
  // — the point of moving this into the framework was that apps were hand-syncing exactly
84
85
  // these two halves across two files with a comment.
85
86
  if (authMeta.mode.kind === 'local-only' && !core_util_1.RuntimeLocality.isLocalDevelopment()) {
86
- log.info(`Skipping @AuthLocalOnly endpoint ${apiName}.${methodName} — this process is not ` +
87
+ log.info(`Skipping @WpAuthLocalOnly endpoint ${apiName}.${methodName} — this process is not ` +
87
88
  `a local developer machine, so the route is not registered at all.`);
88
89
  continue;
89
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ApiRoutingFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiRoutingFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import { Routes, RouteBuilder, RouteDefinition } from './WebAppMeta';\nimport { isApiPath, getApiPath, getEndpoints, getAuthMeta, isFormPost, isRawBody, assertEveryWebhookEndpointRetainsRawBody, getMaskSpec, LogManager, RouteMetadata, AuthMeta, MISSING_AUTH_DECORATOR_FIX, RuntimeLocality } from '@webpieces/core-util';\nimport 'reflect-metadata';\nimport { ROUTING_METADATA_KEYS } from './decorators';\n\nconst log = LogManager.getLogger('ApiRoutingFactory');\n\n/**\n * Type representing a class constructor (abstract or concrete).\n */\n// webpieces-disable no-any-unknown -- generic type alias requires unconstrained default\nexport type ClassType<T = unknown> = Function & { prototype: T };\n\n/**\n * ApiRoutingFactory - Automatically wire API interfaces to controllers.\n * Reads @ApiPath/@Endpoint decorators from an API prototype class and\n * registers POST routes for each endpoint.\n *\n * Replaces the old RESTApiRoutes class.\n *\n * Usage:\n * ```typescript\n * // In your ServerMeta:\n * getRoutes(): Routes[] {\n * return [\n * new ApiRoutingFactory(SaveApi, SaveController),\n * ];\n * }\n * ```\n */\n// webpieces-disable no-any-unknown -- generic class requires unconstrained default type params\nexport class ApiRoutingFactory<TApi = unknown, TController extends TApi = TApi> implements Routes {\n private apiMetaClass: ClassType<TApi>;\n private controllerClass: ClassType<TController>;\n\n /**\n * @param apiMetaClass - The API prototype class with @ApiPath/@Endpoint decorators\n * @param controllerClass - The controller class that implements the API\n */\n constructor(apiMetaClass: ClassType<TApi>, controllerClass: ClassType<TController>) {\n this.apiMetaClass = apiMetaClass;\n this.controllerClass = controllerClass;\n\n // Validate that apiMetaClass is marked with @ApiPath\n if (!isApiPath(apiMetaClass)) {\n const className = apiMetaClass.name || 'Unknown';\n throw new Error(`Class ${className} must be decorated with @ApiPath()`);\n }\n\n // Validate that controllerClass actually extends apiMetaClass.\n // TypeScript's structural typing won't catch a missing `extends` here, so we check\n // the runtime prototype chain. Without this, a controller can silently drift from\n // the API contract (wrong method names, wrong signatures) and only fail later as a\n // confusing routing or method-not-found error.\n const apiName = apiMetaClass.name || 'Unknown';\n const controllerName = controllerClass.name || 'Unknown';\n if (!(apiMetaClass.prototype as object).isPrototypeOf(controllerClass.prototype as object)) {\n throw new Error(\n `Controller ${controllerName} must extend ${apiName}. ` +\n `Change the class declaration to: ` +\n `'export class ${controllerName} extends ${apiName} { ... }'`,\n );\n }\n\n }\n\n /**\n * Configure routes by reading @ApiPath + @Endpoint metadata.\n * Validates controller methods and auth decorators in single loop.\n */\n configure(routeBuilder: RouteBuilder): void {\n // A webhook route whose transport keeps no bytes has nothing to verify. That is a wiring\n // mistake, so it dies at STARTUP naming the fix — not as a 401 in production on exactly the\n // traffic the endpoint exists for.\n assertEveryWebhookEndpointRetainsRawBody(this.apiMetaClass);\n\n const basePath = getApiPath(this.apiMetaClass)!;\n const endpoints = getEndpoints(this.apiMetaClass) || {};\n const controllerFilepath = this.getControllerFilepath();\n const apiName = this.apiMetaClass.name || 'Unknown';\n const controllerName = this.controllerClass.name || 'Unknown';\n\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n // Validate controller implements this method\n if (typeof this.controllerClass.prototype[methodName] !== 'function') {\n throw new Error(\n `Controller ${controllerName} must implement method ${methodName} from API ${apiName}`,\n );\n }\n\n // Validate auth decorator exists (class-level or method-level)\n const authMeta = getAuthMeta(this.apiMetaClass, methodName);\n if (!authMeta) {\n throw new Error(\n `Endpoint '${methodName}' in ${apiName} has no auth decorator. ` +\n MISSING_AUTH_DECORATOR_FIX,\n );\n }\n\n // @AuthLocalOnly: off-local the route is never registered, so the endpoint does not\n // exist rather than existing-and-refusing. This is the PRIMARY gate; AuthFilter's 404 is\n // the backstop for routes added by hand through RouteBuilder. One decorator drives both\n // — the point of moving this into the framework was that apps were hand-syncing exactly\n // these two halves across two files with a comment.\n if (authMeta.mode.kind === 'local-only' && !RuntimeLocality.isLocalDevelopment()) {\n log.info(\n `Skipping @AuthLocalOnly endpoint ${apiName}.${methodName} — this process is not ` +\n `a local developer machine, so the route is not registered at all.`,\n );\n continue;\n }\n\n const fullPath = basePath + endpointPath;\n const routeMeta = new RouteMetadata(\n 'POST',\n fullPath,\n methodName,\n controllerName,\n authMeta,\n apiName,\n isFormPost(this.apiMetaClass, methodName),\n getMaskSpec(this.apiMetaClass, methodName),\n isRawBody(this.apiMetaClass, methodName),\n );\n\n routeBuilder.addRoute(\n new RouteDefinition(routeMeta, this.controllerClass, controllerFilepath, this.apiMetaClass),\n );\n }\n }\n\n /**\n * Get the filepath of the controller source file.\n * Uses a heuristic based on the controller class name.\n */\n private getControllerFilepath(): string | undefined {\n // Check for explicit @SourceFile decorator metadata\n const filepath = Reflect.getMetadata(\n ROUTING_METADATA_KEYS.SOURCE_FILEPATH,\n this.controllerClass,\n );\n if (filepath) {\n return filepath;\n }\n\n // Fallback to class name pattern\n const className = this.controllerClass.name;\n return className ? `**/${className}.ts` : undefined;\n }\n\n /**\n * Get auth metadata for a specific method, falling back to class-level.\n */\n getAuthMetaForMethod(methodName: string): AuthMeta | undefined {\n return getAuthMeta(this.apiMetaClass, methodName);\n }\n\n /**\n * Get the API interface class.\n */\n getApiClass(): ClassType<TApi> {\n return this.apiMetaClass;\n }\n\n /**\n * Get the controller class.\n */\n getControllerClass(): ClassType<TController> {\n return this.controllerClass;\n }\n}\n"]}
1
+ {"version":3,"file":"ApiRoutingFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiRoutingFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import { Routes, RouteBuilder, RouteDefinition } from './WebAppMeta';\nimport {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMeta,\n isFormPost,\n isRawBody,\n assertEveryWebhookEndpointRetainsRawBody,\n assertNotInternalApi,\n getMaskSpec,\n LogManager,\n RouteMetadata,\n AuthMeta,\n MISSING_AUTH_DECORATOR_FIX,\n RuntimeLocality,\n} from '@webpieces/core-util';\nimport 'reflect-metadata';\nimport { ROUTING_METADATA_KEYS } from './decorators';\n\nconst log = LogManager.getLogger('ApiRoutingFactory');\n\n/**\n * Type representing a class constructor (abstract or concrete).\n */\n// webpieces-disable no-any-unknown -- generic type alias requires unconstrained default\nexport type ClassType<T = unknown> = Function & { prototype: T };\n\n/**\n * ApiRoutingFactory - Automatically wire API interfaces to controllers.\n * Reads @ApiPath/@Endpoint decorators from an API prototype class and\n * registers POST routes for each endpoint.\n *\n * Replaces the old RESTApiRoutes class.\n *\n * Usage:\n * ```typescript\n * // In your ServerMeta:\n * getRoutes(): Routes[] {\n * return [\n * new ApiRoutingFactory(SaveApi, SaveController),\n * ];\n * }\n * ```\n */\n// webpieces-disable no-any-unknown -- generic class requires unconstrained default type params\nexport class ApiRoutingFactory<TApi = unknown, TController extends TApi = TApi> implements Routes {\n private apiMetaClass: ClassType<TApi>;\n private controllerClass: ClassType<TController>;\n\n /**\n * @param apiMetaClass - The API prototype class with @ApiPath/@Endpoint decorators\n * @param controllerClass - The controller class that implements the API\n */\n constructor(apiMetaClass: ClassType<TApi>, controllerClass: ClassType<TController>) {\n assertNotInternalApi(apiMetaClass, 'ApiRoutingFactory');\n this.apiMetaClass = apiMetaClass;\n this.controllerClass = controllerClass;\n\n // Validate that apiMetaClass is marked with @ApiPath\n if (!isApiPath(apiMetaClass)) {\n const className = apiMetaClass.name || 'Unknown';\n throw new Error(`Class ${className} must be decorated with @ApiPath()`);\n }\n\n // Validate that controllerClass actually extends apiMetaClass.\n // TypeScript's structural typing won't catch a missing `extends` here, so we check\n // the runtime prototype chain. Without this, a controller can silently drift from\n // the API contract (wrong method names, wrong signatures) and only fail later as a\n // confusing routing or method-not-found error.\n const apiName = apiMetaClass.name || 'Unknown';\n const controllerName = controllerClass.name || 'Unknown';\n if (\n !(apiMetaClass.prototype as object).isPrototypeOf(controllerClass.prototype as object)\n ) {\n throw new Error(\n `Controller ${controllerName} must extend ${apiName}. ` +\n `Change the class declaration to: ` +\n `'export class ${controllerName} extends ${apiName} { ... }'`,\n );\n }\n }\n\n /**\n * Configure routes by reading @ApiPath + @Endpoint metadata.\n * Validates controller methods and auth decorators in single loop.\n */\n configure(routeBuilder: RouteBuilder): void {\n // A webhook route whose transport keeps no bytes has nothing to verify. That is a wiring\n // mistake, so it dies at STARTUP naming the fix — not as a 401 in production on exactly the\n // traffic the endpoint exists for.\n assertEveryWebhookEndpointRetainsRawBody(this.apiMetaClass);\n\n const basePath = getApiPath(this.apiMetaClass)!;\n const endpoints = getEndpoints(this.apiMetaClass) || {};\n const controllerFilepath = this.getControllerFilepath();\n const apiName = this.apiMetaClass.name || 'Unknown';\n const controllerName = this.controllerClass.name || 'Unknown';\n\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n // Validate controller implements this method\n if (typeof this.controllerClass.prototype[methodName] !== 'function') {\n throw new Error(\n `Controller ${controllerName} must implement method ${methodName} from API ${apiName}`,\n );\n }\n\n // Validate auth decorator exists (class-level or method-level)\n const authMeta = getAuthMeta(this.apiMetaClass, methodName);\n if (!authMeta) {\n throw new Error(\n `Endpoint '${methodName}' in ${apiName} has no auth decorator. ` +\n MISSING_AUTH_DECORATOR_FIX,\n );\n }\n\n // @WpAuthLocalOnly: off-local the route is never registered, so the endpoint does not\n // exist rather than existing-and-refusing. This is the PRIMARY gate; AuthFilter's 404 is\n // the backstop for routes added by hand through RouteBuilder. One decorator drives both\n // — the point of moving this into the framework was that apps were hand-syncing exactly\n // these two halves across two files with a comment.\n if (authMeta.mode.kind === 'local-only' && !RuntimeLocality.isLocalDevelopment()) {\n log.info(\n `Skipping @WpAuthLocalOnly endpoint ${apiName}.${methodName} — this process is not ` +\n `a local developer machine, so the route is not registered at all.`,\n );\n continue;\n }\n\n const fullPath = basePath + endpointPath;\n const routeMeta = new RouteMetadata(\n 'POST',\n fullPath,\n methodName,\n controllerName,\n authMeta,\n apiName,\n isFormPost(this.apiMetaClass, methodName),\n getMaskSpec(this.apiMetaClass, methodName),\n isRawBody(this.apiMetaClass, methodName),\n );\n\n routeBuilder.addRoute(\n new RouteDefinition(\n routeMeta,\n this.controllerClass,\n controllerFilepath,\n this.apiMetaClass,\n ),\n );\n }\n }\n\n /**\n * Get the filepath of the controller source file.\n * Uses a heuristic based on the controller class name.\n */\n private getControllerFilepath(): string | undefined {\n // Check for explicit @SourceFile decorator metadata\n const filepath = Reflect.getMetadata(\n ROUTING_METADATA_KEYS.SOURCE_FILEPATH,\n this.controllerClass,\n );\n if (filepath) {\n return filepath;\n }\n\n // Fallback to class name pattern\n const className = this.controllerClass.name;\n return className ? `**/${className}.ts` : undefined;\n }\n\n /**\n * Get auth metadata for a specific method, falling back to class-level.\n */\n getAuthMetaForMethod(methodName: string): AuthMeta | undefined {\n return getAuthMeta(this.apiMetaClass, methodName);\n }\n\n /**\n * Get the API interface class.\n */\n getApiClass(): ClassType<TApi> {\n return this.apiMetaClass;\n }\n\n /**\n * Get the controller class.\n */\n getControllerClass(): ClassType<TController> {\n return this.controllerClass;\n }\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { ContextKey, ContextTuple } from '@webpieces/core-util';
2
2
  /**
3
- * SharedSecrets - the accepted values for ONE `@AuthSharedSecret(name)`. BOTH secret1 AND secret2
3
+ * SharedSecrets - the accepted values for ONE `@WpAuthSharedSecret(name)`. BOTH secret1 AND secret2
4
4
  * are accepted — this is what makes zero-downtime ROTATION possible:
5
5
  *
6
6
  * to rotate: shift secret2 → secret1, and put the NEW secret in secret2. Callers cut over from
@@ -59,7 +59,7 @@ export declare class AuthenticatedCaller {
59
59
  export declare const AUTHENTICATED_CALLER_KEY: ContextKey<AuthenticatedCaller, "trusted">;
60
60
  /**
61
61
  * AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to
62
- * enforce `@AuthSharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —
62
+ * enforce `@WpAuthSharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —
63
63
  * there is no verification code here. The verification MECHANISMS are separate optional hooks the
64
64
  * app binds when it needs them:
65
65
  *
@@ -73,7 +73,7 @@ export declare const AUTHENTICATED_CALLER_KEY: ContextKey<AuthenticatedCaller, "
73
73
  * when unbound, shared-secret endpoints simply have no accepted secret and fail fast (401).
74
74
  */
75
75
  export declare class AuthConfig {
76
- /** Accepted shared-secret values keyed by `@AuthSharedSecret(name)`. DEFAULT empty — pass to enable. */
76
+ /** Accepted shared-secret values keyed by `@WpAuthSharedSecret(name)`. DEFAULT empty — pass to enable. */
77
77
  readonly sharedSecrets: Record<string, SharedSecrets>;
78
78
  constructor(sharedSecrets?: Record<string, SharedSecrets>);
79
79
  }
package/src/AuthConfig.js CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AUTH_CONFIG = exports.AuthConfig = exports.AUTHENTICATED_CALLER_KEY = exports.AuthenticatedCaller = exports.SharedSecrets = void 0;
4
4
  const core_util_1 = require("@webpieces/core-util");
5
5
  /**
6
- * SharedSecrets - the accepted values for ONE `@AuthSharedSecret(name)`. BOTH secret1 AND secret2
6
+ * SharedSecrets - the accepted values for ONE `@WpAuthSharedSecret(name)`. BOTH secret1 AND secret2
7
7
  * are accepted — this is what makes zero-downtime ROTATION possible:
8
8
  *
9
9
  * to rotate: shift secret2 → secret1, and put the NEW secret in secret2. Callers cut over from
@@ -77,7 +77,7 @@ exports.AUTHENTICATED_CALLER_KEY = core_util_1.ContextKey.trusted('authenticated
77
77
  /*isLogged*/ false);
78
78
  /**
79
79
  * AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to
80
- * enforce `@AuthSharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —
80
+ * enforce `@WpAuthSharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —
81
81
  * there is no verification code here. The verification MECHANISMS are separate optional hooks the
82
82
  * app binds when it needs them:
83
83
  *
@@ -91,7 +91,7 @@ exports.AUTHENTICATED_CALLER_KEY = core_util_1.ContextKey.trusted('authenticated
91
91
  * when unbound, shared-secret endpoints simply have no accepted secret and fail fast (401).
92
92
  */
93
93
  class AuthConfig {
94
- /** Accepted shared-secret values keyed by `@AuthSharedSecret(name)`. DEFAULT empty — pass to enable. */
94
+ /** Accepted shared-secret values keyed by `@WpAuthSharedSecret(name)`. DEFAULT empty — pass to enable. */
95
95
  sharedSecrets;
96
96
  constructor(sharedSecrets = {}) {
97
97
  this.sharedSecrets = sharedSecrets;
@@ -1 +1 @@
1
- {"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthConfig.ts"],"names":[],"mappings":";;;AAAA,oDAAgE;AAEhE;;;;;;;;;GASG;AACH,MAAa,aAAa;IAEF;IACA;IAFpB,YACoB,OAAe,EACf,OAAe;QADf,YAAO,GAAP,OAAO,CAAQ;QACf,YAAO,GAAP,OAAO,CAAQ;IAChC,CAAC;CACP;AALD,sCAKC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,mBAAmB;IAER;IACA;IACA;IAEA;IALpB,YACoB,MAAc,EACd,QAAkB,EAAE,EACpB,UAA0B,EAAE;IAC5C,wGAAwG;IACxF,SAAkC,EAAE;QAJpC,WAAM,GAAN,MAAM,CAAQ;QACd,UAAK,GAAL,KAAK,CAAe;QACpB,YAAO,GAAP,OAAO,CAAqB;QAE5B,WAAM,GAAN,MAAM,CAA8B;IACrD,CAAC;CACP;AARD,kDAQC;AAED;;;;;;;;;;;;;;;GAeG;AACU,QAAA,wBAAwB,GAAG,sBAAU,CAAC,OAAO,CACtD,qBAAqB,EACrB,yHAAyH;AACzH,cAAc,CAAC,SAAS;AACxB,cAAc,CAAC,KAAK;AACpB,YAAY,CAAC,KAAK,CACrB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAa,UAAU;IACnB,wGAAwG;IAC/F,aAAa,CAAgC;IAEtD,YAAY,gBAA+C,EAAE;QACzD,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACvC,CAAC;CACJ;AAPD,gCAOC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC","sourcesContent":["import { ContextKey, ContextTuple } from '@webpieces/core-util';\n\n/**\n * SharedSecrets - the accepted values for ONE `@AuthSharedSecret(name)`. BOTH secret1 AND secret2\n * are accepted — this is what makes zero-downtime ROTATION possible:\n *\n * to rotate: shift secret2 → secret1, and put the NEW secret in secret2. Callers cut over from\n * the old value to the new during the window; once every caller sends the new one, the stale\n * value falls out on the next shift. At all times EITHER key works, so no request is dropped.\n *\n * Data-only structure (a class, per the guidelines). Leave secret2 empty for a single secret.\n */\nexport class SharedSecrets {\n constructor(\n public readonly secret1: string,\n public readonly secret2: string,\n ) {}\n}\n\n/**\n * AuthenticatedCaller - what an authenticator PROVED about the caller of ONE request. Every hook\n * that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and\n * {@link WebhookAuthCallback.verifyWebhook}. Four fields, three jobs:\n *\n * - `userId` — WHO the caller is, as the credential proved it.\n * - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check\n * reads, `claims` is the raw payload an app's {@link JwtHook.authorizeJwt}\n * override reads for app-defined requirements (inOrg, tenant, ...).\n * - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with\n * {@link RequestContext.putTrusted}, so return only what THIS authenticator\n * derived from the credential it just verified.\n *\n * NAMING, said out loud so it is not \"fixed\" back: it is deliberately NOT a `TrustedContextMap`.\n * Three of the four fields are not context, and it is not a Map — it is the authenticated caller,\n * and the context is one thing it carries.\n *\n * Data-only structure (a class, per the guidelines).\n */\nexport class AuthenticatedCaller {\n constructor(\n public readonly userId: string,\n public readonly roles: string[] = [],\n public readonly entries: ContextTuple[] = [],\n // webpieces-disable no-any-unknown -- raw JWT claims for app-defined authorization (inOrg, tenant, ...)\n public readonly claims: Record<string, unknown> = {},\n ) {}\n}\n\n/**\n * The context slot holding the {@link AuthenticatedCaller} the framework {@link AuthFilter} resolved\n * for this request. A real TRUSTED {@link ContextKey}, written with `RequestContext.putTrusted` and\n * read with `RequestContext.getTrusted` — it replaces a raw `'__webpieces_principal__'` string that\n * was the one place in the codebase bypassing the typed context layer.\n *\n * `httpHeader` is deliberately UNDEFINED, so the key is context-only and NEVER travels. Two reasons,\n * and either alone is decisive: the value is an OBJECT, which no HTTP header can carry; and a\n * principal is proof THIS hop's authenticator produced, so forwarding it would hand the next service\n * a \"proven\" caller nothing on that hop verified. The individual facts a downstream service needs\n * (userId, orgId, roles) already propagate as their own transferred keys in\n * {@link WebpiecesCoreHeaders}, gated by the caller-verified rule.\n *\n * `isLogged` is FALSE: it is an object carrying the raw credential claims, which has no business\n * being serialized into a log line.\n */\nexport const AUTHENTICATED_CALLER_KEY = ContextKey.trusted<AuthenticatedCaller>(\n 'authenticatedCaller',\n 'resolved by the framework AuthFilter from a credential an app-bound JwtHook, ApiKeyHook or WebhookAuthCallback verified',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ false,\n);\n\n/**\n * AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to\n * enforce `@AuthSharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —\n * there is no verification code here. The verification MECHANISMS are separate optional hooks the\n * app binds when it needs them:\n *\n * - user JWT → bind a {@link JwtHook} (async parseJwt + async authorizeJwt).\n * - api key → bind an {@link ApiKeyHook} (async verifyApiKey over the request's headers).\n * - OIDC → bind an {@link OidcHook} to override the framework's default verifier; a server that\n * binds nothing still verifies Google OIDC via the built-in {@link DefaultOidcVerifier}.\n *\n * So a zero-wiring server accepts service-to-service OIDC out of the box, and an app only binds the\n * pieces it actually uses. This class is injected `@optional` into AuthFilter (rebindable in tests);\n * when unbound, shared-secret endpoints simply have no accepted secret and fail fast (401).\n */\nexport class AuthConfig {\n /** Accepted shared-secret values keyed by `@AuthSharedSecret(name)`. DEFAULT empty — pass to enable. */\n readonly sharedSecrets: Record<string, SharedSecrets>;\n\n constructor(sharedSecrets: Record<string, SharedSecrets> = {}) {\n this.sharedSecrets = sharedSecrets;\n }\n}\n\n/**\n * DI identifier for the optional {@link AuthConfig} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(AUTH_CONFIG)`\n * correct — undefined when unbound. The AuthConfig class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const AUTH_CONFIG = Symbol.for('AuthConfig');\n"]}
1
+ {"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthConfig.ts"],"names":[],"mappings":";;;AAAA,oDAAgE;AAEhE;;;;;;;;;GASG;AACH,MAAa,aAAa;IAEF;IACA;IAFpB,YACoB,OAAe,EACf,OAAe;QADf,YAAO,GAAP,OAAO,CAAQ;QACf,YAAO,GAAP,OAAO,CAAQ;IAChC,CAAC;CACP;AALD,sCAKC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,mBAAmB;IAER;IACA;IACA;IAEA;IALpB,YACoB,MAAc,EACd,QAAkB,EAAE,EACpB,UAA0B,EAAE;IAC5C,wGAAwG;IACxF,SAAkC,EAAE;QAJpC,WAAM,GAAN,MAAM,CAAQ;QACd,UAAK,GAAL,KAAK,CAAe;QACpB,YAAO,GAAP,OAAO,CAAqB;QAE5B,WAAM,GAAN,MAAM,CAA8B;IACrD,CAAC;CACP;AARD,kDAQC;AAED;;;;;;;;;;;;;;;GAeG;AACU,QAAA,wBAAwB,GAAG,sBAAU,CAAC,OAAO,CACtD,qBAAqB,EACrB,yHAAyH;AACzH,cAAc,CAAC,SAAS;AACxB,cAAc,CAAC,KAAK;AACpB,YAAY,CAAC,KAAK,CACrB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAa,UAAU;IACnB,0GAA0G;IACjG,aAAa,CAAgC;IAEtD,YAAY,gBAA+C,EAAE;QACzD,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACvC,CAAC;CACJ;AAPD,gCAOC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC","sourcesContent":["import { ContextKey, ContextTuple } from '@webpieces/core-util';\n\n/**\n * SharedSecrets - the accepted values for ONE `@WpAuthSharedSecret(name)`. BOTH secret1 AND secret2\n * are accepted — this is what makes zero-downtime ROTATION possible:\n *\n * to rotate: shift secret2 → secret1, and put the NEW secret in secret2. Callers cut over from\n * the old value to the new during the window; once every caller sends the new one, the stale\n * value falls out on the next shift. At all times EITHER key works, so no request is dropped.\n *\n * Data-only structure (a class, per the guidelines). Leave secret2 empty for a single secret.\n */\nexport class SharedSecrets {\n constructor(\n public readonly secret1: string,\n public readonly secret2: string,\n ) {}\n}\n\n/**\n * AuthenticatedCaller - what an authenticator PROVED about the caller of ONE request. Every hook\n * that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and\n * {@link WebhookAuthCallback.verifyWebhook}. Four fields, three jobs:\n *\n * - `userId` — WHO the caller is, as the credential proved it.\n * - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check\n * reads, `claims` is the raw payload an app's {@link JwtHook.authorizeJwt}\n * override reads for app-defined requirements (inOrg, tenant, ...).\n * - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with\n * {@link RequestContext.putTrusted}, so return only what THIS authenticator\n * derived from the credential it just verified.\n *\n * NAMING, said out loud so it is not \"fixed\" back: it is deliberately NOT a `TrustedContextMap`.\n * Three of the four fields are not context, and it is not a Map — it is the authenticated caller,\n * and the context is one thing it carries.\n *\n * Data-only structure (a class, per the guidelines).\n */\nexport class AuthenticatedCaller {\n constructor(\n public readonly userId: string,\n public readonly roles: string[] = [],\n public readonly entries: ContextTuple[] = [],\n // webpieces-disable no-any-unknown -- raw JWT claims for app-defined authorization (inOrg, tenant, ...)\n public readonly claims: Record<string, unknown> = {},\n ) {}\n}\n\n/**\n * The context slot holding the {@link AuthenticatedCaller} the framework {@link AuthFilter} resolved\n * for this request. A real TRUSTED {@link ContextKey}, written with `RequestContext.putTrusted` and\n * read with `RequestContext.getTrusted` — it replaces a raw `'__webpieces_principal__'` string that\n * was the one place in the codebase bypassing the typed context layer.\n *\n * `httpHeader` is deliberately UNDEFINED, so the key is context-only and NEVER travels. Two reasons,\n * and either alone is decisive: the value is an OBJECT, which no HTTP header can carry; and a\n * principal is proof THIS hop's authenticator produced, so forwarding it would hand the next service\n * a \"proven\" caller nothing on that hop verified. The individual facts a downstream service needs\n * (userId, orgId, roles) already propagate as their own transferred keys in\n * {@link WebpiecesCoreHeaders}, gated by the caller-verified rule.\n *\n * `isLogged` is FALSE: it is an object carrying the raw credential claims, which has no business\n * being serialized into a log line.\n */\nexport const AUTHENTICATED_CALLER_KEY = ContextKey.trusted<AuthenticatedCaller>(\n 'authenticatedCaller',\n 'resolved by the framework AuthFilter from a credential an app-bound JwtHook, ApiKeyHook or WebhookAuthCallback verified',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ false,\n);\n\n/**\n * AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to\n * enforce `@WpAuthSharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —\n * there is no verification code here. The verification MECHANISMS are separate optional hooks the\n * app binds when it needs them:\n *\n * - user JWT → bind a {@link JwtHook} (async parseJwt + async authorizeJwt).\n * - api key → bind an {@link ApiKeyHook} (async verifyApiKey over the request's headers).\n * - OIDC → bind an {@link OidcHook} to override the framework's default verifier; a server that\n * binds nothing still verifies Google OIDC via the built-in {@link DefaultOidcVerifier}.\n *\n * So a zero-wiring server accepts service-to-service OIDC out of the box, and an app only binds the\n * pieces it actually uses. This class is injected `@optional` into AuthFilter (rebindable in tests);\n * when unbound, shared-secret endpoints simply have no accepted secret and fail fast (401).\n */\nexport class AuthConfig {\n /** Accepted shared-secret values keyed by `@WpAuthSharedSecret(name)`. DEFAULT empty — pass to enable. */\n readonly sharedSecrets: Record<string, SharedSecrets>;\n\n constructor(sharedSecrets: Record<string, SharedSecrets> = {}) {\n this.sharedSecrets = sharedSecrets;\n }\n}\n\n/**\n * DI identifier for the optional {@link AuthConfig} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(AUTH_CONFIG)`\n * correct — undefined when unbound. The AuthConfig class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const AUTH_CONFIG = Symbol.for('AuthConfig');\n"]}
@@ -4,7 +4,7 @@ import { AuthenticatedCaller } from './AuthConfig';
4
4
  /**
5
5
  * JwtHook - the OPTIONAL user-JWT mechanism. Its DI token is the {@link JWT_HOOK} Symbol injected via
6
6
  * `@inject(JWT_HOOK)` (a Symbol, because the app container uses autobind; rebindable in tests). Bind one
7
- * to turn on `@AuthJwt({...})` endpoints. When NO JwtHook is bound, the framework
7
+ * to turn on `@WpAuthJwt({...})` endpoints. When NO JwtHook is bound, the framework
8
8
  * {@link AuthFilter} treats every jwt endpoint as "not enabled" and fails fast (401) — there is no
9
9
  * default JWT verification because it needs an app secret + payload shape the framework can't guess.
10
10
  *
@@ -13,12 +13,12 @@ import { AuthenticatedCaller } from './AuthConfig';
13
13
  * - `authorizeJwt` — AUTHORIZATION: check the authenticated user against the endpoint's
14
14
  * {@link JwtRequirement}. The DEFAULT enforces the roles any-of; override for
15
15
  * app-defined requirements carried by the SAME decorator, e.g.
16
- * `@AuthJwt({allRolesAllowed: true, inOrg: true})` →
16
+ * `@WpAuthJwt({allRolesAllowed: true, inOrg: true})` →
17
17
  * `if (requirement['inOrg'] && !values.claims['orgId']) ...`.
18
18
  *
19
19
  * BOTH ARE ASYNC, and both for the same reason: the strategy is the app's, and an app's strategy
20
20
  * reaches the network. `parseJwt` may fetch a JWKS or call a provider SDK; `authorizeJwt`'s own
21
- * motivating example — `@AuthJwt({allRolesAllowed: true, inOrg: true})` — is a membership question a
21
+ * motivating example — `@WpAuthJwt({allRolesAllowed: true, inOrg: true})` — is a membership question a
22
22
  * real app answers from a datastore. A sync signature makes both of those unwritable, and it made
23
23
  * `JwtHook` the last sync hook: {@link OidcHook.verifyOidc}, {@link WebhookAuthCallback.verifyWebhook} and
24
24
  * {@link ApiKeyHook.verifyApiKey} all return promises. An implementation that needs no I/O simply has
@@ -58,7 +58,7 @@ export declare const JWT_HOOK: unique symbol;
58
58
  * autobind; rebindable in tests). Bind one ONLY to customize the caller policy — e.g. an app that reads an
59
59
  * `ALLOWED_OIDC_CALLERS` env var at its composition root and enforces that allow-list. When NO
60
60
  * OidcHook is bound, the framework {@link AuthFilter} runs the built-in {@link DefaultOidcVerifier}
61
- * directly, so a server that wires nothing still verifies Google OIDC against its `@AuthOidc(...callers)`
61
+ * directly, so a server that wires nothing still verifies Google OIDC against its `@WpAuthOidc(...callers)`
62
62
  * (else trusts the edge — any Google-signed caller). `verifyOidc` verifies the token against `callers`;
63
63
  * throw on failure.
64
64
  */
@@ -72,7 +72,7 @@ export declare abstract class OidcHook {
72
72
  */
73
73
  export declare const OIDC_HOOK: unique symbol;
74
74
  /**
75
- * WebhookAuthCallback - the OPTIONAL mechanism behind `@AuthWebhook(name)`: prove that an inbound request was
75
+ * WebhookAuthCallback - the OPTIONAL mechanism behind `@WpAuthWebhook(name)`: prove that an inbound request was
76
76
  * really authored by the outside vendor the contract names. Its DI token is the {@link WEBHOOK_AUTH_CALLBACK}
77
77
  * Symbol injected via `@inject(WEBHOOK_AUTH_CALLBACK)` (a Symbol, because the app container uses autobind;
78
78
  * rebindable in tests). The third hook, symmetric with {@link JwtHook} / {@link OidcHook}:
@@ -82,10 +82,10 @@ export declare const OIDC_HOOK: unique symbol;
82
82
  * options.bind(WEBHOOK_AUTH_CALLBACK).to(CompanyWebhookAuthCallback);
83
83
  * ```
84
84
  *
85
- * When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every `@AuthWebhook` endpoint,
85
+ * When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every `@WpAuthWebhook` endpoint,
86
86
  * exactly as it does for an unbound JwtHook. There is no framework default and there never will be
87
87
  * one: silently allowing an unverified webhook is the single default that must not exist, and the
88
- * framework ships no vendor crypto by design (see {@link AuthWebhook} for why reimplementing five
88
+ * framework ships no vendor crypto by design (see {@link WpAuthWebhook} for why reimplementing five
89
89
  * vendors' schemes is a losing trade).
90
90
  *
91
91
  * ONE hook serves EVERY vendor: `name` selects which, so an app with a Sentry hook and a Twilio hook
@@ -108,10 +108,10 @@ export declare abstract class WebhookAuthCallback {
108
108
  * caller-NOT-verified (see `AuthFilter.verifiesCaller`): a vendor is not a peer service, so
109
109
  * nothing the vendor merely ASSERTED on the wire is admitted.
110
110
  *
111
- * @param name the string on the contract's `@AuthWebhook(name)` — which vendor this route is.
111
+ * @param name the string on the contract's `@WpAuthWebhook(name)` — which vendor this route is.
112
112
  * @param request the transport-neutral request, narrowed to {@link RawHttpRequest}: `request.raw`
113
113
  * holds the verbatim bytes + absolute url and is PRESENT, never optional.
114
- * `@AuthWebhook` requires `@Endpoint(..., { rawBody: true })` at wiring time, and
114
+ * `@WpAuthWebhook` requires `@Endpoint(..., { rawBody: true })` at wiring time, and
115
115
  * AuthFilter 401s rather than calling this hook with nothing to check — so an
116
116
  * implementation never writes `raw!` or a guard of its own.
117
117
  */
@@ -124,7 +124,7 @@ export declare abstract class WebhookAuthCallback {
124
124
  */
125
125
  export declare const WEBHOOK_AUTH_CALLBACK: unique symbol;
126
126
  /**
127
- * ApiKeyHook - the OPTIONAL mechanism behind `@AuthApiKey(regime, credentials)`: authenticate a
127
+ * ApiKeyHook - the OPTIONAL mechanism behind `@WpAuthApiKey(regime, credentials)`: authenticate a
128
128
  * CUSTOMER-held api key
129
129
  * against the app's own datastore and return the context to seed. Its DI token is the
130
130
  * {@link API_KEY_HOOK} Symbol injected via `@inject(API_KEY_HOOK)` (a Symbol, because the app container
@@ -136,7 +136,7 @@ export declare const WEBHOOK_AUTH_CALLBACK: unique symbol;
136
136
  * options.bind(API_KEY_HOOK).to(OneTabletApiKeyHook);
137
137
  * ```
138
138
  *
139
- * When NO ApiKeyHook is bound, the framework {@link AuthFilter} 401s every `@AuthApiKey` endpoint,
139
+ * When NO ApiKeyHook is bound, the framework {@link AuthFilter} 401s every `@WpAuthApiKey` endpoint,
140
140
  * exactly as it does for an unbound JwtHook. There is no framework default and there never will be
141
141
  * one: the key regime lives in the app's datastore, under the app's hashing scheme, behind the app's
142
142
  * choice of header names.
@@ -162,10 +162,10 @@ export declare abstract class ApiKeyHook {
162
162
  *
163
163
  * NOTE the seeded entries are TRUSTED context keys, so return only what THIS hook proved from the
164
164
  * credential it just verified. Anything the caller merely asserted on the wire is not admitted by
165
- * `@AuthApiKey` — the mode is deliberately caller-NOT-verified (see `AuthFilter.verifiesCaller`),
165
+ * `@WpAuthApiKey` — the mode is deliberately caller-NOT-verified (see `AuthFilter.verifiesCaller`),
166
166
  * because a customer is not an internal service.
167
167
  *
168
- * @param regime the first argument of the contract's `@AuthApiKey(regime, credentials)` — which key
168
+ * @param regime the first argument of the contract's `@WpAuthApiKey(regime, credentials)` — which key
169
169
  * regime this route belongs to.
170
170
  * @param request the inbound request; read as many headers as the regime needs with
171
171
  * `getHeader` / `getHeaderValues`, either by raw name or by {@link ContextKey}.
package/src/AuthHooks.js CHANGED
@@ -5,7 +5,7 @@ const core_util_1 = require("@webpieces/core-util");
5
5
  /**
6
6
  * JwtHook - the OPTIONAL user-JWT mechanism. Its DI token is the {@link JWT_HOOK} Symbol injected via
7
7
  * `@inject(JWT_HOOK)` (a Symbol, because the app container uses autobind; rebindable in tests). Bind one
8
- * to turn on `@AuthJwt({...})` endpoints. When NO JwtHook is bound, the framework
8
+ * to turn on `@WpAuthJwt({...})` endpoints. When NO JwtHook is bound, the framework
9
9
  * {@link AuthFilter} treats every jwt endpoint as "not enabled" and fails fast (401) — there is no
10
10
  * default JWT verification because it needs an app secret + payload shape the framework can't guess.
11
11
  *
@@ -14,12 +14,12 @@ const core_util_1 = require("@webpieces/core-util");
14
14
  * - `authorizeJwt` — AUTHORIZATION: check the authenticated user against the endpoint's
15
15
  * {@link JwtRequirement}. The DEFAULT enforces the roles any-of; override for
16
16
  * app-defined requirements carried by the SAME decorator, e.g.
17
- * `@AuthJwt({allRolesAllowed: true, inOrg: true})` →
17
+ * `@WpAuthJwt({allRolesAllowed: true, inOrg: true})` →
18
18
  * `if (requirement['inOrg'] && !values.claims['orgId']) ...`.
19
19
  *
20
20
  * BOTH ARE ASYNC, and both for the same reason: the strategy is the app's, and an app's strategy
21
21
  * reaches the network. `parseJwt` may fetch a JWKS or call a provider SDK; `authorizeJwt`'s own
22
- * motivating example — `@AuthJwt({allRolesAllowed: true, inOrg: true})` — is a membership question a
22
+ * motivating example — `@WpAuthJwt({allRolesAllowed: true, inOrg: true})` — is a membership question a
23
23
  * real app answers from a datastore. A sync signature makes both of those unwritable, and it made
24
24
  * `JwtHook` the last sync hook: {@link OidcHook.verifyOidc}, {@link WebhookAuthCallback.verifyWebhook} and
25
25
  * {@link ApiKeyHook.verifyApiKey} all return promises. An implementation that needs no I/O simply has
@@ -54,7 +54,7 @@ exports.JWT_HOOK = Symbol.for('JwtHook');
54
54
  * autobind; rebindable in tests). Bind one ONLY to customize the caller policy — e.g. an app that reads an
55
55
  * `ALLOWED_OIDC_CALLERS` env var at its composition root and enforces that allow-list. When NO
56
56
  * OidcHook is bound, the framework {@link AuthFilter} runs the built-in {@link DefaultOidcVerifier}
57
- * directly, so a server that wires nothing still verifies Google OIDC against its `@AuthOidc(...callers)`
57
+ * directly, so a server that wires nothing still verifies Google OIDC against its `@WpAuthOidc(...callers)`
58
58
  * (else trusts the edge — any Google-signed caller). `verifyOidc` verifies the token against `callers`;
59
59
  * throw on failure.
60
60
  */
@@ -69,7 +69,7 @@ exports.OidcHook = OidcHook;
69
69
  // webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)
70
70
  exports.OIDC_HOOK = Symbol.for('OidcHook');
71
71
  /**
72
- * WebhookAuthCallback - the OPTIONAL mechanism behind `@AuthWebhook(name)`: prove that an inbound request was
72
+ * WebhookAuthCallback - the OPTIONAL mechanism behind `@WpAuthWebhook(name)`: prove that an inbound request was
73
73
  * really authored by the outside vendor the contract names. Its DI token is the {@link WEBHOOK_AUTH_CALLBACK}
74
74
  * Symbol injected via `@inject(WEBHOOK_AUTH_CALLBACK)` (a Symbol, because the app container uses autobind;
75
75
  * rebindable in tests). The third hook, symmetric with {@link JwtHook} / {@link OidcHook}:
@@ -79,10 +79,10 @@ exports.OIDC_HOOK = Symbol.for('OidcHook');
79
79
  * options.bind(WEBHOOK_AUTH_CALLBACK).to(CompanyWebhookAuthCallback);
80
80
  * ```
81
81
  *
82
- * When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every `@AuthWebhook` endpoint,
82
+ * When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every `@WpAuthWebhook` endpoint,
83
83
  * exactly as it does for an unbound JwtHook. There is no framework default and there never will be
84
84
  * one: silently allowing an unverified webhook is the single default that must not exist, and the
85
- * framework ships no vendor crypto by design (see {@link AuthWebhook} for why reimplementing five
85
+ * framework ships no vendor crypto by design (see {@link WpAuthWebhook} for why reimplementing five
86
86
  * vendors' schemes is a losing trade).
87
87
  *
88
88
  * ONE hook serves EVERY vendor: `name` selects which, so an app with a Sentry hook and a Twilio hook
@@ -101,7 +101,7 @@ exports.WebhookAuthCallback = WebhookAuthCallback;
101
101
  // webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)
102
102
  exports.WEBHOOK_AUTH_CALLBACK = Symbol.for('WebhookAuthCallback');
103
103
  /**
104
- * ApiKeyHook - the OPTIONAL mechanism behind `@AuthApiKey(regime, credentials)`: authenticate a
104
+ * ApiKeyHook - the OPTIONAL mechanism behind `@WpAuthApiKey(regime, credentials)`: authenticate a
105
105
  * CUSTOMER-held api key
106
106
  * against the app's own datastore and return the context to seed. Its DI token is the
107
107
  * {@link API_KEY_HOOK} Symbol injected via `@inject(API_KEY_HOOK)` (a Symbol, because the app container
@@ -113,7 +113,7 @@ exports.WEBHOOK_AUTH_CALLBACK = Symbol.for('WebhookAuthCallback');
113
113
  * options.bind(API_KEY_HOOK).to(OneTabletApiKeyHook);
114
114
  * ```
115
115
  *
116
- * When NO ApiKeyHook is bound, the framework {@link AuthFilter} 401s every `@AuthApiKey` endpoint,
116
+ * When NO ApiKeyHook is bound, the framework {@link AuthFilter} 401s every `@WpAuthApiKey` endpoint,
117
117
  * exactly as it does for an unbound JwtHook. There is no framework default and there never will be
118
118
  * one: the key regime lives in the app's datastore, under the app's hashing scheme, behind the app's
119
119
  * choice of header names.
@@ -1 +1 @@
1
- {"version":3,"file":"AuthHooks.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooks.ts"],"names":[],"mappings":";;;AAAA,oDAAqF;AAIrF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAsB,OAAO;IAgBzB;;;;OAIG;IACH,KAAK,CAAC,YAAY,CAAC,MAA2B,EAAE,WAA2B;QACvE,qFAAqF;QACrF,0FAA0F;QAC1F,MAAM,KAAK,GAAG,IAAA,yBAAa,EAAC,WAAW,CAAC,CAAC;QACzC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACjF,MAAM,IAAI,0BAAc,CAAC,mCAAmC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACpF,CAAC;IACL,CAAC;CACJ;AA7BD,0BA6BC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAsB,QAAQ;CAE7B;AAFD,4BAEC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAsB,mBAAmB;CAuBxC;AAvBD,kDAuBC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAsB,UAAU;CAiB/B;AAjBD,gCAiBC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC","sourcesContent":["import { JwtRequirement, rolesRequired, ForbiddenError } from '@webpieces/core-util';\nimport { HttpRequest, RawHttpRequest } from '@webpieces/core-context';\nimport { AuthenticatedCaller } from './AuthConfig';\n\n/**\n * JwtHook - the OPTIONAL user-JWT mechanism. Its DI token is the {@link JWT_HOOK} Symbol injected via\n * `@inject(JWT_HOOK)` (a Symbol, because the app container uses autobind; rebindable in tests). Bind one\n * to turn on `@AuthJwt({...})` endpoints. When NO JwtHook is bound, the framework\n * {@link AuthFilter} treats every jwt endpoint as \"not enabled\" and fails fast (401) — there is no\n * default JWT verification because it needs an app secret + payload shape the framework can't guess.\n *\n * - `parseJwt` — AUTHENTICATION: decode/verify a user JWT into {@link AuthenticatedCaller}, or throw. The\n * app owns the strategy (HS256 secret, RS256 + JWKS, a provider SDK, ...).\n * - `authorizeJwt` — AUTHORIZATION: check the authenticated user against the endpoint's\n * {@link JwtRequirement}. The DEFAULT enforces the roles any-of; override for\n * app-defined requirements carried by the SAME decorator, e.g.\n * `@AuthJwt({allRolesAllowed: true, inOrg: true})` →\n * `if (requirement['inOrg'] && !values.claims['orgId']) ...`.\n *\n * BOTH ARE ASYNC, and both for the same reason: the strategy is the app's, and an app's strategy\n * reaches the network. `parseJwt` may fetch a JWKS or call a provider SDK; `authorizeJwt`'s own\n * motivating example — `@AuthJwt({allRolesAllowed: true, inOrg: true})` — is a membership question a\n * real app answers from a datastore. A sync signature makes both of those unwritable, and it made\n * `JwtHook` the last sync hook: {@link OidcHook.verifyOidc}, {@link WebhookAuthCallback.verifyWebhook} and\n * {@link ApiKeyHook.verifyApiKey} all return promises. An implementation that needs no I/O simply has\n * no `await` in its body — {@link DefaultJwtHook} is exactly that and pays nothing for it.\n */\nexport abstract class JwtHook {\n /**\n * Parse a user JWT (kind:'jwt') — AUTHENTICATION only. Return who the user is, or throw.\n * ASYNC so an app can reach a JWKS endpoint or a provider SDK; see the class doc.\n *\n * IT TAKES THE TOKEN, NOT THE REQUEST — the one deliberate asymmetry among the four hooks, and\n * NOT an oversight to be \"fixed\". {@link ApiKeyHook.verifyApiKey} and\n * {@link WebhookAuthCallback.verifyWebhook} take the whole {@link HttpRequest} because their\n * credential regime is the APP's: which headers carry an api key, and how a vendor signs, are\n * things the framework cannot know. A user JWT is different — the framework owns the\n * `Authorization: Bearer` scheme and has already extracted the token from it. Widening this to\n * the request would only invite a JwtHook to authenticate off some OTHER header, which is a\n * second, ungoverned credential path on the mode that guards browser traffic.\n */\n abstract parseJwt(token: string): Promise<AuthenticatedCaller>;\n\n /**\n * DEFAULT authorization: enforce the endpoint's roles (any-of). Override to enforce app-defined\n * requirements. Throw ForbiddenError to deny; return to allow. ASYNC so an app-defined\n * requirement can be answered from a datastore; see the class doc.\n */\n async authorizeJwt(caller: AuthenticatedCaller, requirement: JwtRequirement): Promise<void> {\n // rolesRequired is the ONE reader of the JwtRoles union: [] means the endpoint typed\n // `allRolesAllowed: true`, never \"the field was missing\" — that state no longer compiles.\n const roles = rolesRequired(requirement);\n if (roles.length > 0 && !roles.some((role: string) => caller.roles.includes(role))) {\n throw new ForbiddenError(`Endpoint requires one of roles: ${roles.join(', ')}`);\n }\n }\n}\n\n/**\n * DI identifier for the optional {@link JwtHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(JWT_HOOK)`\n * correct — undefined when unbound. The JwtHook class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const JWT_HOOK = Symbol.for('JwtHook');\n\n/**\n * OidcHook - the OPTIONAL override for Google OIDC service-to-service verification. Its DI token is the\n * {@link OIDC_HOOK} Symbol injected via `@inject(OIDC_HOOK)` (a Symbol, because the app container uses\n * autobind; rebindable in tests). Bind one ONLY to customize the caller policy — e.g. an app that reads an\n * `ALLOWED_OIDC_CALLERS` env var at its composition root and enforces that allow-list. When NO\n * OidcHook is bound, the framework {@link AuthFilter} runs the built-in {@link DefaultOidcVerifier}\n * directly, so a server that wires nothing still verifies Google OIDC against its `@AuthOidc(...callers)`\n * (else trusts the edge — any Google-signed caller). `verifyOidc` verifies the token against `callers`;\n * throw on failure.\n */\nexport abstract class OidcHook {\n abstract verifyOidc(token: string, callers: string[]): Promise<void>;\n}\n\n/**\n * DI identifier for the optional {@link OidcHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(OIDC_HOOK)`\n * correct — undefined when unbound. The OidcHook class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const OIDC_HOOK = Symbol.for('OidcHook');\n\n/**\n * WebhookAuthCallback - the OPTIONAL mechanism behind `@AuthWebhook(name)`: prove that an inbound request was\n * really authored by the outside vendor the contract names. Its DI token is the {@link WEBHOOK_AUTH_CALLBACK}\n * Symbol injected via `@inject(WEBHOOK_AUTH_CALLBACK)` (a Symbol, because the app container uses autobind;\n * rebindable in tests). The third hook, symmetric with {@link JwtHook} / {@link OidcHook}:\n *\n * ```typescript\n * // AppModule.ts, beside the CompanyJwtHook binding\n * options.bind(WEBHOOK_AUTH_CALLBACK).to(CompanyWebhookAuthCallback);\n * ```\n *\n * When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every `@AuthWebhook` endpoint,\n * exactly as it does for an unbound JwtHook. There is no framework default and there never will be\n * one: silently allowing an unverified webhook is the single default that must not exist, and the\n * framework ships no vendor crypto by design (see {@link AuthWebhook} for why reimplementing five\n * vendors' schemes is a losing trade).\n *\n * ONE hook serves EVERY vendor: `name` selects which, so an app with a Sentry hook and a Twilio hook\n * switches on it rather than binding a token per vendor. What arrives is enough of the raw request to\n * call the vendor's OWN validator — `request.raw.rawBody` for a body-signing vendor (Sentry, GitHub,\n * Stripe, Slack), `request.raw.absoluteUrl` for one that signs the url instead (Twilio).\n */\nexport abstract class WebhookAuthCallback {\n /**\n * Verify ONE inbound request. Return the {@link AuthenticatedCaller} the vendor's signature\n * proved; throw {@link UnauthorizedError} to deny.\n *\n * IT RETURNS A CALLER, not `void`, for the same reason {@link ApiKeyHook.verifyApiKey} does: once\n * the signature checks out, the payload's vendor account is a PROVEN fact, and a hook that could\n * only return `void` had no way to say so. The framework seeds `entries` with\n * `RequestContext.putTrusted` exactly as it does for a jwt or api-key caller, so a controller\n * reads which vendor account this webhook is for off the context instead of re-deriving it.\n *\n * Return only what THIS hook proved from the signature it just verified. `webhook` remains\n * caller-NOT-verified (see `AuthFilter.verifiesCaller`): a vendor is not a peer service, so\n * nothing the vendor merely ASSERTED on the wire is admitted.\n *\n * @param name the string on the contract's `@AuthWebhook(name)` — which vendor this route is.\n * @param request the transport-neutral request, narrowed to {@link RawHttpRequest}: `request.raw`\n * holds the verbatim bytes + absolute url and is PRESENT, never optional.\n * `@AuthWebhook` requires `@Endpoint(..., { rawBody: true })` at wiring time, and\n * AuthFilter 401s rather than calling this hook with nothing to check — so an\n * implementation never writes `raw!` or a guard of its own.\n */\n abstract verifyWebhook(name: string, request: RawHttpRequest): Promise<AuthenticatedCaller>;\n}\n\n/**\n * DI identifier for the optional {@link WebhookAuthCallback} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(WEBHOOK_AUTH_CALLBACK)`\n * correct — undefined when unbound. The WebhookAuthCallback class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const WEBHOOK_AUTH_CALLBACK = Symbol.for('WebhookAuthCallback');\n\n/**\n * ApiKeyHook - the OPTIONAL mechanism behind `@AuthApiKey(regime, credentials)`: authenticate a\n * CUSTOMER-held api key\n * against the app's own datastore and return the context to seed. Its DI token is the\n * {@link API_KEY_HOOK} Symbol injected via `@inject(API_KEY_HOOK)` (a Symbol, because the app container\n * uses autobind; rebindable in tests). The fourth hook, symmetric with {@link JwtHook} /\n * {@link OidcHook} / {@link WebhookAuthCallback}:\n *\n * ```typescript\n * // AppModule.ts, beside the CompanyJwtHook binding\n * options.bind(API_KEY_HOOK).to(OneTabletApiKeyHook);\n * ```\n *\n * When NO ApiKeyHook is bound, the framework {@link AuthFilter} 401s every `@AuthApiKey` endpoint,\n * exactly as it does for an unbound JwtHook. There is no framework default and there never will be\n * one: the key regime lives in the app's datastore, under the app's hashing scheme, behind the app's\n * choice of header names.\n *\n * THE ONE THING THIS HAS THAT `JwtHook.parseJwt` DOES NOT: it receives the whole {@link HttpRequest},\n * not one pre-extracted token. A real key regime validates the key TOGETHER WITH a second header — the\n * organization the caller is acting for — and a hook handed one header's value physically cannot do\n * that cross-check. The framework therefore EXTRACTS no api-key header: which headers carry the\n * credential is the app's business, and `getHeader` / `getHeaderValues` read as many as the regime\n * needs. The contract's `credentials` list DECLARES those header names for readers and spec\n * generators — it is documentation of the regime, never an instruction to this hook, so the hook\n * stays the single place the pair is actually validated. (Being ASYNC is no longer a difference — every hook here is, and for the same reason: an\n * app's strategy reaches the network.)\n *\n * ONE hook serves EVERY regime: `regime` selects which, so a server with a partner-api regime and an\n * internal-tooling regime switches on it rather than binding a token per regime.\n */\nexport abstract class ApiKeyHook {\n /**\n * AUTHENTICATE one inbound request. Return who the caller is plus the {@link AuthenticatedCaller.entries}\n * the framework seeds into `RequestContext` via `putTrusted`, or throw\n * {@link UnauthorizedError} to deny.\n *\n * NOTE the seeded entries are TRUSTED context keys, so return only what THIS hook proved from the\n * credential it just verified. Anything the caller merely asserted on the wire is not admitted by\n * `@AuthApiKey` — the mode is deliberately caller-NOT-verified (see `AuthFilter.verifiesCaller`),\n * because a customer is not an internal service.\n *\n * @param regime the first argument of the contract's `@AuthApiKey(regime, credentials)` — which key\n * regime this route belongs to.\n * @param request the inbound request; read as many headers as the regime needs with\n * `getHeader` / `getHeaderValues`, either by raw name or by {@link ContextKey}.\n */\n abstract verifyApiKey(regime: string, request: HttpRequest): Promise<AuthenticatedCaller>;\n}\n\n/**\n * DI identifier for the optional {@link ApiKeyHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(API_KEY_HOOK)`\n * correct — undefined when unbound. The ApiKeyHook class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const API_KEY_HOOK = Symbol.for('ApiKeyHook');\n"]}
1
+ {"version":3,"file":"AuthHooks.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooks.ts"],"names":[],"mappings":";;;AAAA,oDAAqF;AAIrF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAsB,OAAO;IAgBzB;;;;OAIG;IACH,KAAK,CAAC,YAAY,CAAC,MAA2B,EAAE,WAA2B;QACvE,qFAAqF;QACrF,0FAA0F;QAC1F,MAAM,KAAK,GAAG,IAAA,yBAAa,EAAC,WAAW,CAAC,CAAC;QACzC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACjF,MAAM,IAAI,0BAAc,CAAC,mCAAmC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACpF,CAAC;IACL,CAAC;CACJ;AA7BD,0BA6BC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAsB,QAAQ;CAE7B;AAFD,4BAEC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAsB,mBAAmB;CAuBxC;AAvBD,kDAuBC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAsB,UAAU;CAiB/B;AAjBD,gCAiBC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC","sourcesContent":["import { JwtRequirement, rolesRequired, ForbiddenError } from '@webpieces/core-util';\nimport { HttpRequest, RawHttpRequest } from '@webpieces/core-context';\nimport { AuthenticatedCaller } from './AuthConfig';\n\n/**\n * JwtHook - the OPTIONAL user-JWT mechanism. Its DI token is the {@link JWT_HOOK} Symbol injected via\n * `@inject(JWT_HOOK)` (a Symbol, because the app container uses autobind; rebindable in tests). Bind one\n * to turn on `@WpAuthJwt({...})` endpoints. When NO JwtHook is bound, the framework\n * {@link AuthFilter} treats every jwt endpoint as \"not enabled\" and fails fast (401) — there is no\n * default JWT verification because it needs an app secret + payload shape the framework can't guess.\n *\n * - `parseJwt` — AUTHENTICATION: decode/verify a user JWT into {@link AuthenticatedCaller}, or throw. The\n * app owns the strategy (HS256 secret, RS256 + JWKS, a provider SDK, ...).\n * - `authorizeJwt` — AUTHORIZATION: check the authenticated user against the endpoint's\n * {@link JwtRequirement}. The DEFAULT enforces the roles any-of; override for\n * app-defined requirements carried by the SAME decorator, e.g.\n * `@WpAuthJwt({allRolesAllowed: true, inOrg: true})` →\n * `if (requirement['inOrg'] && !values.claims['orgId']) ...`.\n *\n * BOTH ARE ASYNC, and both for the same reason: the strategy is the app's, and an app's strategy\n * reaches the network. `parseJwt` may fetch a JWKS or call a provider SDK; `authorizeJwt`'s own\n * motivating example — `@WpAuthJwt({allRolesAllowed: true, inOrg: true})` — is a membership question a\n * real app answers from a datastore. A sync signature makes both of those unwritable, and it made\n * `JwtHook` the last sync hook: {@link OidcHook.verifyOidc}, {@link WebhookAuthCallback.verifyWebhook} and\n * {@link ApiKeyHook.verifyApiKey} all return promises. An implementation that needs no I/O simply has\n * no `await` in its body — {@link DefaultJwtHook} is exactly that and pays nothing for it.\n */\nexport abstract class JwtHook {\n /**\n * Parse a user JWT (kind:'jwt') — AUTHENTICATION only. Return who the user is, or throw.\n * ASYNC so an app can reach a JWKS endpoint or a provider SDK; see the class doc.\n *\n * IT TAKES THE TOKEN, NOT THE REQUEST — the one deliberate asymmetry among the four hooks, and\n * NOT an oversight to be \"fixed\". {@link ApiKeyHook.verifyApiKey} and\n * {@link WebhookAuthCallback.verifyWebhook} take the whole {@link HttpRequest} because their\n * credential regime is the APP's: which headers carry an api key, and how a vendor signs, are\n * things the framework cannot know. A user JWT is different — the framework owns the\n * `Authorization: Bearer` scheme and has already extracted the token from it. Widening this to\n * the request would only invite a JwtHook to authenticate off some OTHER header, which is a\n * second, ungoverned credential path on the mode that guards browser traffic.\n */\n abstract parseJwt(token: string): Promise<AuthenticatedCaller>;\n\n /**\n * DEFAULT authorization: enforce the endpoint's roles (any-of). Override to enforce app-defined\n * requirements. Throw ForbiddenError to deny; return to allow. ASYNC so an app-defined\n * requirement can be answered from a datastore; see the class doc.\n */\n async authorizeJwt(caller: AuthenticatedCaller, requirement: JwtRequirement): Promise<void> {\n // rolesRequired is the ONE reader of the JwtRoles union: [] means the endpoint typed\n // `allRolesAllowed: true`, never \"the field was missing\" — that state no longer compiles.\n const roles = rolesRequired(requirement);\n if (roles.length > 0 && !roles.some((role: string) => caller.roles.includes(role))) {\n throw new ForbiddenError(`Endpoint requires one of roles: ${roles.join(', ')}`);\n }\n }\n}\n\n/**\n * DI identifier for the optional {@link JwtHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(JWT_HOOK)`\n * correct — undefined when unbound. The JwtHook class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const JWT_HOOK = Symbol.for('JwtHook');\n\n/**\n * OidcHook - the OPTIONAL override for Google OIDC service-to-service verification. Its DI token is the\n * {@link OIDC_HOOK} Symbol injected via `@inject(OIDC_HOOK)` (a Symbol, because the app container uses\n * autobind; rebindable in tests). Bind one ONLY to customize the caller policy — e.g. an app that reads an\n * `ALLOWED_OIDC_CALLERS` env var at its composition root and enforces that allow-list. When NO\n * OidcHook is bound, the framework {@link AuthFilter} runs the built-in {@link DefaultOidcVerifier}\n * directly, so a server that wires nothing still verifies Google OIDC against its `@WpAuthOidc(...callers)`\n * (else trusts the edge — any Google-signed caller). `verifyOidc` verifies the token against `callers`;\n * throw on failure.\n */\nexport abstract class OidcHook {\n abstract verifyOidc(token: string, callers: string[]): Promise<void>;\n}\n\n/**\n * DI identifier for the optional {@link OidcHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(OIDC_HOOK)`\n * correct — undefined when unbound. The OidcHook class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const OIDC_HOOK = Symbol.for('OidcHook');\n\n/**\n * WebhookAuthCallback - the OPTIONAL mechanism behind `@WpAuthWebhook(name)`: prove that an inbound request was\n * really authored by the outside vendor the contract names. Its DI token is the {@link WEBHOOK_AUTH_CALLBACK}\n * Symbol injected via `@inject(WEBHOOK_AUTH_CALLBACK)` (a Symbol, because the app container uses autobind;\n * rebindable in tests). The third hook, symmetric with {@link JwtHook} / {@link OidcHook}:\n *\n * ```typescript\n * // AppModule.ts, beside the CompanyJwtHook binding\n * options.bind(WEBHOOK_AUTH_CALLBACK).to(CompanyWebhookAuthCallback);\n * ```\n *\n * When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every `@WpAuthWebhook` endpoint,\n * exactly as it does for an unbound JwtHook. There is no framework default and there never will be\n * one: silently allowing an unverified webhook is the single default that must not exist, and the\n * framework ships no vendor crypto by design (see {@link WpAuthWebhook} for why reimplementing five\n * vendors' schemes is a losing trade).\n *\n * ONE hook serves EVERY vendor: `name` selects which, so an app with a Sentry hook and a Twilio hook\n * switches on it rather than binding a token per vendor. What arrives is enough of the raw request to\n * call the vendor's OWN validator — `request.raw.rawBody` for a body-signing vendor (Sentry, GitHub,\n * Stripe, Slack), `request.raw.absoluteUrl` for one that signs the url instead (Twilio).\n */\nexport abstract class WebhookAuthCallback {\n /**\n * Verify ONE inbound request. Return the {@link AuthenticatedCaller} the vendor's signature\n * proved; throw {@link UnauthorizedError} to deny.\n *\n * IT RETURNS A CALLER, not `void`, for the same reason {@link ApiKeyHook.verifyApiKey} does: once\n * the signature checks out, the payload's vendor account is a PROVEN fact, and a hook that could\n * only return `void` had no way to say so. The framework seeds `entries` with\n * `RequestContext.putTrusted` exactly as it does for a jwt or api-key caller, so a controller\n * reads which vendor account this webhook is for off the context instead of re-deriving it.\n *\n * Return only what THIS hook proved from the signature it just verified. `webhook` remains\n * caller-NOT-verified (see `AuthFilter.verifiesCaller`): a vendor is not a peer service, so\n * nothing the vendor merely ASSERTED on the wire is admitted.\n *\n * @param name the string on the contract's `@WpAuthWebhook(name)` — which vendor this route is.\n * @param request the transport-neutral request, narrowed to {@link RawHttpRequest}: `request.raw`\n * holds the verbatim bytes + absolute url and is PRESENT, never optional.\n * `@WpAuthWebhook` requires `@Endpoint(..., { rawBody: true })` at wiring time, and\n * AuthFilter 401s rather than calling this hook with nothing to check — so an\n * implementation never writes `raw!` or a guard of its own.\n */\n abstract verifyWebhook(name: string, request: RawHttpRequest): Promise<AuthenticatedCaller>;\n}\n\n/**\n * DI identifier for the optional {@link WebhookAuthCallback} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(WEBHOOK_AUTH_CALLBACK)`\n * correct — undefined when unbound. The WebhookAuthCallback class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const WEBHOOK_AUTH_CALLBACK = Symbol.for('WebhookAuthCallback');\n\n/**\n * ApiKeyHook - the OPTIONAL mechanism behind `@WpAuthApiKey(regime, credentials)`: authenticate a\n * CUSTOMER-held api key\n * against the app's own datastore and return the context to seed. Its DI token is the\n * {@link API_KEY_HOOK} Symbol injected via `@inject(API_KEY_HOOK)` (a Symbol, because the app container\n * uses autobind; rebindable in tests). The fourth hook, symmetric with {@link JwtHook} /\n * {@link OidcHook} / {@link WebhookAuthCallback}:\n *\n * ```typescript\n * // AppModule.ts, beside the CompanyJwtHook binding\n * options.bind(API_KEY_HOOK).to(OneTabletApiKeyHook);\n * ```\n *\n * When NO ApiKeyHook is bound, the framework {@link AuthFilter} 401s every `@WpAuthApiKey` endpoint,\n * exactly as it does for an unbound JwtHook. There is no framework default and there never will be\n * one: the key regime lives in the app's datastore, under the app's hashing scheme, behind the app's\n * choice of header names.\n *\n * THE ONE THING THIS HAS THAT `JwtHook.parseJwt` DOES NOT: it receives the whole {@link HttpRequest},\n * not one pre-extracted token. A real key regime validates the key TOGETHER WITH a second header — the\n * organization the caller is acting for — and a hook handed one header's value physically cannot do\n * that cross-check. The framework therefore EXTRACTS no api-key header: which headers carry the\n * credential is the app's business, and `getHeader` / `getHeaderValues` read as many as the regime\n * needs. The contract's `credentials` list DECLARES those header names for readers and spec\n * generators — it is documentation of the regime, never an instruction to this hook, so the hook\n * stays the single place the pair is actually validated. (Being ASYNC is no longer a difference — every hook here is, and for the same reason: an\n * app's strategy reaches the network.)\n *\n * ONE hook serves EVERY regime: `regime` selects which, so a server with a partner-api regime and an\n * internal-tooling regime switches on it rather than binding a token per regime.\n */\nexport abstract class ApiKeyHook {\n /**\n * AUTHENTICATE one inbound request. Return who the caller is plus the {@link AuthenticatedCaller.entries}\n * the framework seeds into `RequestContext` via `putTrusted`, or throw\n * {@link UnauthorizedError} to deny.\n *\n * NOTE the seeded entries are TRUSTED context keys, so return only what THIS hook proved from the\n * credential it just verified. Anything the caller merely asserted on the wire is not admitted by\n * `@WpAuthApiKey` — the mode is deliberately caller-NOT-verified (see `AuthFilter.verifiesCaller`),\n * because a customer is not an internal service.\n *\n * @param regime the first argument of the contract's `@WpAuthApiKey(regime, credentials)` — which key\n * regime this route belongs to.\n * @param request the inbound request; read as many headers as the regime needs with\n * `getHeader` / `getHeaderValues`, either by raw name or by {@link ContextKey}.\n */\n abstract verifyApiKey(regime: string, request: HttpRequest): Promise<AuthenticatedCaller>;\n}\n\n/**\n * DI identifier for the optional {@link ApiKeyHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(API_KEY_HOOK)`\n * correct — undefined when unbound. The ApiKeyHook class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const API_KEY_HOOK = Symbol.for('ApiKeyHook');\n"]}
@@ -3,7 +3,7 @@ import { AuthenticatedCaller } from './AuthConfig';
3
3
  /**
4
4
  * DefaultJwtHook - a batteries-included {@link JwtHook} for the common case: HS256 user JWTs signed
5
5
  * with ONE shared secret. Construct it with the secret and bind it — `new DefaultJwtHook(secret)` —
6
- * and `@AuthJwt` endpoints work with NO custom verification code.
6
+ * and `@WpAuthJwt` endpoints work with NO custom verification code.
7
7
  *
8
8
  * `parseJwt` verifies the signature + expiry (jsonwebtoken, HS256 only) and maps standard claims:
9
9
  * `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. `authorizeJwt`
@@ -8,7 +8,7 @@ const AuthConfig_1 = require("./AuthConfig");
8
8
  /**
9
9
  * DefaultJwtHook - a batteries-included {@link JwtHook} for the common case: HS256 user JWTs signed
10
10
  * with ONE shared secret. Construct it with the secret and bind it — `new DefaultJwtHook(secret)` —
11
- * and `@AuthJwt` endpoints work with NO custom verification code.
11
+ * and `@WpAuthJwt` endpoints work with NO custom verification code.
12
12
  *
13
13
  * `parseJwt` verifies the signature + expiry (jsonwebtoken, HS256 only) and maps standard claims:
14
14
  * `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. `authorizeJwt`
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultJwtHook.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/DefaultJwtHook.ts"],"names":[],"mappings":";;;AAAA,+CAAkD;AAClD,oDAAkE;AAClE,2CAAsC;AACtC,6CAAmD;AAEnD;;;;;;;;;;;;;;GAcG;AACH,MAAa,cAAe,SAAQ,mBAAO;IACtB,MAAM,CAAS;IAEhC,YAAY,MAAc;QACtB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAEQ,KAAK,CAAC,QAAQ,CAAC,KAAa;QACjC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;QAC3B,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,IAAI,6BAAiB,CAAC,mDAAmD,CAAC,CAAC;QACrF,CAAC;QACD,OAAO,IAAI,gCAAmB,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;IACpF,CAAC;IAED,gGAAgG;IACxF,WAAW,CAAC,KAAa;QAC7B,8QAA8Q;QAC9Q,IAAI,CAAC;YACD,MAAM,OAAO,GAAG,IAAA,qBAAM,EAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACtE,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAC9B,MAAM,IAAI,6BAAiB,CAAC,iDAAiD,CAAC,CAAC;YACnF,CAAC;YACD,OAAO,OAAO,CAAC;QACnB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,KAAK,YAAY,6BAAiB,EAAE,CAAC;gBACrC,MAAM,KAAK,CAAC;YAChB,CAAC;YACD,MAAM,IAAI,6BAAiB,CAAC,yBAAyB,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QAC7E,CAAC;IACL,CAAC;IAEO,YAAY,CAAC,OAAmB;QACpC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC;QACpE,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;CACJ;AA1CD,wCA0CC","sourcesContent":["import { verify, JwtPayload } from 'jsonwebtoken';\nimport { UnauthorizedError, toError } from '@webpieces/core-util';\nimport { JwtHook } from './AuthHooks';\nimport { AuthenticatedCaller } from './AuthConfig';\n\n/**\n * DefaultJwtHook - a batteries-included {@link JwtHook} for the common case: HS256 user JWTs signed\n * with ONE shared secret. Construct it with the secret and bind it — `new DefaultJwtHook(secret)` —\n * and `@AuthJwt` endpoints work with NO custom verification code.\n *\n * `parseJwt` verifies the signature + expiry (jsonwebtoken, HS256 only) and maps standard claims:\n * `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. `authorizeJwt`\n * (role enforcement) is inherited from JwtHook. For RS256 + JWKS, a provider SDK, or a non-standard\n * payload, write your own JwtHook subclass instead.\n *\n * It satisfies {@link JwtHook}'s ASYNC signature with a body that awaits NOTHING, and that is the\n * point rather than an oversight: HS256 against a local secret is pure CPU. The signature is async\n * because the hook is the APP's seam and an app's strategy reaches the network — not because this\n * implementation does. No fake await is added to justify it.\n */\nexport class DefaultJwtHook extends JwtHook {\n private readonly secret: string;\n\n constructor(secret: string) {\n super();\n this.secret = secret;\n }\n\n override async parseJwt(token: string): Promise<AuthenticatedCaller> {\n const payload = this.verifyToken(token);\n const userId = payload.sub;\n if (!userId) {\n throw new UnauthorizedError('JWT is missing the required \"sub\" (subject) claim');\n }\n return new AuthenticatedCaller(userId, this.extractRoles(payload), [], payload);\n }\n\n /** Verify HS256 signature + expiry; translate jsonwebtoken's raw error into a framework 401. */\n private verifyToken(token: string): JwtPayload {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- AUTH TRANSLATION CHOKEPOINT: jsonwebtoken.verify throws on a bad/expired token; that must surface as a 401 Unauthorized, not bubble to the global handler as a 500. The original error is chained via cause.\n try {\n const decoded = verify(token, this.secret, { algorithms: ['HS256'] });\n if (typeof decoded === 'string') {\n throw new UnauthorizedError('JWT payload must be a JSON object, not a string');\n }\n return decoded;\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof UnauthorizedError) {\n throw error;\n }\n throw new UnauthorizedError('JWT verification failed', undefined, error);\n }\n }\n\n private extractRoles(payload: JwtPayload): string[] {\n const roles = payload['roles'];\n if (Array.isArray(roles)) {\n return roles.filter((role: string) => typeof role === 'string');\n }\n return [];\n }\n}\n"]}
1
+ {"version":3,"file":"DefaultJwtHook.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/DefaultJwtHook.ts"],"names":[],"mappings":";;;AAAA,+CAAkD;AAClD,oDAAkE;AAClE,2CAAsC;AACtC,6CAAmD;AAEnD;;;;;;;;;;;;;;GAcG;AACH,MAAa,cAAe,SAAQ,mBAAO;IACtB,MAAM,CAAS;IAEhC,YAAY,MAAc;QACtB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAEQ,KAAK,CAAC,QAAQ,CAAC,KAAa;QACjC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;QAC3B,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,IAAI,6BAAiB,CAAC,mDAAmD,CAAC,CAAC;QACrF,CAAC;QACD,OAAO,IAAI,gCAAmB,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;IACpF,CAAC;IAED,gGAAgG;IACxF,WAAW,CAAC,KAAa;QAC7B,8QAA8Q;QAC9Q,IAAI,CAAC;YACD,MAAM,OAAO,GAAG,IAAA,qBAAM,EAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACtE,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAC9B,MAAM,IAAI,6BAAiB,CAAC,iDAAiD,CAAC,CAAC;YACnF,CAAC;YACD,OAAO,OAAO,CAAC;QACnB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,KAAK,YAAY,6BAAiB,EAAE,CAAC;gBACrC,MAAM,KAAK,CAAC;YAChB,CAAC;YACD,MAAM,IAAI,6BAAiB,CAAC,yBAAyB,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QAC7E,CAAC;IACL,CAAC;IAEO,YAAY,CAAC,OAAmB;QACpC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC;QACpE,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;CACJ;AA1CD,wCA0CC","sourcesContent":["import { verify, JwtPayload } from 'jsonwebtoken';\nimport { UnauthorizedError, toError } from '@webpieces/core-util';\nimport { JwtHook } from './AuthHooks';\nimport { AuthenticatedCaller } from './AuthConfig';\n\n/**\n * DefaultJwtHook - a batteries-included {@link JwtHook} for the common case: HS256 user JWTs signed\n * with ONE shared secret. Construct it with the secret and bind it — `new DefaultJwtHook(secret)` —\n * and `@WpAuthJwt` endpoints work with NO custom verification code.\n *\n * `parseJwt` verifies the signature + expiry (jsonwebtoken, HS256 only) and maps standard claims:\n * `sub` → userId, a string[] `roles` claim → roles, the whole payload → claims. `authorizeJwt`\n * (role enforcement) is inherited from JwtHook. For RS256 + JWKS, a provider SDK, or a non-standard\n * payload, write your own JwtHook subclass instead.\n *\n * It satisfies {@link JwtHook}'s ASYNC signature with a body that awaits NOTHING, and that is the\n * point rather than an oversight: HS256 against a local secret is pure CPU. The signature is async\n * because the hook is the APP's seam and an app's strategy reaches the network — not because this\n * implementation does. No fake await is added to justify it.\n */\nexport class DefaultJwtHook extends JwtHook {\n private readonly secret: string;\n\n constructor(secret: string) {\n super();\n this.secret = secret;\n }\n\n override async parseJwt(token: string): Promise<AuthenticatedCaller> {\n const payload = this.verifyToken(token);\n const userId = payload.sub;\n if (!userId) {\n throw new UnauthorizedError('JWT is missing the required \"sub\" (subject) claim');\n }\n return new AuthenticatedCaller(userId, this.extractRoles(payload), [], payload);\n }\n\n /** Verify HS256 signature + expiry; translate jsonwebtoken's raw error into a framework 401. */\n private verifyToken(token: string): JwtPayload {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- AUTH TRANSLATION CHOKEPOINT: jsonwebtoken.verify throws on a bad/expired token; that must surface as a 401 Unauthorized, not bubble to the global handler as a 500. The original error is chained via cause.\n try {\n const decoded = verify(token, this.secret, { algorithms: ['HS256'] });\n if (typeof decoded === 'string') {\n throw new UnauthorizedError('JWT payload must be a JSON object, not a string');\n }\n return decoded;\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof UnauthorizedError) {\n throw error;\n }\n throw new UnauthorizedError('JWT verification failed', undefined, error);\n }\n }\n\n private extractRoles(payload: JwtPayload): string[] {\n const roles = payload['roles'];\n if (Array.isArray(roles)) {\n return roles.filter((role: string) => typeof role === 'string');\n }\n return [];\n }\n}\n"]}
@@ -5,10 +5,10 @@ import { GcpOidc } from '@webpieces/gcp-identity';
5
5
  * OIDC "just work" with ZERO wiring: http-routing depends on @webpieces/gcp-identity ON PURPOSE so a
6
6
  * server that binds nothing still verifies service-to-service OIDC.
7
7
  *
8
- * `verify` honors the endpoint's `@AuthOidc(...callers)` contract exactly: EMPTY (a bare `@AuthOidc()`)
8
+ * `verify` honors the endpoint's `@WpAuthOidc(...callers)` contract exactly: EMPTY (a bare `@WpAuthOidc()`)
9
9
  * = TRUST THE EDGE — verify the token is genuinely Google-signed and let the deployment's `run.invoker`
10
10
  * IAM restrict WHO (gcp-identity warns loudly, once, if the service is actually public and thus the
11
- * edge is NOT the gate). A non-empty list (`@AuthOidc('svc-a')`) enforces that explicit app-level
11
+ * edge is NOT the gate). A non-empty list (`@WpAuthOidc('svc-a')`) enforces that explicit app-level
12
12
  * allow-list as defense-in-depth. Off-GCP, gcp-identity mints + accepts a dev token so local dev needs
13
13
  * no GCP. Framework code reads NO process.env — an app that wants an env-driven allow-list binds an
14
14
  * {@link OidcHook} instead (env read at its composition root), keeping this default env-free and tests
@@ -12,10 +12,10 @@ const gcp_identity_1 = require("@webpieces/gcp-identity");
12
12
  * OIDC "just work" with ZERO wiring: http-routing depends on @webpieces/gcp-identity ON PURPOSE so a
13
13
  * server that binds nothing still verifies service-to-service OIDC.
14
14
  *
15
- * `verify` honors the endpoint's `@AuthOidc(...callers)` contract exactly: EMPTY (a bare `@AuthOidc()`)
15
+ * `verify` honors the endpoint's `@WpAuthOidc(...callers)` contract exactly: EMPTY (a bare `@WpAuthOidc()`)
16
16
  * = TRUST THE EDGE — verify the token is genuinely Google-signed and let the deployment's `run.invoker`
17
17
  * IAM restrict WHO (gcp-identity warns loudly, once, if the service is actually public and thus the
18
- * edge is NOT the gate). A non-empty list (`@AuthOidc('svc-a')`) enforces that explicit app-level
18
+ * edge is NOT the gate). A non-empty list (`@WpAuthOidc('svc-a')`) enforces that explicit app-level
19
19
  * allow-list as defense-in-depth. Off-GCP, gcp-identity mints + accepts a dev token so local dev needs
20
20
  * no GCP. Framework code reads NO process.env — an app that wants an env-driven allow-list binds an
21
21
  * {@link OidcHook} instead (env read at its composition root), keeping this default env-free and tests
@@ -27,7 +27,7 @@ let DefaultOidcVerifier = class DefaultOidcVerifier {
27
27
  this.gcpOidc = gcpOidc;
28
28
  }
29
29
  async verify(token, callers) {
30
- // callers pass straight through: EMPTY (@AuthOidc() with no callers) = TRUST THE EDGE, a
30
+ // callers pass straight through: EMPTY (@WpAuthOidc() with no callers) = TRUST THE EDGE, a
31
31
  // non-empty list enforces the explicit allow-list. Do NOT inject a ['self'] default — that
32
32
  // would reject a legitimate cross-SA caller the edge already admitted.
33
33
  const result = await this.gcpOidc.verifyFromCallers(token, callers);
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultOidcVerifier.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/DefaultOidcVerifier.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,0DAAoE;AACpE,oDAAyD;AACzD,0DAAkD;AAElD;;;;;;;;;;;;;;GAcG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAGU;IAFtC,YAEsC,OAAgB;QAAhB,YAAO,GAAP,OAAO,CAAS;IACnD,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,OAAiB;QACzC,yFAAyF;QACzF,2FAA2F;QAC3F,uEAAuE;QACvE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACpE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACb,MAAM,IAAI,6BAAiB,CAAC,kBAAkB,MAAM,CAAC,MAAM,IAAI,uBAAuB,EAAE,CAAC,CAAC;QAC9F,CAAC;IACL,CAAC;CACJ,CAAA;AAfY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,wCAAyB,GAAE;IAInB,mBAAA,IAAA,kBAAM,EAAC,sBAAO,CAAC,CAAA;6CAA2B,sBAAO;GAH7C,mBAAmB,CAe/B","sourcesContent":["import { inject } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { UnauthorizedError } from '@webpieces/core-util';\nimport { GcpOidc } from '@webpieces/gcp-identity';\n\n/**\n * DefaultOidcVerifier - the framework's built-in Google OIDC verifier, injected into\n * {@link AuthFilter} and run directly whenever no app {@link OidcHook} is bound. It is what makes\n * OIDC \"just work\" with ZERO wiring: http-routing depends on @webpieces/gcp-identity ON PURPOSE so a\n * server that binds nothing still verifies service-to-service OIDC.\n *\n * `verify` honors the endpoint's `@AuthOidc(...callers)` contract exactly: EMPTY (a bare `@AuthOidc()`)\n * = TRUST THE EDGE — verify the token is genuinely Google-signed and let the deployment's `run.invoker`\n * IAM restrict WHO (gcp-identity warns loudly, once, if the service is actually public and thus the\n * edge is NOT the gate). A non-empty list (`@AuthOidc('svc-a')`) enforces that explicit app-level\n * allow-list as defense-in-depth. Off-GCP, gcp-identity mints + accepts a dev token so local dev needs\n * no GCP. Framework code reads NO process.env — an app that wants an env-driven allow-list binds an\n * {@link OidcHook} instead (env read at its composition root), keeping this default env-free and tests\n * parallel-safe.\n */\n@provideFrameworkSingleton()\nexport class DefaultOidcVerifier {\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n ) {}\n\n async verify(token: string, callers: string[]): Promise<void> {\n // callers pass straight through: EMPTY (@AuthOidc() with no callers) = TRUST THE EDGE, a\n // non-empty list enforces the explicit allow-list. Do NOT inject a ['self'] default — that\n // would reject a legitimate cross-SA caller the edge already admitted.\n const result = await this.gcpOidc.verifyFromCallers(token, callers);\n if (!result.ok) {\n throw new UnauthorizedError(`OIDC rejected: ${result.reason ?? 'not an allowed caller'}`);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"DefaultOidcVerifier.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/DefaultOidcVerifier.ts"],"names":[],"mappings":";;;;AAAA,yCAAmC;AACnC,0DAAoE;AACpE,oDAAyD;AACzD,0DAAkD;AAElD;;;;;;;;;;;;;;GAcG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAGU;IAFtC,YAEsC,OAAgB;QAAhB,YAAO,GAAP,OAAO,CAAS;IACnD,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,OAAiB;QACzC,2FAA2F;QAC3F,2FAA2F;QAC3F,uEAAuE;QACvE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACpE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACb,MAAM,IAAI,6BAAiB,CACvB,kBAAkB,MAAM,CAAC,MAAM,IAAI,uBAAuB,EAAE,CAC/D,CAAC;QACN,CAAC;IACL,CAAC;CACJ,CAAA;AAjBY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,wCAAyB,GAAE;IAInB,mBAAA,IAAA,kBAAM,EAAC,sBAAO,CAAC,CAAA;6CAA2B,sBAAO;GAH7C,mBAAmB,CAiB/B","sourcesContent":["import { inject } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { UnauthorizedError } from '@webpieces/core-util';\nimport { GcpOidc } from '@webpieces/gcp-identity';\n\n/**\n * DefaultOidcVerifier - the framework's built-in Google OIDC verifier, injected into\n * {@link AuthFilter} and run directly whenever no app {@link OidcHook} is bound. It is what makes\n * OIDC \"just work\" with ZERO wiring: http-routing depends on @webpieces/gcp-identity ON PURPOSE so a\n * server that binds nothing still verifies service-to-service OIDC.\n *\n * `verify` honors the endpoint's `@WpAuthOidc(...callers)` contract exactly: EMPTY (a bare `@WpAuthOidc()`)\n * = TRUST THE EDGE — verify the token is genuinely Google-signed and let the deployment's `run.invoker`\n * IAM restrict WHO (gcp-identity warns loudly, once, if the service is actually public and thus the\n * edge is NOT the gate). A non-empty list (`@WpAuthOidc('svc-a')`) enforces that explicit app-level\n * allow-list as defense-in-depth. Off-GCP, gcp-identity mints + accepts a dev token so local dev needs\n * no GCP. Framework code reads NO process.env — an app that wants an env-driven allow-list binds an\n * {@link OidcHook} instead (env read at its composition root), keeping this default env-free and tests\n * parallel-safe.\n */\n@provideFrameworkSingleton()\nexport class DefaultOidcVerifier {\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n ) {}\n\n async verify(token: string, callers: string[]): Promise<void> {\n // callers pass straight through: EMPTY (@WpAuthOidc() with no callers) = TRUST THE EDGE, a\n // non-empty list enforces the explicit allow-list. Do NOT inject a ['self'] default — that\n // would reject a legitimate cross-SA caller the edge already admitted.\n const result = await this.gcpOidc.verifyFromCallers(token, callers);\n if (!result.ok) {\n throw new UnauthorizedError(\n `OIDC rejected: ${result.reason ?? 'not an allowed caller'}`,\n );\n }\n }\n}\n"]}
@@ -41,7 +41,7 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
41
41
  constructor(oidcVerifier: DefaultOidcVerifier, authConfig?: AuthConfig | undefined, jwtHook?: JwtHook | undefined, oidcHook?: OidcHook | undefined, webhookAuthCallback?: WebhookAuthCallback | undefined, apiKeyHook?: ApiKeyHook | undefined);
42
42
  filter(meta: MethodMeta, nextFilter: Service<MethodMeta, WpResponse<unknown>>): Promise<WpResponse<unknown>>;
43
43
  /**
44
- * `@AuthWebhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the
44
+ * `@WpAuthWebhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the
45
45
  * VENDOR's own validator. Three ways to fail, all 401, all before the controller is entered:
46
46
  *
47
47
  * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior;
@@ -65,7 +65,7 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
65
65
  */
66
66
  private hasRawBytes;
67
67
  /**
68
- * `@AuthApiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound
68
+ * `@WpAuthApiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound
69
69
  * headers and let it look the CUSTOMER's key up. The declared `credentials` are NOT read here — they
70
70
  * describe the contract for generators; the hook owns extraction. Three ways to fail, all 401, all
71
71
  * before the controller:
@@ -122,8 +122,8 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
122
122
  * Decide what happens to the trusted keys that arrived on the WIRE and were held back by
123
123
  * {@link PendingWireTrust} (read that class for why they are held rather than written).
124
124
  *
125
- * `callerVerified` — the endpoint authenticated the SENDER **as a peer service** (`@AuthOidc`,
126
- * `@AuthSharedSecret`).
125
+ * `callerVerified` — the endpoint authenticated the SENDER **as a peer service** (`@WpAuthOidc`,
126
+ * `@WpAuthSharedSecret`).
127
127
  * The sender is a service we trust, this is the service-to-service hop, and its forwarded
128
128
  * identity is admitted as-is. This is the case that makes propagating a verified userId across
129
129
  * internal services work.
@@ -154,7 +154,7 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
154
154
  */
155
155
  private requireVouched;
156
156
  /**
157
- * `@AuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the
157
+ * `@WpAuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the
158
158
  * endpoint did not exist.
159
159
  *
160
160
  * WHY 404 AND NOT THE 403 APPS HAND-ROLLED. Off-local the route is not registered at all
@@ -25,7 +25,7 @@ const AUTHORIZATION_HEADER = 'authorization';
25
25
  * can never be mistaken for a token, nor accepted where the other was expected:
26
26
  *
27
27
  * Authorization: Bearer <user JWT | service OIDC token>
28
- * Authorization: Webpieces <@AuthSharedSecret value>
28
+ * Authorization: Webpieces <@WpAuthSharedSecret value>
29
29
  *
30
30
  * The scheme is REQUIRED. A bare value with no scheme is rejected.
31
31
  */
@@ -110,7 +110,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
110
110
  return nextFilter.invoke(meta);
111
111
  }
112
112
  /**
113
- * `@AuthWebhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the
113
+ * `@WpAuthWebhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the
114
114
  * VENDOR's own validator. Three ways to fail, all 401, all before the controller is entered:
115
115
  *
116
116
  * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior;
@@ -127,13 +127,13 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
127
127
  */
128
128
  async enforceWebhook(name, meta) {
129
129
  if (!this.webhookAuthCallback) {
130
- log.warn(`Refusing @AuthWebhook('${name}') endpoint ${meta.routeMeta.path}: no WebhookAuthCallback is bound. ` +
130
+ log.warn(`Refusing @WpAuthWebhook('${name}') endpoint ${meta.routeMeta.path}: no WebhookAuthCallback is bound. ` +
131
131
  `Bind one (options.bind(WEBHOOK_AUTH_CALLBACK).to(YourWebhookAuthCallback)) to enable webhook verification.`);
132
132
  throw new core_util_1.UnauthorizedError('Webhook auth is not enabled on this server');
133
133
  }
134
134
  const request = core_context_1.RequestContext.getRequest();
135
135
  if (!this.hasRawBytes(request)) {
136
- log.warn(`Refusing @AuthWebhook('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +
136
+ log.warn(`Refusing @WpAuthWebhook('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +
137
137
  `no raw bytes. Declare @Endpoint(path, 'external', { calledBy: '${name}', rawBody: true }); a ` +
138
138
  `spec driving this route in-process must publish an HttpRequest built with a RawRequest.`);
139
139
  throw new core_util_1.UnauthorizedError('Webhook signature cannot be verified: no raw request was retained');
@@ -153,7 +153,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
153
153
  return request?.raw !== undefined;
154
154
  }
155
155
  /**
156
- * `@AuthApiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound
156
+ * `@WpAuthApiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound
157
157
  * headers and let it look the CUSTOMER's key up. The declared `credentials` are NOT read here — they
158
158
  * describe the contract for generators; the hook owns extraction. Three ways to fail, all 401, all
159
159
  * before the controller:
@@ -170,13 +170,13 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
170
170
  */
171
171
  async enforceApiKey(regime, meta) {
172
172
  if (!this.apiKeyHook) {
173
- log.warn(`Refusing @AuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no ApiKeyHook is bound. ` +
173
+ log.warn(`Refusing @WpAuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no ApiKeyHook is bound. ` +
174
174
  `Bind one (options.bind(API_KEY_HOOK).to(YourApiKeyHook)) to enable api-key verification.`);
175
175
  throw new core_util_1.UnauthorizedError('API-key auth is not enabled on this server');
176
176
  }
177
177
  const request = core_context_1.RequestContext.getRequest();
178
178
  if (!request) {
179
- log.warn(`Refusing @AuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no inbound HttpRequest is ` +
179
+ log.warn(`Refusing @WpAuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no inbound HttpRequest is ` +
180
180
  `in scope, so the hook has no headers to read. A spec driving this route in-process must ` +
181
181
  `publish an HttpRequest carrying the api-key headers.`);
182
182
  throw new core_util_1.UnauthorizedError('API key cannot be verified: no inbound request was published');
@@ -255,8 +255,8 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
255
255
  * Decide what happens to the trusted keys that arrived on the WIRE and were held back by
256
256
  * {@link PendingWireTrust} (read that class for why they are held rather than written).
257
257
  *
258
- * `callerVerified` — the endpoint authenticated the SENDER **as a peer service** (`@AuthOidc`,
259
- * `@AuthSharedSecret`).
258
+ * `callerVerified` — the endpoint authenticated the SENDER **as a peer service** (`@WpAuthOidc`,
259
+ * `@WpAuthSharedSecret`).
260
260
  * The sender is a service we trust, this is the service-to-service hop, and its forwarded
261
261
  * identity is admitted as-is. This is the case that makes propagating a verified userId across
262
262
  * internal services work.
@@ -302,11 +302,14 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
302
302
  }
303
303
  log.error(`Rejecting inbound '${item.key.httpHeader}': it is a TRUSTED context key, this route does ` +
304
304
  `not authenticate its caller, and the credential ` +
305
- (vouched === undefined ? 'vouched for no such value' : 'derived a different value') + '.');
305
+ (vouched === undefined
306
+ ? 'vouched for no such value'
307
+ : 'derived a different value') +
308
+ '.');
306
309
  throw new core_util_1.UnauthorizedError(`Header '${item.key.httpHeader}' cannot be supplied by the caller on this endpoint`);
307
310
  }
308
311
  /**
309
- * `@AuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the
312
+ * `@WpAuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the
310
313
  * endpoint did not exist.
311
314
  *
312
315
  * WHY 404 AND NOT THE 403 APPS HAND-ROLLED. Off-local the route is not registered at all
@@ -325,7 +328,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
325
328
  if (core_util_1.RuntimeLocality.isLocalDevelopment()) {
326
329
  return;
327
330
  }
328
- log.warn(`Refusing @AuthLocalOnly endpoint ${meta.routeMeta.path}: ` +
331
+ log.warn(`Refusing @WpAuthLocalOnly endpoint ${meta.routeMeta.path}: ` +
329
332
  (core_util_1.RuntimeLocality.isDeclared()
330
333
  ? 'this process declared itself DEPLOYED.'
331
334
  : 'no startup declared a RuntimeLocality, so this process is treated as DEPLOYED. ' +
@@ -348,7 +351,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
348
351
  async enforceOidc(header, callers) {
349
352
  const token = this.credential(header, BEARER_SCHEME);
350
353
  if (!token) {
351
- throw new core_util_1.UnauthorizedError('Missing OIDC bearer token for @AuthOidc endpoint');
354
+ throw new core_util_1.UnauthorizedError('Missing OIDC bearer token for @WpAuthOidc endpoint');
352
355
  }
353
356
  // App-bound OidcHook overrides the caller policy; otherwise the framework default runs directly.
354
357
  if (this.oidcHook) {
@@ -362,7 +365,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
362
365
  enforceSharedSecret(provided, secretKey) {
363
366
  const accepted = this.authConfig?.sharedSecrets[secretKey];
364
367
  if (!accepted || !provided || !this.matchesEither(provided, accepted)) {
365
- throw new core_util_1.UnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');
368
+ throw new core_util_1.UnauthorizedError('Invalid shared secret for @WpAuthSharedSecret endpoint');
366
369
  }
367
370
  }
368
371
  /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */
@@ -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 { provideFrameworkSingleton, HttpRequest, PendingWireTrust, PendingTrustedValue, RawHttpRequest, RequestContext } from '@webpieces/core-context';\nimport { AuthMode, EndpointNotFoundError, BadRequestError, UnauthorizedError, JwtRequirement, LogManager, RuntimeLocality, toError } from '@webpieces/core-util';\nimport { Filter, Service } from '@webpieces/core-util';\nimport { WpResponse } from '../WpResponse';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AUTH_CONFIG, AuthenticatedCaller, AUTHENTICATED_CALLER_KEY, SharedSecrets } from '../AuthConfig';\nimport { ApiKeyHook, API_KEY_HOOK, JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK, WebhookAuthCallback, WEBHOOK_AUTH_CALLBACK } from '../AuthHooks';\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 <@AuthSharedSecret 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 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 @AuthSharedSecret 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 @AuthJwt 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,\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 @AuthWebhook 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() @inject(WEBHOOK_AUTH_CALLBACK) private readonly webhookAuthCallback?: WebhookAuthCallback,\n // @optional: only bind an ApiKeyHook to enable @AuthApiKey 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 = 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.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);\n break;\n case 'oidc':\n await this.enforceOidc(authHeader, mode.callers);\n break;\n case 'shared-secret':\n this.enforceSharedSecret(this.credential(authHeader, SHARED_SECRET_SCHEME), mode.secretKey);\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.reconcileWireTrust(AuthFilter.verifiesCaller(mode));\n this.rethrowDeferredBodyError();\n return nextFilter.invoke(meta);\n }\n\n /**\n * `@AuthWebhook(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 @AuthWebhook('${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 UnauthorizedError('Webhook auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n if (!this.hasRawBytes(request)) {\n log.warn(\n `Refusing @AuthWebhook('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +\n `no raw bytes. Declare @Endpoint(path, '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 UnauthorizedError('Webhook signature cannot be verified: no raw request was retained');\n }\n // Throws UnauthorizedError 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.applyAuthenticatedCaller(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 * `@AuthApiKey(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 @AuthApiKey('${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 UnauthorizedError('API-key auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n if (!request) {\n log.warn(\n `Refusing @AuthApiKey('${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 UnauthorizedError('API key cannot be verified: no inbound request was published');\n }\n // Throws UnauthorizedError 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.applyAuthenticatedCaller(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 BadRequestError('Request body is not valid JSON', undefined, undefined, parseError);\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 * Decide what happens to the trusted keys that arrived on the WIRE and were held back by\n * {@link PendingWireTrust} (read that class for why they are held rather than written).\n *\n * `callerVerified` — the endpoint authenticated the SENDER **as a peer service** (`@AuthOidc`,\n * `@AuthSharedSecret`).\n * The sender is a service we trust, this is the service-to-service hop, and its forwarded\n * identity is admitted as-is. This is the case that makes propagating a verified userId across\n * internal services work.\n *\n * Otherwise the sender is a browser or anyone else with curl, and the ONLY acceptable inbound\n * trusted value is one the authenticator independently derived to the same value. Everything\n * else is rejected — see {@link requireVouched}.\n *\n * Runs AFTER the mode enforcement above, because that is what stamps the authenticator's own\n * values (`applyAuthenticatedCaller`); comparing before it ran would compare against nothing.\n */\n private reconcileWireTrust(callerVerified: boolean): void {\n const pending = PendingWireTrust.takeAll();\n for (const item of pending) {\n if (callerVerified) {\n RequestContext.putTrusted(item.key, item.value);\n } else {\n this.requireVouched(item);\n }\n }\n }\n\n /**\n * On a browser-reachable route, an inbound trusted header must match what the authenticator\n * itself derived, or the request dies. Both failure shapes are rejections, not repairs:\n *\n * - DIFFERENT value — the caller said `alice`, the credential says `bob`. Silently letting the\n * credential win is not safe, because upstream rate limiters commonly bucket on the header\n * rather than the token: the request was already counted against the wrong principal, so\n * every forged header would be a free rate-limit bypass. No honest caller contradicts its own\n * credential.\n * - NOTHING vouched for it — nobody derived this key at all, so there is no evidence behind a\n * value a stranger typed. This is the common case, not the exotic one: the framework's\n * {@link DefaultJwtHook} stamps NO entries, and an app hook (jwt or api-key) only stamps the keys it can prove,\n * so any other trusted key a caller sends lands here.\n *\n * The pending value is discarded either way — the throw is what leaves the request.\n */\n private requireVouched(item: PendingTrustedValue): void {\n const vouched = RequestContext.getTrusted(item.key);\n if (vouched === item.value) {\n return;\n }\n log.error(\n `Rejecting inbound '${item.key.httpHeader}': it is a TRUSTED context key, this route does ` +\n `not authenticate its caller, and the credential ` +\n (vouched === undefined ? 'vouched for no such value' : 'derived a different value') + '.',\n );\n throw new UnauthorizedError(\n `Header '${item.key.httpHeader}' cannot be supplied by the caller on this endpoint`,\n );\n }\n\n /**\n * `@AuthLocalOnly`: 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 @AuthLocalOnly 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 EndpointNotFoundError(`No endpoint at ${meta.routeMeta.path}`);\n }\n\n private async enforceJwt(header: string | undefined, requirement: JwtRequirement): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new UnauthorizedError('Authentication required');\n }\n if (!this.jwtHook) {\n throw new UnauthorizedError('User-JWT auth is not enabled on this server');\n }\n const caller = await this.jwtHook.parseJwt(token); // AUTHENTICATE — throws UnauthorizedError if invalid\n this.applyAuthenticatedCaller(caller);\n await this.jwtHook.authorizeJwt(caller, requirement); // AUTHORIZE — app policy; throws ForbiddenError 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 UnauthorizedError('Missing OIDC bearer token for @AuthOidc 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 UnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');\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.applyAuthenticatedCaller(await this.jwtHook.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n log.debug('Best-effort JWT parse on a public endpoint failed (treating as anonymous): ', error);\n }\n }\n\n /**\n * Stamp the authenticated caller's context entries + the caller itself into the RequestContext.\n * ONE path for all three authenticating hooks — jwt, api-key and webhook — so a vendor hook that\n * proved which account a payload belongs to seeds context exactly as a JwtHook does.\n */\n private applyAuthenticatedCaller(caller: AuthenticatedCaller): void {\n for (const entry of caller.entries) {\n // ContextTuple.key is a TRUSTED key by type, so this is the one sanctioned write of a\n // proven identity: the app's hook derived it from a credential we just verified.\n RequestContext.putTrusted(entry.key, entry.value);\n }\n // A real TRUSTED ContextKey, not a raw string slot: the caller IS the framework's own proof,\n // so it is written with the same typed verb every other proven value goes through.\n RequestContext.putTrusted(AUTHENTICATED_CALLER_KEY, caller);\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 PendingWireTrust,\n PendingTrustedValue,\n RawHttpRequest,\n RequestContext,\n} from '@webpieces/core-context';\nimport {\n AuthMode,\n EndpointNotFoundError,\n BadRequestError,\n UnauthorizedError,\n JwtRequirement,\n LogManager,\n RuntimeLocality,\n toError,\n} from '@webpieces/core-util';\nimport { Filter, Service } from '@webpieces/core-util';\nimport { WpResponse } from '../WpResponse';\nimport { MethodMeta } from '../MethodMeta';\nimport {\n AuthConfig,\n AUTH_CONFIG,\n AuthenticatedCaller,\n AUTHENTICATED_CALLER_KEY,\n SharedSecrets,\n} 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 { 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 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,\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 = 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.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);\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.reconcileWireTrust(AuthFilter.verifiesCaller(mode));\n this.rethrowDeferredBodyError();\n return nextFilter.invoke(meta);\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 UnauthorizedError('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(path, '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 UnauthorizedError(\n 'Webhook signature cannot be verified: no raw request was retained',\n );\n }\n // Throws UnauthorizedError 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.applyAuthenticatedCaller(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 UnauthorizedError('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 UnauthorizedError(\n 'API key cannot be verified: no inbound request was published',\n );\n }\n // Throws UnauthorizedError 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.applyAuthenticatedCaller(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 BadRequestError(\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 * Decide what happens to the trusted keys that arrived on the WIRE and were held back by\n * {@link PendingWireTrust} (read that class for why they are held rather than written).\n *\n * `callerVerified` — the endpoint authenticated the SENDER **as a peer service** (`@WpAuthOidc`,\n * `@WpAuthSharedSecret`).\n * The sender is a service we trust, this is the service-to-service hop, and its forwarded\n * identity is admitted as-is. This is the case that makes propagating a verified userId across\n * internal services work.\n *\n * Otherwise the sender is a browser or anyone else with curl, and the ONLY acceptable inbound\n * trusted value is one the authenticator independently derived to the same value. Everything\n * else is rejected — see {@link requireVouched}.\n *\n * Runs AFTER the mode enforcement above, because that is what stamps the authenticator's own\n * values (`applyAuthenticatedCaller`); comparing before it ran would compare against nothing.\n */\n private reconcileWireTrust(callerVerified: boolean): void {\n const pending = PendingWireTrust.takeAll();\n for (const item of pending) {\n if (callerVerified) {\n RequestContext.putTrusted(item.key, item.value);\n } else {\n this.requireVouched(item);\n }\n }\n }\n\n /**\n * On a browser-reachable route, an inbound trusted header must match what the authenticator\n * itself derived, or the request dies. Both failure shapes are rejections, not repairs:\n *\n * - DIFFERENT value — the caller said `alice`, the credential says `bob`. Silently letting the\n * credential win is not safe, because upstream rate limiters commonly bucket on the header\n * rather than the token: the request was already counted against the wrong principal, so\n * every forged header would be a free rate-limit bypass. No honest caller contradicts its own\n * credential.\n * - NOTHING vouched for it — nobody derived this key at all, so there is no evidence behind a\n * value a stranger typed. This is the common case, not the exotic one: the framework's\n * {@link DefaultJwtHook} stamps NO entries, and an app hook (jwt or api-key) only stamps the keys it can prove,\n * so any other trusted key a caller sends lands here.\n *\n * The pending value is discarded either way — the throw is what leaves the request.\n */\n private requireVouched(item: PendingTrustedValue): void {\n const vouched = RequestContext.getTrusted(item.key);\n if (vouched === item.value) {\n return;\n }\n log.error(\n `Rejecting inbound '${item.key.httpHeader}': it is a TRUSTED context key, this route does ` +\n `not authenticate its caller, and the credential ` +\n (vouched === undefined\n ? 'vouched for no such value'\n : 'derived a different value') +\n '.',\n );\n throw new UnauthorizedError(\n `Header '${item.key.httpHeader}' cannot be supplied by the caller on this endpoint`,\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 EndpointNotFoundError(`No endpoint at ${meta.routeMeta.path}`);\n }\n\n private async enforceJwt(\n header: string | undefined,\n requirement: JwtRequirement,\n ): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new UnauthorizedError('Authentication required');\n }\n if (!this.jwtHook) {\n throw new UnauthorizedError('User-JWT auth is not enabled on this server');\n }\n const caller = await this.jwtHook.parseJwt(token); // AUTHENTICATE — throws UnauthorizedError if invalid\n this.applyAuthenticatedCaller(caller);\n await this.jwtHook.authorizeJwt(caller, requirement); // AUTHORIZE — app policy; throws ForbiddenError 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 UnauthorizedError('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 UnauthorizedError('Invalid shared secret for @WpAuthSharedSecret endpoint');\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.applyAuthenticatedCaller(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 * Stamp the authenticated caller's context entries + the caller itself into the RequestContext.\n * ONE path for all three authenticating hooks — jwt, api-key and webhook — so a vendor hook that\n * proved which account a payload belongs to seeds context exactly as a JwtHook does.\n */\n private applyAuthenticatedCaller(caller: AuthenticatedCaller): void {\n for (const entry of caller.entries) {\n // ContextTuple.key is a TRUSTED key by type, so this is the one sanctioned write of a\n // proven identity: the app's hook derived it from a credential we just verified.\n RequestContext.putTrusted(entry.key, entry.value);\n }\n // A real TRUSTED ContextKey, not a raw string slot: the caller IS the framework's own proof,\n // so it is written with the same typed verb every other proven value goes through.\n RequestContext.putTrusted(AUTHENTICATED_CALLER_KEY, caller);\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"]}
package/src/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- export { ApiPath, Endpoint, Public, AuthJwt, rolesRequired, AuthOidc, AuthSharedSecret, AuthWebhook, AuthLocalOnly, Rpc, PubSub, Queue, getApiPath, getEndpoints, getEndpointOptions, isFormPost, isRawBody, isApiPath, getAuthMeta, getAuthMode, assertEveryEndpointHasAuthMode, getApiKind, assertApiKind, assertPubSubConventions, getQueueName, AuthMeta, RouteMetadata, METADATA_KEYS, ValidateImplementation, DocumentDesign, isDocumentDesign, } from '@webpieces/core-util';
1
+ export { ApiPath, Endpoint, WpAuthPublic, WpAuthJwt, rolesRequired, WpAuthOidc, WpAuthSharedSecret, WpAuthWebhook, WpAuthLocalOnly, Rpc, PubSub, Queue, getApiPath, getEndpoints, getEndpointOptions, isFormPost, isRawBody, isApiPath, getAuthMeta, getAuthMode, assertEveryEndpointHasAuthMode, getApiKind, assertApiKind, assertPubSubConventions, getQueueName, AuthMeta, RouteMetadata, METADATA_KEYS, ValidateImplementation, DocumentDesign, isDocumentDesign, } from '@webpieces/core-util';
2
2
  export type { AuthMode, ApiKind, EndpointOptions } from '@webpieces/core-util';
3
- export { SourceFile, ROUTING_METADATA_KEYS, } from './decorators';
3
+ export { SourceFile, ROUTING_METADATA_KEYS } from './decorators';
4
4
  export { provideSingletonDefaultForApi } from '@webpieces/core-context';
5
5
  export { provideFrameworkSingleton, provideFrameworkSingletonDefaultForApi, buildFrameworkModule, } from '@webpieces/core-context';
6
6
  export { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';
7
- export { Routes, RouteBuilder, RouteDefinition, FilterDefinition, } from './WebAppMeta';
7
+ export { Routes, RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';
8
8
  export { HttpRequest, RawHttpRequest, RawRequest } from '@webpieces/core-context';
9
9
  export { WpResponse } from './WpResponse';
10
10
  export { MethodMeta } from './MethodMeta';
@@ -14,7 +14,7 @@ export { FilterMatcher, HttpFilter } from './FilterMatcher';
14
14
  export { AppModules, RouteModule } from './AppModules';
15
15
  export { ApiFactory } from './ApiFactory';
16
16
  export { ApiClient, ApiClientProxy } from './ApiClient';
17
- export { AuthConfig, AUTH_CONFIG, AuthenticatedCaller, AUTHENTICATED_CALLER_KEY, SharedSecrets } from './AuthConfig';
17
+ export { AuthConfig, AUTH_CONFIG, AuthenticatedCaller, AUTHENTICATED_CALLER_KEY, SharedSecrets, } from './AuthConfig';
18
18
  export { JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK, WebhookAuthCallback, WEBHOOK_AUTH_CALLBACK, ApiKeyHook, API_KEY_HOOK, } from './AuthHooks';
19
19
  export { DefaultOidcVerifier } from './DefaultOidcVerifier';
20
20
  export { DefaultJwtHook } from './DefaultJwtHook';
package/src/index.js CHANGED
@@ -1,18 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AuthenticatedCaller = exports.AUTH_CONFIG = exports.AuthConfig = exports.ApiClient = exports.FilterMatcher = exports.LogApiFilter = exports.RouteHandler = exports.MethodMeta = exports.WpResponse = exports.RawRequest = exports.HttpRequest = exports.FilterDefinition = exports.RouteDefinition = exports.ApiRoutingFactory = exports.buildFrameworkModule = exports.provideFrameworkSingletonDefaultForApi = exports.provideFrameworkSingleton = exports.provideSingletonDefaultForApi = exports.ROUTING_METADATA_KEYS = exports.SourceFile = exports.isDocumentDesign = exports.DocumentDesign = exports.METADATA_KEYS = exports.RouteMetadata = exports.AuthMeta = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.isRawBody = exports.isFormPost = exports.getEndpointOptions = exports.getEndpoints = exports.getApiPath = exports.Queue = exports.PubSub = exports.Rpc = exports.AuthLocalOnly = exports.AuthWebhook = exports.AuthSharedSecret = exports.AuthOidc = exports.rolesRequired = exports.AuthJwt = exports.Public = exports.Endpoint = exports.ApiPath = void 0;
3
+ exports.AuthenticatedCaller = exports.AUTH_CONFIG = exports.AuthConfig = exports.ApiClient = exports.FilterMatcher = exports.LogApiFilter = exports.RouteHandler = exports.MethodMeta = exports.WpResponse = exports.RawRequest = exports.HttpRequest = exports.FilterDefinition = exports.RouteDefinition = exports.ApiRoutingFactory = exports.buildFrameworkModule = exports.provideFrameworkSingletonDefaultForApi = exports.provideFrameworkSingleton = exports.provideSingletonDefaultForApi = exports.ROUTING_METADATA_KEYS = exports.SourceFile = exports.isDocumentDesign = exports.DocumentDesign = exports.METADATA_KEYS = exports.RouteMetadata = exports.AuthMeta = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.isRawBody = exports.isFormPost = exports.getEndpointOptions = exports.getEndpoints = exports.getApiPath = exports.Queue = exports.PubSub = exports.Rpc = exports.WpAuthLocalOnly = exports.WpAuthWebhook = exports.WpAuthSharedSecret = exports.WpAuthOidc = exports.rolesRequired = exports.WpAuthJwt = exports.WpAuthPublic = exports.Endpoint = exports.ApiPath = void 0;
4
4
  exports.WEBPIECES_CONFIG_TOKEN = exports.WebpiecesConfig = exports.RuntimeSetupOptions = exports.setupRuntime = exports.WebpiecesRouterFactory = exports.WebpiecesRouter = exports.DefaultJwtHook = exports.DefaultOidcVerifier = exports.API_KEY_HOOK = exports.ApiKeyHook = exports.WEBHOOK_AUTH_CALLBACK = exports.WebhookAuthCallback = exports.OIDC_HOOK = exports.OidcHook = exports.JWT_HOOK = exports.JwtHook = exports.SharedSecrets = exports.AUTHENTICATED_CALLER_KEY = void 0;
5
5
  // Re-export API decorators from core-util for convenience
6
6
  var core_util_1 = require("@webpieces/core-util");
7
7
  Object.defineProperty(exports, "ApiPath", { enumerable: true, get: function () { return core_util_1.ApiPath; } });
8
8
  Object.defineProperty(exports, "Endpoint", { enumerable: true, get: function () { return core_util_1.Endpoint; } });
9
- Object.defineProperty(exports, "Public", { enumerable: true, get: function () { return core_util_1.Public; } });
10
- Object.defineProperty(exports, "AuthJwt", { enumerable: true, get: function () { return core_util_1.AuthJwt; } });
9
+ Object.defineProperty(exports, "WpAuthPublic", { enumerable: true, get: function () { return core_util_1.WpAuthPublic; } });
10
+ Object.defineProperty(exports, "WpAuthJwt", { enumerable: true, get: function () { return core_util_1.WpAuthJwt; } });
11
11
  Object.defineProperty(exports, "rolesRequired", { enumerable: true, get: function () { return core_util_1.rolesRequired; } });
12
- Object.defineProperty(exports, "AuthOidc", { enumerable: true, get: function () { return core_util_1.AuthOidc; } });
13
- Object.defineProperty(exports, "AuthSharedSecret", { enumerable: true, get: function () { return core_util_1.AuthSharedSecret; } });
14
- Object.defineProperty(exports, "AuthWebhook", { enumerable: true, get: function () { return core_util_1.AuthWebhook; } });
15
- Object.defineProperty(exports, "AuthLocalOnly", { enumerable: true, get: function () { return core_util_1.AuthLocalOnly; } });
12
+ Object.defineProperty(exports, "WpAuthOidc", { enumerable: true, get: function () { return core_util_1.WpAuthOidc; } });
13
+ Object.defineProperty(exports, "WpAuthSharedSecret", { enumerable: true, get: function () { return core_util_1.WpAuthSharedSecret; } });
14
+ Object.defineProperty(exports, "WpAuthWebhook", { enumerable: true, get: function () { return core_util_1.WpAuthWebhook; } });
15
+ Object.defineProperty(exports, "WpAuthLocalOnly", { enumerable: true, get: function () { return core_util_1.WpAuthLocalOnly; } });
16
16
  Object.defineProperty(exports, "Rpc", { enumerable: true, get: function () { return core_util_1.Rpc; } });
17
17
  Object.defineProperty(exports, "PubSub", { enumerable: true, get: function () { return core_util_1.PubSub; } });
18
18
  Object.defineProperty(exports, "Queue", { enumerable: true, get: function () { return core_util_1.Queue; } });
@@ -80,7 +80,7 @@ Object.defineProperty(exports, "FilterMatcher", { enumerable: true, get: functio
80
80
  var ApiClient_1 = require("./ApiClient");
81
81
  Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return ApiClient_1.ApiClient; } });
82
82
  // Auth: the app-provided, container-bound pieces the framework AuthFilter injects.
83
- // - AuthConfig: shared-secret STATE (@AuthSharedSecret values).
83
+ // - AuthConfig: shared-secret STATE (@WpAuthSharedSecret values).
84
84
  // - JwtHook / OidcHook / WebhookAuthCallback / ApiKeyHook: OPTIONAL verification mechanisms
85
85
  // (bind only what you use; unbound means the matching endpoints 401, never open).
86
86
  // - DefaultOidcVerifier: the built-in Google OIDC verifier used when no OidcHook is bound.
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/index.ts"],"names":[],"mappings":"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","sourcesContent":["// Re-export API decorators from core-util for convenience\nexport {\n ApiPath,\n Endpoint,\n Public,\n AuthJwt,\n rolesRequired,\n AuthOidc,\n AuthSharedSecret,\n AuthWebhook,\n AuthLocalOnly,\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n isFormPost,\n isRawBody,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n ValidateImplementation,\n // @DocumentDesign moved to core-util (design-root marker, browser + Node);\n // re-exported here for back-compat.\n DocumentDesign,\n isDocumentDesign,\n} from '@webpieces/core-util';\nexport type { AuthMode, ApiKind, EndpointOptions } from '@webpieces/core-util';\n\n// Server-side routing decorators and utilities\nexport {\n SourceFile,\n ROUTING_METADATA_KEYS,\n} from './decorators';\n\n// DI provider decorators moved to core-context; re-exported here for back-compat\nexport { provideSingletonDefaultForApi } from '@webpieces/core-context';\n// Framework-only DI registry (packages/** framework classes use these; see frameworkProvide.ts)\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonDefaultForApi,\n buildFrameworkModule,\n} from '@webpieces/core-context';\n\nexport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\n\n// Core routing types\nexport {\n Routes,\n RouteBuilder,\n RouteDefinition,\n FilterDefinition,\n} from './WebAppMeta';\n\n// The transport-neutral request type (defined in core-context; this is http-routing's\n// public request — a transport adapter builds one and the chain reads it from RequestContext).\nexport { HttpRequest, RawHttpRequest, RawRequest } from '@webpieces/core-context';\n\n// The INBOUND chain's response type. `Filter`, `Service` and `FilterChain` are NOT re-exported\n// here: they moved to @webpieces/core-util so the outbound client chain is the SAME abstraction\n// rather than a second spelling of it. Import them from '@webpieces/core-util'.\nexport { WpResponse } from './WpResponse';\nexport { MethodMeta } from './MethodMeta';\nexport { RouteHandler } from './RouteHandler';\n\n// LogApiFilter: the fixed OUTERMOST framework filter (auto-installed at 1,000,000 above\n// AuthFilter). Exported for reference/testing only — apps must NOT install it themselves.\nexport { LogApiFilter } from './filters/LogApiFilter';\n\n// RouteBuilderImpl (the route table + chain composer) is now INTERNAL — it is never\n// handed to upper layers. The express layer consumes ApiFactory.apiClients() instead.\n\n// Filter matching\nexport { FilterMatcher, HttpFilter } from './FilterMatcher';\n\n// The app's server-surface declaration: DI binding modules + route groups + headers.\nexport { AppModules, RouteModule } from './AppModules';\n\n// The public API-surface abstraction: declare routes/filters, get them back as ApiClient[].\nexport { ApiFactory } from './ApiFactory';\nexport { ApiClient, ApiClientProxy } from './ApiClient';\n\n// Auth: the app-provided, container-bound pieces the framework AuthFilter injects.\n// - AuthConfig: shared-secret STATE (@AuthSharedSecret values).\n// - JwtHook / OidcHook / WebhookAuthCallback / ApiKeyHook: OPTIONAL verification mechanisms\n// (bind only what you use; unbound means the matching endpoints 401, never open).\n// - DefaultOidcVerifier: the built-in Google OIDC verifier used when no OidcHook is bound.\nexport { AuthConfig, AUTH_CONFIG, AuthenticatedCaller, AUTHENTICATED_CALLER_KEY, SharedSecrets } from './AuthConfig';\nexport {\n JwtHook, JWT_HOOK,\n OidcHook, OIDC_HOOK,\n WebhookAuthCallback, WEBHOOK_AUTH_CALLBACK,\n ApiKeyHook, API_KEY_HOOK,\n} from './AuthHooks';\nexport { DefaultOidcVerifier } from './DefaultOidcVerifier';\n// DefaultJwtHook: batteries-included HS256 JwtHook — `new DefaultJwtHook(secret)` and go.\nexport { DefaultJwtHook } from './DefaultJwtHook';\n\n// Above-boundary context setup shared by every transport adapter.\n\n// Node-only router (the express-free heart: container + filter chain + in-process client)\nexport { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';\n\n// The ONE transport-free startup sequence (headers → logging → router → routes) → ApiFactory.\n// Reusable by any company/app and any framework adapter; a company wraps it with its own headers.\nexport { setupRuntime, RuntimeSetupOptions } from './setupRuntime';\n\n// Server configuration\nexport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/index.ts"],"names":[],"mappings":";;;;AAAA,0DAA0D;AAC1D,kDAkC8B;AAjC1B,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,yGAAA,YAAY,OAAA;AACZ,sGAAA,SAAS,OAAA;AACT,0GAAA,aAAa,OAAA;AACb,uGAAA,UAAU,OAAA;AACV,+GAAA,kBAAkB,OAAA;AAClB,0GAAA,aAAa,OAAA;AACb,4GAAA,eAAe,OAAA;AACf,gGAAA,GAAG,OAAA;AACH,mGAAA,MAAM,OAAA;AACN,kGAAA,KAAK,OAAA;AACL,uGAAA,UAAU,OAAA;AACV,yGAAA,YAAY,OAAA;AACZ,+GAAA,kBAAkB,OAAA;AAClB,uGAAA,UAAU,OAAA;AACV,sGAAA,SAAS,OAAA;AACT,sGAAA,SAAS,OAAA;AACT,wGAAA,WAAW,OAAA;AACX,wGAAA,WAAW,OAAA;AACX,2HAAA,8BAA8B,OAAA;AAC9B,uGAAA,UAAU,OAAA;AACV,0GAAA,aAAa,OAAA;AACb,oHAAA,uBAAuB,OAAA;AACvB,yGAAA,YAAY,OAAA;AACZ,qGAAA,QAAQ,OAAA;AACR,0GAAA,aAAa,OAAA;AACb,0GAAA,aAAa,OAAA;AAEb,2EAA2E;AAC3E,oCAAoC;AACpC,2GAAA,cAAc,OAAA;AACd,6GAAA,gBAAgB,OAAA;AAIpB,+CAA+C;AAC/C,2CAAiE;AAAxD,wGAAA,UAAU,OAAA;AAAE,mHAAA,qBAAqB,OAAA;AAE1C,iFAAiF;AACjF,wDAAwE;AAA/D,6HAAA,6BAA6B,OAAA;AACtC,gGAAgG;AAChG,wDAIiC;AAH7B,yHAAA,yBAAyB,OAAA;AACzB,sIAAA,sCAAsC,OAAA;AACtC,oHAAA,oBAAoB,OAAA;AAGxB,yDAAmE;AAA1D,sHAAA,iBAAiB,OAAA;AAE1B,qBAAqB;AACrB,2CAAuF;AAAxD,6GAAA,eAAe,OAAA;AAAE,8GAAA,gBAAgB,OAAA;AAEhE,sFAAsF;AACtF,+FAA+F;AAC/F,wDAAkF;AAAzE,2GAAA,WAAW,OAAA;AAAkB,0GAAA,UAAU,OAAA;AAEhD,+FAA+F;AAC/F,gGAAgG;AAChG,gFAAgF;AAChF,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,wFAAwF;AACxF,0FAA0F;AAC1F,uDAAsD;AAA7C,4GAAA,YAAY,OAAA;AAErB,oFAAoF;AACpF,sFAAsF;AAEtF,kBAAkB;AAClB,iDAA4D;AAAnD,8GAAA,aAAa,OAAA;AAOtB,yCAAwD;AAA/C,sGAAA,SAAS,OAAA;AAElB,mFAAmF;AACnF,mEAAmE;AACnE,6FAA6F;AAC7F,qFAAqF;AACrF,4FAA4F;AAC5F,2CAMsB;AALlB,wGAAA,UAAU,OAAA;AACV,yGAAA,WAAW,OAAA;AACX,iHAAA,mBAAmB,OAAA;AACnB,sHAAA,wBAAwB,OAAA;AACxB,2GAAA,aAAa,OAAA;AAEjB,yCASqB;AARjB,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,qGAAA,QAAQ,OAAA;AACR,sGAAA,SAAS,OAAA;AACT,gHAAA,mBAAmB,OAAA;AACnB,kHAAA,qBAAqB,OAAA;AACrB,uGAAA,UAAU,OAAA;AACV,yGAAA,YAAY,OAAA;AAEhB,6DAA4D;AAAnD,0HAAA,mBAAmB,OAAA;AAC5B,0FAA0F;AAC1F,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAEvB,kEAAkE;AAElE,0FAA0F;AAC1F,qDAAoG;AAA3F,kHAAA,eAAe,OAAA;AAAE,yHAAA,sBAAsB,OAAA;AAEhD,8FAA8F;AAC9F,kGAAkG;AAClG,+CAAmE;AAA1D,4GAAA,YAAY,OAAA;AAAE,mHAAA,mBAAmB,OAAA;AAE1C,uBAAuB;AACvB,qDAA4E;AAAnE,kHAAA,eAAe,OAAA;AAAE,yHAAA,sBAAsB,OAAA","sourcesContent":["// Re-export API decorators from core-util for convenience\nexport {\n ApiPath,\n Endpoint,\n WpAuthPublic,\n WpAuthJwt,\n rolesRequired,\n WpAuthOidc,\n WpAuthSharedSecret,\n WpAuthWebhook,\n WpAuthLocalOnly,\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n isFormPost,\n isRawBody,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n ValidateImplementation,\n // @DocumentDesign moved to core-util (design-root marker, browser + Node);\n // re-exported here for back-compat.\n DocumentDesign,\n isDocumentDesign,\n} from '@webpieces/core-util';\nexport type { AuthMode, ApiKind, EndpointOptions } from '@webpieces/core-util';\n\n// Server-side routing decorators and utilities\nexport { SourceFile, ROUTING_METADATA_KEYS } from './decorators';\n\n// DI provider decorators moved to core-context; re-exported here for back-compat\nexport { provideSingletonDefaultForApi } from '@webpieces/core-context';\n// Framework-only DI registry (packages/** framework classes use these; see frameworkProvide.ts)\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonDefaultForApi,\n buildFrameworkModule,\n} from '@webpieces/core-context';\n\nexport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\n\n// Core routing types\nexport { Routes, RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';\n\n// The transport-neutral request type (defined in core-context; this is http-routing's\n// public request — a transport adapter builds one and the chain reads it from RequestContext).\nexport { HttpRequest, RawHttpRequest, RawRequest } from '@webpieces/core-context';\n\n// The INBOUND chain's response type. `Filter`, `Service` and `FilterChain` are NOT re-exported\n// here: they moved to @webpieces/core-util so the outbound client chain is the SAME abstraction\n// rather than a second spelling of it. Import them from '@webpieces/core-util'.\nexport { WpResponse } from './WpResponse';\nexport { MethodMeta } from './MethodMeta';\nexport { RouteHandler } from './RouteHandler';\n\n// LogApiFilter: the fixed OUTERMOST framework filter (auto-installed at 1,000,000 above\n// AuthFilter). Exported for reference/testing only — apps must NOT install it themselves.\nexport { LogApiFilter } from './filters/LogApiFilter';\n\n// RouteBuilderImpl (the route table + chain composer) is now INTERNAL — it is never\n// handed to upper layers. The express layer consumes ApiFactory.apiClients() instead.\n\n// Filter matching\nexport { FilterMatcher, HttpFilter } from './FilterMatcher';\n\n// The app's server-surface declaration: DI binding modules + route groups + headers.\nexport { AppModules, RouteModule } from './AppModules';\n\n// The public API-surface abstraction: declare routes/filters, get them back as ApiClient[].\nexport { ApiFactory } from './ApiFactory';\nexport { ApiClient, ApiClientProxy } from './ApiClient';\n\n// Auth: the app-provided, container-bound pieces the framework AuthFilter injects.\n// - AuthConfig: shared-secret STATE (@WpAuthSharedSecret values).\n// - JwtHook / OidcHook / WebhookAuthCallback / ApiKeyHook: OPTIONAL verification mechanisms\n// (bind only what you use; unbound means the matching endpoints 401, never open).\n// - DefaultOidcVerifier: the built-in Google OIDC verifier used when no OidcHook is bound.\nexport {\n AuthConfig,\n AUTH_CONFIG,\n AuthenticatedCaller,\n AUTHENTICATED_CALLER_KEY,\n SharedSecrets,\n} from './AuthConfig';\nexport {\n JwtHook,\n JWT_HOOK,\n OidcHook,\n OIDC_HOOK,\n WebhookAuthCallback,\n WEBHOOK_AUTH_CALLBACK,\n ApiKeyHook,\n API_KEY_HOOK,\n} from './AuthHooks';\nexport { DefaultOidcVerifier } from './DefaultOidcVerifier';\n// DefaultJwtHook: batteries-included HS256 JwtHook — `new DefaultJwtHook(secret)` and go.\nexport { DefaultJwtHook } from './DefaultJwtHook';\n\n// Above-boundary context setup shared by every transport adapter.\n\n// Node-only router (the express-free heart: container + filter chain + in-process client)\nexport { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';\n\n// The ONE transport-free startup sequence (headers → logging → router → routes) → ApiFactory.\n// Reusable by any company/app and any framework adapter; a company wraps it with its own headers.\nexport { setupRuntime, RuntimeSetupOptions } from './setupRuntime';\n\n// Server configuration\nexport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\n"]}
@@ -23,7 +23,7 @@ export declare class RuntimeSetupOptions {
23
23
  readonly svcVersion: string;
24
24
  /**
25
25
  * WHERE this process runs — `'local'` (a developer's machine) or `'deployed'` (everything
26
- * else). Published to {@link RuntimeLocality}; the ONE input to `@AuthLocalOnly` enforcement.
26
+ * else). Published to {@link RuntimeLocality}; the ONE input to `@WpAuthLocalOnly` enforcement.
27
27
  *
28
28
  * REQUIRED and POSITIONAL on purpose, exactly like `@Endpoint(path, kind)`: only the app
29
29
  * knows how its platform is detected (Cloud Run's `K_SERVICE`, an ECS metadata URL, the
@@ -48,7 +48,7 @@ export declare class RuntimeSetupOptions {
48
48
  svcVersion: string,
49
49
  /**
50
50
  * WHERE this process runs — `'local'` (a developer's machine) or `'deployed'` (everything
51
- * else). Published to {@link RuntimeLocality}; the ONE input to `@AuthLocalOnly` enforcement.
51
+ * else). Published to {@link RuntimeLocality}; the ONE input to `@WpAuthLocalOnly` enforcement.
52
52
  *
53
53
  * REQUIRED and POSITIONAL on purpose, exactly like `@Endpoint(path, kind)`: only the app
54
54
  * knows how its platform is detected (Cloud Run's `K_SERVICE`, an ECS metadata URL, the
@@ -32,7 +32,7 @@ class RuntimeSetupOptions {
32
32
  svcVersion,
33
33
  /**
34
34
  * WHERE this process runs — `'local'` (a developer's machine) or `'deployed'` (everything
35
- * else). Published to {@link RuntimeLocality}; the ONE input to `@AuthLocalOnly` enforcement.
35
+ * else). Published to {@link RuntimeLocality}; the ONE input to `@WpAuthLocalOnly` enforcement.
36
36
  *
37
37
  * REQUIRED and POSITIONAL on purpose, exactly like `@Endpoint(path, kind)`: only the app
38
38
  * knows how its platform is detected (Cloud Run's `K_SERVICE`, an ECS metadata URL, the
@@ -82,7 +82,7 @@ appOverrides) {
82
82
  // one startup every server runs.
83
83
  core_util_1.ServiceInfo.setInfo(options.svcName, options.svcVersion);
84
84
  // 0b. Declare WHERE we run, before any route is built: ApiRoutingFactory reads it in step 4 to
85
- // decide whether @AuthLocalOnly routes are registered at all. Undeclared reads as DEPLOYED, so
85
+ // decide whether @WpAuthLocalOnly routes are registered at all. Undeclared reads as DEPLOYED, so
86
86
  // this call is what lets a local-only endpoint exist — never what hides one.
87
87
  core_util_1.RuntimeLocality.declare(options.locality);
88
88
  // 1. Register the global HeaderRegistry FIRST (this service's own keys come from AppModules).
@@ -1 +1 @@
1
- {"version":3,"file":"setupRuntime.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/setupRuntime.ts"],"names":[],"mappings":";;;AA4DA,oCAsCC;AAjGD,oDAAyH;AACzH,uDAAoD;AACpD,uDAA2D;AAI3D;;;;;;;;;GASG;AACH,MAAa,mBAAmB;IAIR;IAIA;IAWA;IAEA;IAEA;IAEA;IAxBpB;IACI;iFAC6E;IAC7D,OAAe;IAC/B;;+DAE2D;IAC3C,UAAkB;IAClC;;;;;;;;;OASG;IACa,QAAkB;IAClC,0DAA0D;IAC1C,aAA4B;IAC5C,sDAAsD;IACtC,kBAA2B,IAAI;IAC/C,gFAAgF;IAChE,MAAwB;QArBxB,YAAO,GAAP,OAAO,CAAQ;QAIf,eAAU,GAAV,UAAU,CAAQ;QAWlB,aAAQ,GAAR,QAAQ,CAAU;QAElB,kBAAa,GAAb,aAAa,CAAe;QAE5B,oBAAe,GAAf,eAAe,CAAgB;QAE/B,WAAM,GAAN,MAAM,CAAkB;IACzC,CAAC;CACP;AA3BD,kDA2BC;AAED;;;;;;;;;;;;;GAaG;AACI,KAAK,UAAU,YAAY,CAC9B,OAA4B,EAC5B,UAAsB;AACtB;mEACmE;AACnE,YAA8B;IAE9B,gGAAgG;IAChG,6FAA6F;IAC7F,kGAAkG;IAClG,kGAAkG;IAClG,kGAAkG;IAClG,iCAAiC;IACjC,uBAAW,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAEzD,+FAA+F;IAC/F,+FAA+F;IAC/F,6EAA6E;IAC7E,2BAAe,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE1C,8FAA8F;IAC9F,0BAAc,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,EAAE,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAE3E,kEAAkE;IAClE,sBAAU,CAAC,UAAU,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAE7C,gDAAgD;IAChD,MAAM,MAAM,GAAG,MAAM,wCAAsB,CAAC,MAAM,CAAC;QAC/C,WAAW,EAAE,CAAC,GAAG,UAAU,CAAC,iBAAiB,EAAE,CAAC;QAChD,YAAY,EAAE,YAAY;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI,iCAAe,EAAE;KAClD,CAAC,CAAC;IAEH,6FAA6F;IAC7F,KAAK,MAAM,WAAW,IAAI,UAAU,CAAC,iBAAiB,EAAE,EAAE,CAAC;QACvD,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["import { ContainerModule } from 'inversify';\nimport { HeaderRegistry, Locality, LoggerFactory, LogManager, RuntimeLocality, ServiceInfo } from '@webpieces/core-util';\nimport { WebpiecesConfig } from './WebpiecesConfig';\nimport { WebpiecesRouterFactory } from './WebpiecesRouter';\nimport { AppModules } from './AppModules';\nimport { ApiFactory } from './ApiFactory';\n\n/**\n * RuntimeSetupOptions - the environment/wiring inputs to {@link setupRuntime} (everything NOT\n * declared by the app's {@link AppModules}): the logging backend, whether to include the platform\n * default headers, and config. Data-only structure (a class, per the webpieces guidelines). The\n * app's own binding modules + route groups + headers come from the AppModules passed alongside;\n * the test-override module is the separate `appOverrides` param of {@link setupRuntime}.\n *\n * Headers: {@link HeaderRegistry.configure} registers the platform defaults (when\n * `platformHeaders` is true) plus AppModules.getHeaders() (by convention the company-wide set).\n */\nexport class RuntimeSetupOptions {\n constructor(\n /** This service's name — published to ServiceInfo. Names every log line and stamps\n * `requestIdSource` on request-ids this service mints. Must be non-blank. */\n public readonly svcName: string,\n /** This build's version — published to ServiceInfo alongside svcName. Opaque to webpieces\n * (a git SHA, a semver tag, a CI build number); it just has to identify THIS build so a log\n * line can say which one emitted it. Must be non-blank. */\n public readonly svcVersion: string,\n /**\n * WHERE this process runs — `'local'` (a developer's machine) or `'deployed'` (everything\n * else). Published to {@link RuntimeLocality}; the ONE input to `@AuthLocalOnly` enforcement.\n *\n * REQUIRED and POSITIONAL on purpose, exactly like `@Endpoint(path, kind)`: only the app\n * knows how its platform is detected (Cloud Run's `K_SERVICE`, an ECS metadata URL, the\n * absence of both), the framework must not guess, and a defaulted field would mean \"forgot\n * to say\" and \"said deployed\" are the same line of code. Derive it at your startup, e.g.\n * `getServiceName() === 'local' ? 'local' : 'deployed'`.\n */\n public readonly locality: Locality,\n /** Logging backend to install (LogManager.setFactory). */\n public readonly loggerFactory: LoggerFactory,\n /** Include the webpieces platform default headers. */\n public readonly platformHeaders: boolean = true,\n /** Optional WebpiecesConfig (e.g. recording flags); defaults to a fresh one. */\n public readonly config?: WebpiecesConfig,\n ) {}\n}\n\n/**\n * setupRuntime - the ONE canonical, TRANSPORT-FREE startup sequence, reusable by any company/app\n * AND any framework adapter (express, fastify, a serverless handler, ...). It runs, in the correct\n * fail-fast order:\n *\n * 1. HeaderRegistry.configure (filters read it at construction; logging masks off it)\n * 2. LogManager.setFactory (fails fast unless the registry is configured first)\n * 3. build the router + DI container (from appModules.getBindingModules())\n * 4. configure each appModules.getRoutingModules() onto the router (addRoutes/addFilter)\n *\n * and returns the built {@link ApiFactory} — `apiClients()` for a transport to bind, or\n * `createApiClient()` for in-process tests. There is NO express (or any transport) here; a\n * transport adapter (e.g. WebpiecesExpressRouter in @webpieces/http-server) serves the result.\n */\nexport async function setupRuntime(\n options: RuntimeSetupOptions,\n appModules: AppModules,\n /** A single DI module loaded LAST so tests can rebind bindings to mocks.\n * Or special case servers that want to override specific things */\n appOverrides?: ContainerModule,\n): Promise<ApiFactory> {\n // 0. IDENTIFY this service (name + build version) FIRST, before anything logs. Name and version\n // are REQUIRED inputs to this call, so a build cannot boot anonymously — setInfo throws on a\n // blank value, which kills the deploy (the revision never goes healthy) rather than shipping logs\n // that cannot say which build emitted them. Reads of ServiceInfo elsewhere never throw (a missing\n // log field must never 500 live traffic); the \"say which build you are\" guarantee lives HERE, the\n // one startup every server runs.\n ServiceInfo.setInfo(options.svcName, options.svcVersion);\n\n // 0b. Declare WHERE we run, before any route is built: ApiRoutingFactory reads it in step 4 to\n // decide whether @AuthLocalOnly routes are registered at all. Undeclared reads as DEPLOYED, so\n // this call is what lets a local-only endpoint exist — never what hides one.\n RuntimeLocality.declare(options.locality);\n\n // 1. Register the global HeaderRegistry FIRST (this service's own keys come from AppModules).\n HeaderRegistry.configure(appModules.getHeaders(), options.platformHeaders);\n\n // 2. Install the logging backend ONCE, before anything else logs.\n LogManager.setFactory(options.loggerFactory);\n\n // 3. Build the node-only router + DI container.\n const router = await WebpiecesRouterFactory.create({\n appBindings: [...appModules.getBindingModules()],\n appOverrides: appOverrides,\n config: options.config ?? new WebpiecesConfig(),\n });\n\n // 4. Let each route group declare its routes + filters, then hand back the consumer surface.\n for (const routeModule of appModules.getRoutingModules()) {\n routeModule.configure(router);\n }\n return router;\n}\n"]}
1
+ {"version":3,"file":"setupRuntime.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/setupRuntime.ts"],"names":[],"mappings":";;;AAmEA,oCAsCC;AAxGD,oDAO8B;AAC9B,uDAAoD;AACpD,uDAA2D;AAI3D;;;;;;;;;GASG;AACH,MAAa,mBAAmB;IAIR;IAIA;IAWA;IAEA;IAEA;IAEA;IAxBpB;IACI;iFAC6E;IAC7D,OAAe;IAC/B;;+DAE2D;IAC3C,UAAkB;IAClC;;;;;;;;;OASG;IACa,QAAkB;IAClC,0DAA0D;IAC1C,aAA4B;IAC5C,sDAAsD;IACtC,kBAA2B,IAAI;IAC/C,gFAAgF;IAChE,MAAwB;QArBxB,YAAO,GAAP,OAAO,CAAQ;QAIf,eAAU,GAAV,UAAU,CAAQ;QAWlB,aAAQ,GAAR,QAAQ,CAAU;QAElB,kBAAa,GAAb,aAAa,CAAe;QAE5B,oBAAe,GAAf,eAAe,CAAgB;QAE/B,WAAM,GAAN,MAAM,CAAkB;IACzC,CAAC;CACP;AA3BD,kDA2BC;AAED;;;;;;;;;;;;;GAaG;AACI,KAAK,UAAU,YAAY,CAC9B,OAA4B,EAC5B,UAAsB;AACtB;mEACmE;AACnE,YAA8B;IAE9B,gGAAgG;IAChG,6FAA6F;IAC7F,kGAAkG;IAClG,kGAAkG;IAClG,kGAAkG;IAClG,iCAAiC;IACjC,uBAAW,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAEzD,+FAA+F;IAC/F,iGAAiG;IACjG,6EAA6E;IAC7E,2BAAe,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE1C,8FAA8F;IAC9F,0BAAc,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,EAAE,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAE3E,kEAAkE;IAClE,sBAAU,CAAC,UAAU,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAE7C,gDAAgD;IAChD,MAAM,MAAM,GAAG,MAAM,wCAAsB,CAAC,MAAM,CAAC;QAC/C,WAAW,EAAE,CAAC,GAAG,UAAU,CAAC,iBAAiB,EAAE,CAAC;QAChD,YAAY,EAAE,YAAY;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,IAAI,iCAAe,EAAE;KAClD,CAAC,CAAC;IAEH,6FAA6F;IAC7F,KAAK,MAAM,WAAW,IAAI,UAAU,CAAC,iBAAiB,EAAE,EAAE,CAAC;QACvD,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["import { ContainerModule } from 'inversify';\nimport {\n HeaderRegistry,\n Locality,\n LoggerFactory,\n LogManager,\n RuntimeLocality,\n ServiceInfo,\n} from '@webpieces/core-util';\nimport { WebpiecesConfig } from './WebpiecesConfig';\nimport { WebpiecesRouterFactory } from './WebpiecesRouter';\nimport { AppModules } from './AppModules';\nimport { ApiFactory } from './ApiFactory';\n\n/**\n * RuntimeSetupOptions - the environment/wiring inputs to {@link setupRuntime} (everything NOT\n * declared by the app's {@link AppModules}): the logging backend, whether to include the platform\n * default headers, and config. Data-only structure (a class, per the webpieces guidelines). The\n * app's own binding modules + route groups + headers come from the AppModules passed alongside;\n * the test-override module is the separate `appOverrides` param of {@link setupRuntime}.\n *\n * Headers: {@link HeaderRegistry.configure} registers the platform defaults (when\n * `platformHeaders` is true) plus AppModules.getHeaders() (by convention the company-wide set).\n */\nexport class RuntimeSetupOptions {\n constructor(\n /** This service's name — published to ServiceInfo. Names every log line and stamps\n * `requestIdSource` on request-ids this service mints. Must be non-blank. */\n public readonly svcName: string,\n /** This build's version — published to ServiceInfo alongside svcName. Opaque to webpieces\n * (a git SHA, a semver tag, a CI build number); it just has to identify THIS build so a log\n * line can say which one emitted it. Must be non-blank. */\n public readonly svcVersion: string,\n /**\n * WHERE this process runs — `'local'` (a developer's machine) or `'deployed'` (everything\n * else). Published to {@link RuntimeLocality}; the ONE input to `@WpAuthLocalOnly` enforcement.\n *\n * REQUIRED and POSITIONAL on purpose, exactly like `@Endpoint(path, kind)`: only the app\n * knows how its platform is detected (Cloud Run's `K_SERVICE`, an ECS metadata URL, the\n * absence of both), the framework must not guess, and a defaulted field would mean \"forgot\n * to say\" and \"said deployed\" are the same line of code. Derive it at your startup, e.g.\n * `getServiceName() === 'local' ? 'local' : 'deployed'`.\n */\n public readonly locality: Locality,\n /** Logging backend to install (LogManager.setFactory). */\n public readonly loggerFactory: LoggerFactory,\n /** Include the webpieces platform default headers. */\n public readonly platformHeaders: boolean = true,\n /** Optional WebpiecesConfig (e.g. recording flags); defaults to a fresh one. */\n public readonly config?: WebpiecesConfig,\n ) {}\n}\n\n/**\n * setupRuntime - the ONE canonical, TRANSPORT-FREE startup sequence, reusable by any company/app\n * AND any framework adapter (express, fastify, a serverless handler, ...). It runs, in the correct\n * fail-fast order:\n *\n * 1. HeaderRegistry.configure (filters read it at construction; logging masks off it)\n * 2. LogManager.setFactory (fails fast unless the registry is configured first)\n * 3. build the router + DI container (from appModules.getBindingModules())\n * 4. configure each appModules.getRoutingModules() onto the router (addRoutes/addFilter)\n *\n * and returns the built {@link ApiFactory} — `apiClients()` for a transport to bind, or\n * `createApiClient()` for in-process tests. There is NO express (or any transport) here; a\n * transport adapter (e.g. WebpiecesExpressRouter in @webpieces/http-server) serves the result.\n */\nexport async function setupRuntime(\n options: RuntimeSetupOptions,\n appModules: AppModules,\n /** A single DI module loaded LAST so tests can rebind bindings to mocks.\n * Or special case servers that want to override specific things */\n appOverrides?: ContainerModule,\n): Promise<ApiFactory> {\n // 0. IDENTIFY this service (name + build version) FIRST, before anything logs. Name and version\n // are REQUIRED inputs to this call, so a build cannot boot anonymously — setInfo throws on a\n // blank value, which kills the deploy (the revision never goes healthy) rather than shipping logs\n // that cannot say which build emitted them. Reads of ServiceInfo elsewhere never throw (a missing\n // log field must never 500 live traffic); the \"say which build you are\" guarantee lives HERE, the\n // one startup every server runs.\n ServiceInfo.setInfo(options.svcName, options.svcVersion);\n\n // 0b. Declare WHERE we run, before any route is built: ApiRoutingFactory reads it in step 4 to\n // decide whether @WpAuthLocalOnly routes are registered at all. Undeclared reads as DEPLOYED, so\n // this call is what lets a local-only endpoint exist — never what hides one.\n RuntimeLocality.declare(options.locality);\n\n // 1. Register the global HeaderRegistry FIRST (this service's own keys come from AppModules).\n HeaderRegistry.configure(appModules.getHeaders(), options.platformHeaders);\n\n // 2. Install the logging backend ONCE, before anything else logs.\n LogManager.setFactory(options.loggerFactory);\n\n // 3. Build the node-only router + DI container.\n const router = await WebpiecesRouterFactory.create({\n appBindings: [...appModules.getBindingModules()],\n appOverrides: appOverrides,\n config: options.config ?? new WebpiecesConfig(),\n });\n\n // 4. Let each route group declare its routes + filters, then hand back the consumer surface.\n for (const routeModule of appModules.getRoutingModules()) {\n routeModule.configure(router);\n }\n return router;\n}\n"]}