@webpieces/http-routing 0.4.640 → 0.4.642
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 +4 -4
- package/src/ApiRoutingFactory.js +5 -1
- package/src/ApiRoutingFactory.js.map +1 -1
- package/src/AuthHooks.d.ts +41 -0
- package/src/AuthHooks.js +33 -1
- package/src/AuthHooks.js.map +1 -1
- package/src/filters/AuthFilter.d.ts +32 -2
- package/src/filters/AuthFilter.js +67 -2
- package/src/filters/AuthFilter.js.map +1 -1
- package/src/index.d.ts +3 -3
- package/src/index.js +7 -2
- package/src/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-routing",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.642",
|
|
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.
|
|
26
|
-
"@webpieces/core-util": "0.4.
|
|
27
|
-
"@webpieces/gcp-identity": "0.4.
|
|
25
|
+
"@webpieces/core-context": "0.4.642",
|
|
26
|
+
"@webpieces/core-util": "0.4.642",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.642",
|
|
28
28
|
"inversify": "7.10.4",
|
|
29
29
|
"jsonwebtoken": "9.0.2",
|
|
30
30
|
"minimatch": "10.0.1"
|
package/src/ApiRoutingFactory.js
CHANGED
|
@@ -57,6 +57,10 @@ class ApiRoutingFactory {
|
|
|
57
57
|
* Validates controller methods and auth decorators in single loop.
|
|
58
58
|
*/
|
|
59
59
|
configure(routeBuilder) {
|
|
60
|
+
// A webhook route whose transport keeps no bytes has nothing to verify. That is a wiring
|
|
61
|
+
// mistake, so it dies at STARTUP naming the fix — not as a 401 in production on exactly the
|
|
62
|
+
// traffic the endpoint exists for.
|
|
63
|
+
(0, core_util_1.assertEveryWebhookEndpointRetainsRawBody)(this.apiMetaClass);
|
|
60
64
|
const basePath = (0, core_util_1.getApiPath)(this.apiMetaClass);
|
|
61
65
|
const endpoints = (0, core_util_1.getEndpoints)(this.apiMetaClass) || {};
|
|
62
66
|
const controllerFilepath = this.getControllerFilepath();
|
|
@@ -84,7 +88,7 @@ class ApiRoutingFactory {
|
|
|
84
88
|
continue;
|
|
85
89
|
}
|
|
86
90
|
const fullPath = basePath + endpointPath;
|
|
87
|
-
const routeMeta = new core_util_1.RouteMetadata('POST', fullPath, methodName, controllerName, authMeta, apiName, (0, core_util_1.isFormPost)(this.apiMetaClass, methodName), (0, core_util_1.getMaskSpec)(this.apiMetaClass, methodName));
|
|
91
|
+
const routeMeta = new core_util_1.RouteMetadata('POST', fullPath, methodName, controllerName, authMeta, apiName, (0, core_util_1.isFormPost)(this.apiMetaClass, methodName), (0, core_util_1.getMaskSpec)(this.apiMetaClass, methodName), (0, core_util_1.isRawBody)(this.apiMetaClass, methodName));
|
|
88
92
|
routeBuilder.addRoute(new WebAppMeta_1.RouteDefinition(routeMeta, this.controllerClass, controllerFilepath, this.apiMetaClass));
|
|
89
93
|
}
|
|
90
94
|
}
|
|
@@ -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, 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 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 );\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 { 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"]}
|
package/src/AuthHooks.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { JwtRequirement } from '@webpieces/core-util';
|
|
2
|
+
import { HttpRequest, RawRequest } from '@webpieces/core-context';
|
|
2
3
|
import { AuthValues } from './AuthConfig';
|
|
3
4
|
/**
|
|
4
5
|
* JwtHook - the OPTIONAL user-JWT mechanism. Its DI token is the {@link JWT_HOOK} Symbol injected via
|
|
@@ -49,3 +50,43 @@ export declare abstract class OidcHook {
|
|
|
49
50
|
* correct — undefined when unbound. The OidcHook class stays the TYPE and the impl base.
|
|
50
51
|
*/
|
|
51
52
|
export declare const OIDC_HOOK: unique symbol;
|
|
53
|
+
/**
|
|
54
|
+
* WebhookHook - the OPTIONAL mechanism behind `@AuthWebhook(name)`: prove that an inbound request was
|
|
55
|
+
* really authored by the outside vendor the contract names. Its DI token is the {@link WEBHOOK_HOOK}
|
|
56
|
+
* Symbol injected via `@inject(WEBHOOK_HOOK)` (a Symbol, because the app container uses autobind;
|
|
57
|
+
* rebindable in tests). The third hook, symmetric with {@link JwtHook} / {@link OidcHook}:
|
|
58
|
+
*
|
|
59
|
+
* ```typescript
|
|
60
|
+
* // AppModule.ts, beside the CompanyJwtHook binding
|
|
61
|
+
* options.bind(WEBHOOK_HOOK).to(CompanyWebhookHook);
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* When NO WebhookHook is bound, the framework {@link AuthFilter} 401s every `@AuthWebhook` endpoint,
|
|
65
|
+
* exactly as it does for an unbound JwtHook. There is no framework default and there never will be
|
|
66
|
+
* one: silently allowing an unverified webhook is the single default that must not exist, and the
|
|
67
|
+
* framework ships no vendor crypto by design (see {@link AuthWebhook} for why reimplementing five
|
|
68
|
+
* vendors' schemes is a losing trade).
|
|
69
|
+
*
|
|
70
|
+
* ONE hook serves EVERY vendor: `name` selects which, so an app with a Sentry hook and a Twilio hook
|
|
71
|
+
* switches on it rather than binding a token per vendor. What arrives is enough of the raw request to
|
|
72
|
+
* call the vendor's OWN validator — the bytes for a body-signing vendor (Sentry, GitHub, Stripe,
|
|
73
|
+
* Slack), the absolute url for one that signs the url instead (Twilio).
|
|
74
|
+
*/
|
|
75
|
+
export declare abstract class WebhookHook {
|
|
76
|
+
/**
|
|
77
|
+
* Verify ONE inbound request. Return to allow; throw {@link HttpUnauthorizedError} to deny.
|
|
78
|
+
*
|
|
79
|
+
* @param name the string on the contract's `@AuthWebhook(name)` — which vendor this route is.
|
|
80
|
+
* @param request the transport-neutral request (method, path, headers).
|
|
81
|
+
* @param raw the verbatim bytes + absolute url, guaranteed present: `@AuthWebhook` requires
|
|
82
|
+
* `@Endpoint(..., { rawBody: true })` at wiring time, and AuthFilter 401s rather
|
|
83
|
+
* than calling this hook with nothing to check.
|
|
84
|
+
*/
|
|
85
|
+
abstract verify(name: string, request: HttpRequest, raw: RawRequest): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* DI identifier for the optional {@link WebhookHook} binding. It is a Symbol (not the class) so the app
|
|
89
|
+
* container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(WEBHOOK_HOOK)`
|
|
90
|
+
* correct — undefined when unbound. The WebhookHook class stays the TYPE and the impl base.
|
|
91
|
+
*/
|
|
92
|
+
export declare const WEBHOOK_HOOK: unique symbol;
|
package/src/AuthHooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OIDC_HOOK = exports.OidcHook = exports.JWT_HOOK = exports.JwtHook = void 0;
|
|
3
|
+
exports.WEBHOOK_HOOK = exports.WebhookHook = exports.OIDC_HOOK = exports.OidcHook = exports.JWT_HOOK = exports.JwtHook = void 0;
|
|
4
4
|
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
|
|
@@ -59,4 +59,36 @@ exports.OidcHook = OidcHook;
|
|
|
59
59
|
*/
|
|
60
60
|
// 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)
|
|
61
61
|
exports.OIDC_HOOK = Symbol.for('OidcHook');
|
|
62
|
+
/**
|
|
63
|
+
* WebhookHook - the OPTIONAL mechanism behind `@AuthWebhook(name)`: prove that an inbound request was
|
|
64
|
+
* really authored by the outside vendor the contract names. Its DI token is the {@link WEBHOOK_HOOK}
|
|
65
|
+
* Symbol injected via `@inject(WEBHOOK_HOOK)` (a Symbol, because the app container uses autobind;
|
|
66
|
+
* rebindable in tests). The third hook, symmetric with {@link JwtHook} / {@link OidcHook}:
|
|
67
|
+
*
|
|
68
|
+
* ```typescript
|
|
69
|
+
* // AppModule.ts, beside the CompanyJwtHook binding
|
|
70
|
+
* options.bind(WEBHOOK_HOOK).to(CompanyWebhookHook);
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* When NO WebhookHook is bound, the framework {@link AuthFilter} 401s every `@AuthWebhook` endpoint,
|
|
74
|
+
* exactly as it does for an unbound JwtHook. There is no framework default and there never will be
|
|
75
|
+
* one: silently allowing an unverified webhook is the single default that must not exist, and the
|
|
76
|
+
* framework ships no vendor crypto by design (see {@link AuthWebhook} for why reimplementing five
|
|
77
|
+
* vendors' schemes is a losing trade).
|
|
78
|
+
*
|
|
79
|
+
* ONE hook serves EVERY vendor: `name` selects which, so an app with a Sentry hook and a Twilio hook
|
|
80
|
+
* switches on it rather than binding a token per vendor. What arrives is enough of the raw request to
|
|
81
|
+
* call the vendor's OWN validator — the bytes for a body-signing vendor (Sentry, GitHub, Stripe,
|
|
82
|
+
* Slack), the absolute url for one that signs the url instead (Twilio).
|
|
83
|
+
*/
|
|
84
|
+
class WebhookHook {
|
|
85
|
+
}
|
|
86
|
+
exports.WebhookHook = WebhookHook;
|
|
87
|
+
/**
|
|
88
|
+
* DI identifier for the optional {@link WebhookHook} binding. It is a Symbol (not the class) so the app
|
|
89
|
+
* container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(WEBHOOK_HOOK)`
|
|
90
|
+
* correct — undefined when unbound. The WebhookHook class stays the TYPE and the impl base.
|
|
91
|
+
*/
|
|
92
|
+
// 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)
|
|
93
|
+
exports.WEBHOOK_HOOK = Symbol.for('WebhookHook');
|
|
62
94
|
//# sourceMappingURL=AuthHooks.js.map
|
package/src/AuthHooks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthHooks.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooks.ts"],"names":[],"mappings":";;;AAAA,oDAAyF;
|
|
1
|
+
{"version":3,"file":"AuthHooks.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooks.ts"],"names":[],"mappings":";;;AAAA,oDAAyF;AAIzF;;;;;;;;;;;;;;GAcG;AACH,MAAsB,OAAO;IAIzB;;;OAGG;IACH,YAAY,CAAC,MAAkB,EAAE,WAA2B;QACxD,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,8BAAkB,CAAC,mCAAmC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxF,CAAC;IACL,CAAC;CACJ;AAhBD,0BAgBC;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,WAAW;CAWhC;AAXD,kCAWC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC","sourcesContent":["import { JwtRequirement, rolesRequired, HttpForbiddenError } from '@webpieces/core-util';\nimport { HttpRequest, RawRequest } from '@webpieces/core-context';\nimport { AuthValues } 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 AuthValues}, 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 */\nexport abstract class JwtHook {\n /** Parse a user JWT (kind:'jwt') — AUTHENTICATION only. Return who the user is, or throw. */\n abstract parseJwt(token: string): AuthValues;\n\n /**\n * DEFAULT authorization: enforce the endpoint's roles (any-of). Override to enforce app-defined\n * requirements. Throw HttpForbiddenError to deny; return to allow.\n */\n authorizeJwt(values: AuthValues, requirement: JwtRequirement): 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) => values.roles.includes(role))) {\n throw new HttpForbiddenError(`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 * WebhookHook - 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_HOOK}\n * Symbol injected via `@inject(WEBHOOK_HOOK)` (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_HOOK).to(CompanyWebhookHook);\n * ```\n *\n * When NO WebhookHook 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 — the bytes for a body-signing vendor (Sentry, GitHub, Stripe,\n * Slack), the absolute url for one that signs the url instead (Twilio).\n */\nexport abstract class WebhookHook {\n /**\n * Verify ONE inbound request. Return to allow; throw {@link HttpUnauthorizedError} to deny.\n *\n * @param name the string on the contract's `@AuthWebhook(name)` — which vendor this route is.\n * @param request the transport-neutral request (method, path, headers).\n * @param raw the verbatim bytes + absolute url, guaranteed present: `@AuthWebhook` requires\n * `@Endpoint(..., { rawBody: true })` at wiring time, and AuthFilter 401s rather\n * than calling this hook with nothing to check.\n */\n abstract verify(name: string, request: HttpRequest, raw: RawRequest): Promise<void>;\n}\n\n/**\n * DI identifier for the optional {@link WebhookHook} 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_HOOK)`\n * correct — undefined when unbound. The WebhookHook 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_HOOK = Symbol.for('WebhookHook');\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Filter, WpResponse, Service } from '../Filter';
|
|
2
2
|
import { MethodMeta } from '../MethodMeta';
|
|
3
3
|
import { AuthConfig } from '../AuthConfig';
|
|
4
|
-
import { JwtHook, OidcHook } from '../AuthHooks';
|
|
4
|
+
import { JwtHook, OidcHook, WebhookHook } from '../AuthHooks';
|
|
5
5
|
import { DefaultOidcVerifier } from '../DefaultOidcVerifier';
|
|
6
6
|
/**
|
|
7
7
|
* AuthFilter - the ONE framework auth filter, auto-installed just below the error filter on every
|
|
@@ -16,6 +16,9 @@ import { DefaultOidcVerifier } from '../DefaultOidcVerifier';
|
|
|
16
16
|
* "not enabled" (401): JWT needs an app secret + payload shape.
|
|
17
17
|
* - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}
|
|
18
18
|
* run DIRECTLY — so a server that wires NOTHING still verifies Google OIDC.
|
|
19
|
+
* - webhook → the bound {@link WebhookHook} verifies the VENDOR's signature over the retained
|
|
20
|
+
* raw request. No WebhookHook bound → 401, like jwt: an unverified webhook is
|
|
21
|
+
* never waved through because wiring was forgotten.
|
|
19
22
|
* - public → BEST-EFFORT jwt parse (only if a JwtHook is bound): stamp the user's context so
|
|
20
23
|
* a logged-out page still knows who is logged in; never fails.
|
|
21
24
|
* - local-only → serve only when {@link RuntimeLocality} says this process is a developer's
|
|
@@ -29,8 +32,35 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
|
|
|
29
32
|
private readonly authConfig?;
|
|
30
33
|
private readonly jwtHook?;
|
|
31
34
|
private readonly oidcHook?;
|
|
32
|
-
|
|
35
|
+
private readonly webhookHook?;
|
|
36
|
+
constructor(oidcVerifier: DefaultOidcVerifier, authConfig?: AuthConfig | undefined, jwtHook?: JwtHook | undefined, oidcHook?: OidcHook | undefined, webhookHook?: WebhookHook | undefined);
|
|
33
37
|
filter(meta: MethodMeta, nextFilter: Service<MethodMeta, WpResponse<unknown>>): Promise<WpResponse<unknown>>;
|
|
38
|
+
/**
|
|
39
|
+
* `@AuthWebhook(name)`: hand the app's {@link WebhookHook} the verbatim request and let it call the
|
|
40
|
+
* VENDOR's own validator. Three ways to fail, all 401, all before the controller is entered:
|
|
41
|
+
*
|
|
42
|
+
* 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior;
|
|
43
|
+
* an unverified webhook must never be waved through because wiring was forgotten.
|
|
44
|
+
* 2. NO raw request — the transport kept no bytes. `assertEveryWebhookEndpointRetainsRawBody`
|
|
45
|
+
* normally makes this a startup error, so reaching it means either a hand-registered route or
|
|
46
|
+
* an in-process caller (a spec) that published an HttpRequest with no {@link RawRequest}. The
|
|
47
|
+
* message says which fix applies rather than leaving a bare 401.
|
|
48
|
+
* 3. The hook threw — the signature did not verify.
|
|
49
|
+
*/
|
|
50
|
+
private enforceWebhook;
|
|
51
|
+
/**
|
|
52
|
+
* A body that failed to parse is held on the {@link RawRequest} and surfaces HERE, after auth, as
|
|
53
|
+
* the 400 it always was — never before it.
|
|
54
|
+
*
|
|
55
|
+
* The order is the whole point. A malformed body from an unauthenticated caller must answer 401,
|
|
56
|
+
* because "your JSON was bad" also says "I got past auth", and on a webhook endpoint — whose url
|
|
57
|
+
* is public by construction — that is a free oracle for anyone probing. Parsing first made the
|
|
58
|
+
* framework hand that out for nothing.
|
|
59
|
+
*
|
|
60
|
+
* Only routes that retain raw bytes can defer at all; every other route still fails at parse time
|
|
61
|
+
* in the transport, exactly as before.
|
|
62
|
+
*/
|
|
63
|
+
private rethrowDeferredBodyError;
|
|
34
64
|
/**
|
|
35
65
|
* Does this mode authenticate the CALLER ITSELF (as opposed to a user, or nobody)? The INBOUND
|
|
36
66
|
* twin of {@link DestinationTrust.forAuthMode}, and deliberately the same question: the client
|
|
@@ -46,6 +46,9 @@ const PRINCIPAL_KEY = '__webpieces_principal__';
|
|
|
46
46
|
* "not enabled" (401): JWT needs an app secret + payload shape.
|
|
47
47
|
* - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}
|
|
48
48
|
* run DIRECTLY — so a server that wires NOTHING still verifies Google OIDC.
|
|
49
|
+
* - webhook → the bound {@link WebhookHook} verifies the VENDOR's signature over the retained
|
|
50
|
+
* raw request. No WebhookHook bound → 401, like jwt: an unverified webhook is
|
|
51
|
+
* never waved through because wiring was forgotten.
|
|
49
52
|
* - public → BEST-EFFORT jwt parse (only if a JwtHook is bound): stamp the user's context so
|
|
50
53
|
* a logged-out page still knows who is logged in; never fails.
|
|
51
54
|
* - local-only → serve only when {@link RuntimeLocality} says this process is a developer's
|
|
@@ -59,12 +62,14 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends Filter_1.Filter {
|
|
|
59
62
|
authConfig;
|
|
60
63
|
jwtHook;
|
|
61
64
|
oidcHook;
|
|
62
|
-
|
|
65
|
+
webhookHook;
|
|
66
|
+
constructor(oidcVerifier, authConfig, jwtHook, oidcHook, webhookHook) {
|
|
63
67
|
super();
|
|
64
68
|
this.oidcVerifier = oidcVerifier;
|
|
65
69
|
this.authConfig = authConfig;
|
|
66
70
|
this.jwtHook = jwtHook;
|
|
67
71
|
this.oidcHook = oidcHook;
|
|
72
|
+
this.webhookHook = webhookHook;
|
|
68
73
|
}
|
|
69
74
|
// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility
|
|
70
75
|
async filter(meta, nextFilter) {
|
|
@@ -74,6 +79,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends Filter_1.Filter {
|
|
|
74
79
|
// Public: best-effort parse so a logged-out page can still know the logged-in user.
|
|
75
80
|
this.bestEffortJwt(authHeader);
|
|
76
81
|
this.reconcileWireTrust(/*callerVerified*/ false);
|
|
82
|
+
this.rethrowDeferredBodyError();
|
|
77
83
|
return nextFilter.invoke(meta);
|
|
78
84
|
}
|
|
79
85
|
switch (mode.kind) {
|
|
@@ -86,13 +92,63 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends Filter_1.Filter {
|
|
|
86
92
|
case 'shared-secret':
|
|
87
93
|
this.enforceSharedSecret(this.credential(authHeader, SHARED_SECRET_SCHEME), mode.secretKey);
|
|
88
94
|
break;
|
|
95
|
+
case 'webhook':
|
|
96
|
+
await this.enforceWebhook(mode.name, meta);
|
|
97
|
+
break;
|
|
89
98
|
case 'local-only':
|
|
90
99
|
this.enforceLocalOnly(meta);
|
|
91
100
|
break;
|
|
92
101
|
}
|
|
93
102
|
this.reconcileWireTrust(AuthFilter_1.verifiesCaller(mode));
|
|
103
|
+
this.rethrowDeferredBodyError();
|
|
94
104
|
return nextFilter.invoke(meta);
|
|
95
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* `@AuthWebhook(name)`: hand the app's {@link WebhookHook} the verbatim request and let it call the
|
|
108
|
+
* VENDOR's own validator. Three ways to fail, all 401, all before the controller is entered:
|
|
109
|
+
*
|
|
110
|
+
* 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior;
|
|
111
|
+
* an unverified webhook must never be waved through because wiring was forgotten.
|
|
112
|
+
* 2. NO raw request — the transport kept no bytes. `assertEveryWebhookEndpointRetainsRawBody`
|
|
113
|
+
* normally makes this a startup error, so reaching it means either a hand-registered route or
|
|
114
|
+
* an in-process caller (a spec) that published an HttpRequest with no {@link RawRequest}. The
|
|
115
|
+
* message says which fix applies rather than leaving a bare 401.
|
|
116
|
+
* 3. The hook threw — the signature did not verify.
|
|
117
|
+
*/
|
|
118
|
+
async enforceWebhook(name, meta) {
|
|
119
|
+
if (!this.webhookHook) {
|
|
120
|
+
log.warn(`Refusing @AuthWebhook('${name}') endpoint ${meta.routeMeta.path}: no WebhookHook is bound. ` +
|
|
121
|
+
`Bind one (options.bind(WEBHOOK_HOOK).to(YourWebhookHook)) to enable webhook verification.`);
|
|
122
|
+
throw new core_util_1.HttpUnauthorizedError('Webhook auth is not enabled on this server');
|
|
123
|
+
}
|
|
124
|
+
const request = core_context_1.RequestContext.getRequest();
|
|
125
|
+
const raw = request?.raw;
|
|
126
|
+
if (!request || !raw) {
|
|
127
|
+
log.warn(`Refusing @AuthWebhook('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +
|
|
128
|
+
`no raw bytes. Declare @Endpoint(path, 'external', { calledBy: '${name}', rawBody: true }); a ` +
|
|
129
|
+
`spec driving this route in-process must publish an HttpRequest built with a RawRequest.`);
|
|
130
|
+
throw new core_util_1.HttpUnauthorizedError('Webhook signature cannot be verified: no raw request was retained');
|
|
131
|
+
}
|
|
132
|
+
await this.webhookHook.verify(name, request, raw); // throws HttpUnauthorizedError to deny
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A body that failed to parse is held on the {@link RawRequest} and surfaces HERE, after auth, as
|
|
136
|
+
* the 400 it always was — never before it.
|
|
137
|
+
*
|
|
138
|
+
* The order is the whole point. A malformed body from an unauthenticated caller must answer 401,
|
|
139
|
+
* because "your JSON was bad" also says "I got past auth", and on a webhook endpoint — whose url
|
|
140
|
+
* is public by construction — that is a free oracle for anyone probing. Parsing first made the
|
|
141
|
+
* framework hand that out for nothing.
|
|
142
|
+
*
|
|
143
|
+
* Only routes that retain raw bytes can defer at all; every other route still fails at parse time
|
|
144
|
+
* in the transport, exactly as before.
|
|
145
|
+
*/
|
|
146
|
+
rethrowDeferredBodyError() {
|
|
147
|
+
const parseError = core_context_1.RequestContext.getRequest()?.raw?.bodyParseError;
|
|
148
|
+
if (parseError) {
|
|
149
|
+
throw new core_util_1.HttpBadRequestError('Request body is not valid JSON', undefined, undefined, parseError);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
96
152
|
/**
|
|
97
153
|
* Does this mode authenticate the CALLER ITSELF (as opposed to a user, or nobody)? The INBOUND
|
|
98
154
|
* twin of {@link DestinationTrust.forAuthMode}, and deliberately the same question: the client
|
|
@@ -122,6 +178,12 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends Filter_1.Filter {
|
|
|
122
178
|
return true;
|
|
123
179
|
case 'jwt':
|
|
124
180
|
case 'public':
|
|
181
|
+
// `webhook` DOES authenticate its sender — but the sender is an outside VENDOR, not a
|
|
182
|
+
// peer in this repo, and a vendor neither speaks nor forwards webpieces context headers.
|
|
183
|
+
// So there is no forwarded identity to believe, and admitting one would mean trusting a
|
|
184
|
+
// key a vendor's payload could carry. Same answer as the OUTBOUND half
|
|
185
|
+
// (DestinationTrust.forAuthMode), which is the invariant that keeps the two ends agreeing.
|
|
186
|
+
case 'webhook':
|
|
125
187
|
case 'local-only':
|
|
126
188
|
return false;
|
|
127
189
|
}
|
|
@@ -302,9 +364,12 @@ exports.AuthFilter = AuthFilter = AuthFilter_1 = tslib_1.__decorate([
|
|
|
302
364
|
tslib_1.__param(2, (0, inversify_1.inject)(AuthHooks_1.JWT_HOOK)),
|
|
303
365
|
tslib_1.__param(3, (0, inversify_1.optional)()),
|
|
304
366
|
tslib_1.__param(3, (0, inversify_1.inject)(AuthHooks_1.OIDC_HOOK)),
|
|
367
|
+
tslib_1.__param(4, (0, inversify_1.optional)()),
|
|
368
|
+
tslib_1.__param(4, (0, inversify_1.inject)(AuthHooks_1.WEBHOOK_HOOK)),
|
|
305
369
|
tslib_1.__metadata("design:paramtypes", [DefaultOidcVerifier_1.DefaultOidcVerifier,
|
|
306
370
|
AuthConfig_1.AuthConfig,
|
|
307
371
|
AuthHooks_1.JwtHook,
|
|
308
|
-
AuthHooks_1.OidcHook
|
|
372
|
+
AuthHooks_1.OidcHook,
|
|
373
|
+
AuthHooks_1.WebhookHook])
|
|
309
374
|
], AuthFilter);
|
|
310
375
|
//# sourceMappingURL=AuthFilter.js.map
|
|
@@ -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, PendingWireTrust, PendingTrustedValue, RequestContext } from '@webpieces/core-context';\nimport { AuthMode, EndpointNotFoundError, HttpUnauthorizedError, JwtRequirement, LogManager, RuntimeLocality, toError } from '@webpieces/core-util';\nimport { Filter, WpResponse, Service } from '../Filter';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AUTH_CONFIG, AuthValues, SharedSecrets } from '../AuthConfig';\nimport { JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK } 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/** Reserved context key holding the authenticated {@link AuthValues} (stamped after a jwt parse). */\nconst PRINCIPAL_KEY = '__webpieces_principal__';\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`). No JwtHook bound →\n * \"not enabled\" (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 * - 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 ) {\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 this.bestEffortJwt(authHeader);\n this.reconcileWireTrust(/*callerVerified*/ false);\n return nextFilter.invoke(meta);\n }\n\n switch (mode.kind) {\n case 'jwt':\n 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 'local-only':\n this.enforceLocalOnly(meta);\n break;\n }\n this.reconcileWireTrust(AuthFilter.verifiesCaller(mode));\n return nextFilter.invoke(meta);\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 * An exhaustive switch with NO `default`, returning on every branch: a sixth 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 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 (`@AuthOidc`, `@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 (`applyAuthValues`); 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 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 HttpUnauthorizedError(\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 enforceJwt(header: string | undefined, requirement: JwtRequirement): void {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new HttpUnauthorizedError('Authentication required');\n }\n if (!this.jwtHook) {\n throw new HttpUnauthorizedError('User-JWT auth is not enabled on this server');\n }\n const values = this.jwtHook.parseJwt(token); // AUTHENTICATE — throws HttpUnauthorizedError if invalid\n this.applyAuthValues(values);\n this.jwtHook.authorizeJwt(values, requirement); // AUTHORIZE — app policy; throws HttpForbiddenError 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 HttpUnauthorizedError('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 HttpUnauthorizedError('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 bestEffortJwt(header: string | undefined): 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.applyAuthValues(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 /** Stamp the parsed user's context entries + the principal into the RequestContext. */\n private applyAuthValues(values: AuthValues): void {\n for (const entry of values.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 JwtHook derived it from a credential we just verified.\n RequestContext.putTrusted(entry.key, entry.value);\n }\n RequestContext.put(PRINCIPAL_KEY, values);\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 { provideFrameworkSingleton, PendingWireTrust, PendingTrustedValue, RequestContext } from '@webpieces/core-context';\nimport { AuthMode, EndpointNotFoundError, HttpBadRequestError, HttpUnauthorizedError, JwtRequirement, LogManager, RuntimeLocality, toError } from '@webpieces/core-util';\nimport { Filter, WpResponse, Service } from '../Filter';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AUTH_CONFIG, AuthValues, SharedSecrets } from '../AuthConfig';\nimport { JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK, WebhookHook, WEBHOOK_HOOK } 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/** Reserved context key holding the authenticated {@link AuthValues} (stamped after a jwt parse). */\nconst PRINCIPAL_KEY = '__webpieces_principal__';\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`). No JwtHook bound →\n * \"not enabled\" (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 WebhookHook} verifies the VENDOR's signature over the retained\n * raw request. No WebhookHook bound → 401, like jwt: an unverified webhook is\n * never waved through because wiring was forgotten.\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 WebhookHook 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_HOOK) private readonly webhookHook?: WebhookHook,\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 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 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 '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 WebhookHook} 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 private async enforceWebhook(name: string, meta: MethodMeta): Promise<void> {\n if (!this.webhookHook) {\n log.warn(\n `Refusing @AuthWebhook('${name}') endpoint ${meta.routeMeta.path}: no WebhookHook is bound. ` +\n `Bind one (options.bind(WEBHOOK_HOOK).to(YourWebhookHook)) to enable webhook verification.`,\n );\n throw new HttpUnauthorizedError('Webhook auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n const raw = request?.raw;\n if (!request || !raw) {\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 HttpUnauthorizedError('Webhook signature cannot be verified: no raw request was retained');\n }\n await this.webhookHook.verify(name, request, raw); // throws HttpUnauthorizedError to deny\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 HttpBadRequestError('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 * An exhaustive switch with NO `default`, returning on every branch: a sixth 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 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 (`@AuthOidc`, `@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 (`applyAuthValues`); 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 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 HttpUnauthorizedError(\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 enforceJwt(header: string | undefined, requirement: JwtRequirement): void {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new HttpUnauthorizedError('Authentication required');\n }\n if (!this.jwtHook) {\n throw new HttpUnauthorizedError('User-JWT auth is not enabled on this server');\n }\n const values = this.jwtHook.parseJwt(token); // AUTHENTICATE — throws HttpUnauthorizedError if invalid\n this.applyAuthValues(values);\n this.jwtHook.authorizeJwt(values, requirement); // AUTHORIZE — app policy; throws HttpForbiddenError 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 HttpUnauthorizedError('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 HttpUnauthorizedError('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 bestEffortJwt(header: string | undefined): 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.applyAuthValues(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 /** Stamp the parsed user's context entries + the principal into the RequestContext. */\n private applyAuthValues(values: AuthValues): void {\n for (const entry of values.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 JwtHook derived it from a credential we just verified.\n RequestContext.putTrusted(entry.key, entry.value);\n }\n RequestContext.put(PRINCIPAL_KEY, values);\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,11 +1,11 @@
|
|
|
1
|
-
export { ApiPath, Endpoint, Public, AuthJwt, rolesRequired, AuthOidc, AuthSharedSecret, AuthLocalOnly, Rpc, PubSub, Queue, getApiPath, getEndpoints, getEndpointOptions, isFormPost, isApiPath, getAuthMeta, getAuthMode, assertEveryEndpointHasAuthMode, getApiKind, assertApiKind, assertPubSubConventions, getQueueName, AuthMeta, RouteMetadata, METADATA_KEYS, ValidateImplementation, DocumentDesign, isDocumentDesign, } from '@webpieces/core-util';
|
|
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';
|
|
2
2
|
export type { AuthMode, ApiKind, EndpointOptions } from '@webpieces/core-util';
|
|
3
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
7
|
export { Routes, RouteBuilder, RouteDefinition, FilterDefinition, } from './WebAppMeta';
|
|
8
|
-
export { HttpRequest } from '@webpieces/core-context';
|
|
8
|
+
export { HttpRequest, RawRequest } from '@webpieces/core-context';
|
|
9
9
|
export { Filter, WpResponse, Service } from './Filter';
|
|
10
10
|
export { FilterChain } from './FilterChain';
|
|
11
11
|
export { MethodMeta } from './MethodMeta';
|
|
@@ -16,7 +16,7 @@ export { AppModules, RouteModule } from './AppModules';
|
|
|
16
16
|
export { ApiFactory } from './ApiFactory';
|
|
17
17
|
export { ApiClient, ApiClientProxy } from './ApiClient';
|
|
18
18
|
export { AuthConfig, AUTH_CONFIG, AuthValues, SharedSecrets } from './AuthConfig';
|
|
19
|
-
export { JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK } from './AuthHooks';
|
|
19
|
+
export { JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK, WebhookHook, WEBHOOK_HOOK } from './AuthHooks';
|
|
20
20
|
export { DefaultOidcVerifier } from './DefaultOidcVerifier';
|
|
21
21
|
export { DefaultJwtHook } from './DefaultJwtHook';
|
|
22
22
|
export { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.WEBPIECES_CONFIG_TOKEN = exports.WebpiecesConfig = exports.RuntimeSetupOptions = exports.setupRuntime = exports.WebpiecesRouterFactory = exports.WebpiecesRouter = exports.DefaultJwtHook = exports.DefaultOidcVerifier = exports.OIDC_HOOK = exports.OidcHook = exports.JWT_HOOK = exports.JwtHook = void 0;
|
|
3
|
+
exports.AuthConfig = exports.ApiClient = exports.FilterMatcher = exports.LogApiFilter = exports.RouteHandler = exports.MethodMeta = exports.FilterChain = exports.WpResponse = exports.Filter = 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;
|
|
4
|
+
exports.WEBPIECES_CONFIG_TOKEN = exports.WebpiecesConfig = exports.RuntimeSetupOptions = exports.setupRuntime = exports.WebpiecesRouterFactory = exports.WebpiecesRouter = exports.DefaultJwtHook = exports.DefaultOidcVerifier = exports.WEBHOOK_HOOK = exports.WebhookHook = exports.OIDC_HOOK = exports.OidcHook = exports.JWT_HOOK = exports.JwtHook = exports.SharedSecrets = exports.AuthValues = exports.AUTH_CONFIG = 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; } });
|
|
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "AuthJwt", { enumerable: true, get: function () {
|
|
|
11
11
|
Object.defineProperty(exports, "rolesRequired", { enumerable: true, get: function () { return core_util_1.rolesRequired; } });
|
|
12
12
|
Object.defineProperty(exports, "AuthOidc", { enumerable: true, get: function () { return core_util_1.AuthOidc; } });
|
|
13
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; } });
|
|
14
15
|
Object.defineProperty(exports, "AuthLocalOnly", { enumerable: true, get: function () { return core_util_1.AuthLocalOnly; } });
|
|
15
16
|
Object.defineProperty(exports, "Rpc", { enumerable: true, get: function () { return core_util_1.Rpc; } });
|
|
16
17
|
Object.defineProperty(exports, "PubSub", { enumerable: true, get: function () { return core_util_1.PubSub; } });
|
|
@@ -19,6 +20,7 @@ Object.defineProperty(exports, "getApiPath", { enumerable: true, get: function (
|
|
|
19
20
|
Object.defineProperty(exports, "getEndpoints", { enumerable: true, get: function () { return core_util_1.getEndpoints; } });
|
|
20
21
|
Object.defineProperty(exports, "getEndpointOptions", { enumerable: true, get: function () { return core_util_1.getEndpointOptions; } });
|
|
21
22
|
Object.defineProperty(exports, "isFormPost", { enumerable: true, get: function () { return core_util_1.isFormPost; } });
|
|
23
|
+
Object.defineProperty(exports, "isRawBody", { enumerable: true, get: function () { return core_util_1.isRawBody; } });
|
|
22
24
|
Object.defineProperty(exports, "isApiPath", { enumerable: true, get: function () { return core_util_1.isApiPath; } });
|
|
23
25
|
Object.defineProperty(exports, "getAuthMeta", { enumerable: true, get: function () { return core_util_1.getAuthMeta; } });
|
|
24
26
|
Object.defineProperty(exports, "getAuthMode", { enumerable: true, get: function () { return core_util_1.getAuthMode; } });
|
|
@@ -56,6 +58,7 @@ Object.defineProperty(exports, "FilterDefinition", { enumerable: true, get: func
|
|
|
56
58
|
// public request — a transport adapter builds one and the chain reads it from RequestContext).
|
|
57
59
|
var core_context_3 = require("@webpieces/core-context");
|
|
58
60
|
Object.defineProperty(exports, "HttpRequest", { enumerable: true, get: function () { return core_context_3.HttpRequest; } });
|
|
61
|
+
Object.defineProperty(exports, "RawRequest", { enumerable: true, get: function () { return core_context_3.RawRequest; } });
|
|
59
62
|
// Filter-chain primitives (absorbed from the former @webpieces/http-filters package)
|
|
60
63
|
var Filter_1 = require("./Filter");
|
|
61
64
|
Object.defineProperty(exports, "Filter", { enumerable: true, get: function () { return Filter_1.Filter; } });
|
|
@@ -91,6 +94,8 @@ Object.defineProperty(exports, "JwtHook", { enumerable: true, get: function () {
|
|
|
91
94
|
Object.defineProperty(exports, "JWT_HOOK", { enumerable: true, get: function () { return AuthHooks_1.JWT_HOOK; } });
|
|
92
95
|
Object.defineProperty(exports, "OidcHook", { enumerable: true, get: function () { return AuthHooks_1.OidcHook; } });
|
|
93
96
|
Object.defineProperty(exports, "OIDC_HOOK", { enumerable: true, get: function () { return AuthHooks_1.OIDC_HOOK; } });
|
|
97
|
+
Object.defineProperty(exports, "WebhookHook", { enumerable: true, get: function () { return AuthHooks_1.WebhookHook; } });
|
|
98
|
+
Object.defineProperty(exports, "WEBHOOK_HOOK", { enumerable: true, get: function () { return AuthHooks_1.WEBHOOK_HOOK; } });
|
|
94
99
|
var DefaultOidcVerifier_1 = require("./DefaultOidcVerifier");
|
|
95
100
|
Object.defineProperty(exports, "DefaultOidcVerifier", { enumerable: true, get: function () { return DefaultOidcVerifier_1.DefaultOidcVerifier; } });
|
|
96
101
|
// DefaultJwtHook: batteries-included HS256 JwtHook — `new DefaultJwtHook(secret)` and go.
|
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":";;;;AAAA,0DAA0D;AAC1D,
|
|
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,mGAAA,MAAM,OAAA;AACN,oGAAA,OAAO,OAAA;AACP,0GAAA,aAAa,OAAA;AACb,qGAAA,QAAQ,OAAA;AACR,6GAAA,gBAAgB,OAAA;AAChB,wGAAA,WAAW,OAAA;AACX,0GAAA,aAAa,OAAA;AACb,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,2CAGsB;AAFlB,wGAAA,UAAU,OAAA;AACV,mHAAA,qBAAqB,OAAA;AAGzB,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,2CAKsB;AAFlB,6GAAA,eAAe,OAAA;AACf,8GAAA,gBAAgB,OAAA;AAGpB,sFAAsF;AACtF,+FAA+F;AAC/F,wDAAkE;AAAzD,2GAAA,WAAW,OAAA;AAAE,0GAAA,UAAU,OAAA;AAEhC,qFAAqF;AACrF,mCAAuD;AAA9C,gGAAA,MAAM,OAAA;AAAE,oGAAA,UAAU,OAAA;AAC3B,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,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,iEAAiE;AACjE,oFAAoF;AACpF,4FAA4F;AAC5F,2CAAkF;AAAzE,wGAAA,UAAU,OAAA;AAAE,yGAAA,WAAW,OAAA;AAAE,wGAAA,UAAU,OAAA;AAAE,2GAAA,aAAa,OAAA;AAC3D,yCAAgG;AAAvF,oGAAA,OAAO,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAAE,sGAAA,SAAS,OAAA;AAAE,wGAAA,WAAW,OAAA;AAAE,yGAAA,YAAY,OAAA;AAC1E,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 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, RawRequest } from '@webpieces/core-context';\n\n// Filter-chain primitives (absorbed from the former @webpieces/http-filters package)\nexport { Filter, WpResponse, Service } from './Filter';\nexport { FilterChain } from './FilterChain';\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: OPTIONAL verification mechanisms (bind only what you use).\n// - DefaultOidcVerifier: the built-in Google OIDC verifier used when no OidcHook is bound.\nexport { AuthConfig, AUTH_CONFIG, AuthValues, SharedSecrets } from './AuthConfig';\nexport { JwtHook, JWT_HOOK, OidcHook, OIDC_HOOK, WebhookHook, WEBHOOK_HOOK } 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"]}
|