@webpieces/http-routing 0.4.957 → 0.4.958
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 +7 -7
- package/src/ApiRoutingFactory.js +8 -2
- package/src/ApiRoutingFactory.js.map +1 -1
- package/src/AuthConfig.d.ts +3 -34
- package/src/AuthConfig.js +8 -48
- package/src/AuthConfig.js.map +1 -1
- package/src/AuthHooks.d.ts +2 -54
- package/src/AuthHooks.js +2 -34
- package/src/AuthHooks.js.map +1 -1
- package/src/AuthHooksCompileAssertions.js +28 -10
- package/src/AuthHooksCompileAssertions.js.map +1 -1
- package/src/RouteBuilderImpl.js +6 -3
- package/src/RouteBuilderImpl.js.map +1 -1
- package/src/filters/AuthFilter.d.ts +13 -12
- package/src/filters/AuthFilter.js +27 -27
- package/src/filters/AuthFilter.js.map +1 -1
- package/src/index.d.ts +3 -2
- package/src/index.js +21 -8
- 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.958",
|
|
4
4
|
"description": "Decorator-based routing with auto-wiring for WebPieces",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,14 +22,14 @@
|
|
|
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.958",
|
|
26
|
+
"@webpieces/core-util": "0.4.958",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.958",
|
|
28
28
|
"inversify": "7.10.4",
|
|
29
29
|
"jsonwebtoken": "9.0.2",
|
|
30
30
|
"minimatch": "10.0.1",
|
|
31
|
-
"@webpieces/http-client-core": "0.4.
|
|
32
|
-
"@webpieces/http-client-node": "0.4.
|
|
33
|
-
"@webpieces/cloudtasks-client": "0.4.
|
|
31
|
+
"@webpieces/http-client-core": "0.4.958",
|
|
32
|
+
"@webpieces/http-client-node": "0.4.958",
|
|
33
|
+
"@webpieces/cloudtasks-client": "0.4.958"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/src/ApiRoutingFactory.js
CHANGED
|
@@ -58,10 +58,16 @@ class ApiRoutingFactory {
|
|
|
58
58
|
* Validates controller methods and auth decorators in single loop.
|
|
59
59
|
*/
|
|
60
60
|
configure(routeBuilder) {
|
|
61
|
-
//
|
|
61
|
+
// An EXTERNAL_OUTBOUND contract is an outside system's API: nothing in this repo serves it.
|
|
62
|
+
if ((0, core_util_1.isExternalOutboundApi)(this.apiMetaClass)) {
|
|
63
|
+
throw new Error(`${this.apiMetaClass.name || 'Unknown'} is an EXTERNAL_OUTBOUND contract — an outside system's API that ` +
|
|
64
|
+
`this repo only CALLS — so no server may addRoutes it. Bind a client to it instead ` +
|
|
65
|
+
`(createRpcClientAndBind), or change its endpoints to RPC / EXTERNAL_INBOUND if this service serves them.`);
|
|
66
|
+
}
|
|
67
|
+
// A vendorAuth route whose transport keeps no bytes has nothing to verify. That is a wiring
|
|
62
68
|
// mistake, so it dies at STARTUP naming the fix — not as a 401 in production on exactly the
|
|
63
69
|
// traffic the endpoint exists for.
|
|
64
|
-
(0, core_util_1.
|
|
70
|
+
(0, core_util_1.assertEveryVendorAuthEndpointRetainsRawBody)(this.apiMetaClass);
|
|
65
71
|
// Two endpoints on one method + path would silently shadow each other in the route map.
|
|
66
72
|
core_util_1.RouteMetadataFactory.assertNoDuplicateRoutes(this.apiMetaClass);
|
|
67
73
|
const endpoints = (0, core_util_1.getEndpoints)(this.apiMetaClass) || {};
|
|
@@ -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 {\n isApiPath,\n getEndpoints,\n getAuthMeta,\n assertEveryWebhookEndpointRetainsRawBody,\n assertNotInternalApi,\n LogManager,\n RouteMetadataFactory,\n AuthMeta,\n MISSING_AUTH_DECORATOR_FIX,\n RuntimeLocality,\n isLocalOnly,\n} from '@webpieces/core-util';\nimport 'reflect-metadata';\nimport { ROUTING_METADATA_KEYS } from './decorators';\n\nconst log = LogManager.getLogger('ApiRoutingFactory');\n\n/**\n * Type representing a class constructor (abstract or concrete).\n */\n// webpieces-disable no-any-unknown -- generic type alias requires unconstrained default\nexport type ClassType<T = unknown> = Function & { prototype: T };\n\n/**\n * ApiRoutingFactory - Automatically wire API interfaces to controllers.\n * Reads @ApiPath/@Endpoint decorators from an API prototype class and\n * registers POST routes for each endpoint.\n *\n * Replaces the old RESTApiRoutes class.\n *\n * Usage:\n * ```typescript\n * // In your ServerMeta:\n * getRoutes(): Routes[] {\n * return [\n * new ApiRoutingFactory(SaveApi, SaveController),\n * ];\n * }\n * ```\n */\n// webpieces-disable no-any-unknown -- generic class requires unconstrained default type params\nexport class ApiRoutingFactory<TApi = unknown, TController extends TApi = TApi> implements Routes {\n private apiMetaClass: ClassType<TApi>;\n private controllerClass: ClassType<TController>;\n\n /**\n * @param apiMetaClass - The API prototype class with @ApiPath/@Endpoint decorators\n * @param controllerClass - The controller class that implements the API\n */\n constructor(apiMetaClass: ClassType<TApi>, controllerClass: ClassType<TController>) {\n assertNotInternalApi(apiMetaClass, 'ApiRoutingFactory');\n this.apiMetaClass = apiMetaClass;\n this.controllerClass = controllerClass;\n\n // Validate that apiMetaClass is marked with @ApiPath\n if (!isApiPath(apiMetaClass)) {\n const className = apiMetaClass.name || 'Unknown';\n throw new Error(`Class ${className} must be decorated with @ApiPath()`);\n }\n\n // Validate that controllerClass actually extends apiMetaClass.\n // TypeScript's structural typing won't catch a missing `extends` here, so we check\n // the runtime prototype chain. Without this, a controller can silently drift from\n // the API contract (wrong method names, wrong signatures) and only fail later as a\n // confusing routing or method-not-found error.\n const apiName = apiMetaClass.name || 'Unknown';\n const controllerName = controllerClass.name || 'Unknown';\n if (\n !(apiMetaClass.prototype as object).isPrototypeOf(controllerClass.prototype as object)\n ) {\n throw new Error(\n `Controller ${controllerName} must extend ${apiName}. ` +\n `Change the class declaration to: ` +\n `'export class ${controllerName} extends ${apiName} { ... }'`,\n );\n }\n }\n\n /**\n * Configure routes by reading @ApiPath + @Endpoint metadata.\n * Validates controller methods and auth decorators in single loop.\n */\n configure(routeBuilder: RouteBuilder): void {\n // A webhook route whose transport keeps no bytes has nothing to verify. That is a wiring\n // mistake, so it dies at STARTUP naming the fix — not as a 401 in production on exactly the\n // traffic the endpoint exists for.\n assertEveryWebhookEndpointRetainsRawBody(this.apiMetaClass);\n // Two endpoints on one method + path would silently shadow each other in the route map.\n RouteMetadataFactory.assertNoDuplicateRoutes(this.apiMetaClass);\n\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 of Object.keys(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 // @WpLocalOnly: 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 (isLocalOnly(this.apiMetaClass, methodName) && !RuntimeLocality.isLocalDevelopment()) {\n log.info(\n `Skipping @WpLocalOnly 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 routeMeta = RouteMetadataFactory.create(\n this.apiMetaClass,\n methodName,\n controllerName,\n );\n\n routeBuilder.addRoute(\n new RouteDefinition(\n routeMeta,\n this.controllerClass,\n controllerFilepath,\n this.apiMetaClass,\n ),\n );\n }\n }\n\n /**\n * Get the filepath of the controller source file.\n * Uses a heuristic based on the controller class name.\n */\n private getControllerFilepath(): string | undefined {\n // Check for explicit @SourceFile decorator metadata\n const filepath = Reflect.getMetadata(\n ROUTING_METADATA_KEYS.SOURCE_FILEPATH,\n this.controllerClass,\n );\n if (filepath) {\n return filepath;\n }\n\n // Fallback to class name pattern\n const className = this.controllerClass.name;\n return className ? `**/${className}.ts` : undefined;\n }\n\n /**\n * Get auth metadata for a specific method, falling back to class-level.\n */\n getAuthMetaForMethod(methodName: string): AuthMeta | undefined {\n return getAuthMeta(this.apiMetaClass, methodName);\n }\n\n /**\n * Get the API interface class.\n */\n getApiClass(): ClassType<TApi> {\n return this.apiMetaClass;\n }\n\n /**\n * Get the controller class.\n */\n getControllerClass(): ClassType<TController> {\n return this.controllerClass;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ApiRoutingFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiRoutingFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import { Routes, RouteBuilder, RouteDefinition } from './WebAppMeta';\nimport {\n isApiPath,\n getEndpoints,\n getAuthMeta,\n assertEveryVendorAuthEndpointRetainsRawBody,\n assertNotInternalApi,\n LogManager,\n RouteMetadataFactory,\n AuthMeta,\n MISSING_AUTH_DECORATOR_FIX,\n RuntimeLocality,\n isLocalOnly,\n isExternalOutboundApi,\n} from '@webpieces/core-util';\nimport 'reflect-metadata';\nimport { ROUTING_METADATA_KEYS } from './decorators';\n\nconst log = LogManager.getLogger('ApiRoutingFactory');\n\n/**\n * Type representing a class constructor (abstract or concrete).\n */\n// webpieces-disable no-any-unknown -- generic type alias requires unconstrained default\nexport type ClassType<T = unknown> = Function & { prototype: T };\n\n/**\n * ApiRoutingFactory - Automatically wire API interfaces to controllers.\n * Reads @ApiPath/@Endpoint decorators from an API prototype class and\n * registers POST routes for each endpoint.\n *\n * Replaces the old RESTApiRoutes class.\n *\n * Usage:\n * ```typescript\n * // In your ServerMeta:\n * getRoutes(): Routes[] {\n * return [\n * new ApiRoutingFactory(SaveApi, SaveController),\n * ];\n * }\n * ```\n */\n// webpieces-disable no-any-unknown -- generic class requires unconstrained default type params\nexport class ApiRoutingFactory<TApi = unknown, TController extends TApi = TApi> implements Routes {\n private apiMetaClass: ClassType<TApi>;\n private controllerClass: ClassType<TController>;\n\n /**\n * @param apiMetaClass - The API prototype class with @ApiPath/@Endpoint decorators\n * @param controllerClass - The controller class that implements the API\n */\n constructor(apiMetaClass: ClassType<TApi>, controllerClass: ClassType<TController>) {\n assertNotInternalApi(apiMetaClass, 'ApiRoutingFactory');\n this.apiMetaClass = apiMetaClass;\n this.controllerClass = controllerClass;\n\n // Validate that apiMetaClass is marked with @ApiPath\n if (!isApiPath(apiMetaClass)) {\n const className = apiMetaClass.name || 'Unknown';\n throw new Error(`Class ${className} must be decorated with @ApiPath()`);\n }\n\n // Validate that controllerClass actually extends apiMetaClass.\n // TypeScript's structural typing won't catch a missing `extends` here, so we check\n // the runtime prototype chain. Without this, a controller can silently drift from\n // the API contract (wrong method names, wrong signatures) and only fail later as a\n // confusing routing or method-not-found error.\n const apiName = apiMetaClass.name || 'Unknown';\n const controllerName = controllerClass.name || 'Unknown';\n if (\n !(apiMetaClass.prototype as object).isPrototypeOf(controllerClass.prototype as object)\n ) {\n throw new Error(\n `Controller ${controllerName} must extend ${apiName}. ` +\n `Change the class declaration to: ` +\n `'export class ${controllerName} extends ${apiName} { ... }'`,\n );\n }\n }\n\n /**\n * Configure routes by reading @ApiPath + @Endpoint metadata.\n * Validates controller methods and auth decorators in single loop.\n */\n configure(routeBuilder: RouteBuilder): void {\n // An EXTERNAL_OUTBOUND contract is an outside system's API: nothing in this repo serves it.\n if (isExternalOutboundApi(this.apiMetaClass)) {\n throw new Error(\n `${this.apiMetaClass.name || 'Unknown'} is an EXTERNAL_OUTBOUND contract — an outside system's API that ` +\n `this repo only CALLS — so no server may addRoutes it. Bind a client to it instead ` +\n `(createRpcClientAndBind), or change its endpoints to RPC / EXTERNAL_INBOUND if this service serves them.`,\n );\n }\n // A vendorAuth 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 assertEveryVendorAuthEndpointRetainsRawBody(this.apiMetaClass);\n // Two endpoints on one method + path would silently shadow each other in the route map.\n RouteMetadataFactory.assertNoDuplicateRoutes(this.apiMetaClass);\n\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 of Object.keys(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 // @WpLocalOnly: 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 (isLocalOnly(this.apiMetaClass, methodName) && !RuntimeLocality.isLocalDevelopment()) {\n log.info(\n `Skipping @WpLocalOnly 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 routeMeta = RouteMetadataFactory.create(\n this.apiMetaClass,\n methodName,\n controllerName,\n );\n\n routeBuilder.addRoute(\n new RouteDefinition(\n routeMeta,\n this.controllerClass,\n controllerFilepath,\n this.apiMetaClass,\n ),\n );\n }\n }\n\n /**\n * Get the filepath of the controller source file.\n * Uses a heuristic based on the controller class name.\n */\n private getControllerFilepath(): string | undefined {\n // Check for explicit @SourceFile decorator metadata\n const filepath = Reflect.getMetadata(\n ROUTING_METADATA_KEYS.SOURCE_FILEPATH,\n this.controllerClass,\n );\n if (filepath) {\n return filepath;\n }\n\n // Fallback to class name pattern\n const className = this.controllerClass.name;\n return className ? `**/${className}.ts` : undefined;\n }\n\n /**\n * Get auth metadata for a specific method, falling back to class-level.\n */\n getAuthMetaForMethod(methodName: string): AuthMeta | undefined {\n return getAuthMeta(this.apiMetaClass, methodName);\n }\n\n /**\n * Get the API interface class.\n */\n getApiClass(): ClassType<TApi> {\n return this.apiMetaClass;\n }\n\n /**\n * Get the controller class.\n */\n getControllerClass(): ClassType<TController> {\n return this.controllerClass;\n }\n}\n"]}
|
package/src/AuthConfig.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { ContextKey
|
|
1
|
+
import { ContextKey } from '@webpieces/core-util';
|
|
2
|
+
import { AuthenticatedCaller } from '@webpieces/core-context';
|
|
3
|
+
export { AuthenticatedCaller, AuthenticatedMachineIdentity } from '@webpieces/core-context';
|
|
2
4
|
/**
|
|
3
5
|
* SharedSecrets - the accepted values for ONE `sharedSecret(name)`. BOTH secret1 AND secret2
|
|
4
6
|
* are accepted — this is what makes zero-downtime ROTATION possible:
|
|
@@ -14,39 +16,6 @@ export declare class SharedSecrets {
|
|
|
14
16
|
readonly secret2: string;
|
|
15
17
|
constructor(secret1: string, secret2: string);
|
|
16
18
|
}
|
|
17
|
-
/**
|
|
18
|
-
* AuthenticatedCaller - what an authenticator PROVED about the caller of ONE request. Every hook
|
|
19
|
-
* that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and
|
|
20
|
-
* {@link WebhookAuthCallback.verifyWebhook}. Four fields, three jobs:
|
|
21
|
-
*
|
|
22
|
-
* - `userId` — WHO the caller is, as the credential proved it.
|
|
23
|
-
* - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check
|
|
24
|
-
* reads, `claims` carries credential facts for the authenticator to validate and publish
|
|
25
|
-
* as trusted application context before AuthorizationHook evaluates policy.
|
|
26
|
-
* - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with
|
|
27
|
-
* {@link RequestContext.putTrusted}, so return only what THIS authenticator
|
|
28
|
-
* derived from the credential it just verified.
|
|
29
|
-
*
|
|
30
|
-
* NAMING, said out loud so it is not "fixed" back: it is deliberately NOT a `TrustedContextMap`.
|
|
31
|
-
* Three of the four fields are not context, and it is not a Map — it is the authenticated caller,
|
|
32
|
-
* and the context is one thing it carries.
|
|
33
|
-
*
|
|
34
|
-
* Data-only structure (a class, per the guidelines).
|
|
35
|
-
*/
|
|
36
|
-
export declare class AuthenticatedCaller {
|
|
37
|
-
readonly userId: string | undefined;
|
|
38
|
-
readonly roles: string[];
|
|
39
|
-
readonly entries: ContextTuple[];
|
|
40
|
-
readonly claims: Record<string, unknown>;
|
|
41
|
-
readonly machine?: AuthenticatedMachineIdentity | undefined;
|
|
42
|
-
constructor(userId: string | undefined, roles?: string[], entries?: ContextTuple[], claims?: Record<string, unknown>, machine?: AuthenticatedMachineIdentity | undefined);
|
|
43
|
-
}
|
|
44
|
-
/** Verified external machine identity does not grant trusted-header delegation. */
|
|
45
|
-
export declare class AuthenticatedMachineIdentity {
|
|
46
|
-
readonly mechanism: string;
|
|
47
|
-
readonly identity: string;
|
|
48
|
-
constructor(mechanism: string, identity: string);
|
|
49
|
-
}
|
|
50
19
|
/**
|
|
51
20
|
* The context slot holding the {@link AuthenticatedCaller} the framework {@link AuthFilter} resolved
|
|
52
21
|
* for this request. A real TRUSTED {@link ContextKey}, written with `RequestContext.putTrusted` and
|
package/src/AuthConfig.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AUTH_CONFIG = exports.AuthConfig = exports.AUTHENTICATED_CALLER_KEY = exports.
|
|
3
|
+
exports.AUTH_CONFIG = exports.AuthConfig = exports.AUTHENTICATED_CALLER_KEY = exports.SharedSecrets = exports.AuthenticatedMachineIdentity = exports.AuthenticatedCaller = void 0;
|
|
4
4
|
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
// Defined in @webpieces/core-context (beside HttpRequest, which http-routing re-exports the same way)
|
|
6
|
+
// so VendorAuthHook — ONE hook serving the client (mint/refresh) and the server (verify) — can name
|
|
7
|
+
// it from a package both the node client and this router depend on.
|
|
8
|
+
var core_context_1 = require("@webpieces/core-context");
|
|
9
|
+
Object.defineProperty(exports, "AuthenticatedCaller", { enumerable: true, get: function () { return core_context_1.AuthenticatedCaller; } });
|
|
10
|
+
Object.defineProperty(exports, "AuthenticatedMachineIdentity", { enumerable: true, get: function () { return core_context_1.AuthenticatedMachineIdentity; } });
|
|
5
11
|
/**
|
|
6
12
|
* SharedSecrets - the accepted values for ONE `sharedSecret(name)`. BOTH secret1 AND secret2
|
|
7
13
|
* are accepted — this is what makes zero-downtime ROTATION possible:
|
|
@@ -21,52 +27,6 @@ class SharedSecrets {
|
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
exports.SharedSecrets = SharedSecrets;
|
|
24
|
-
/**
|
|
25
|
-
* AuthenticatedCaller - what an authenticator PROVED about the caller of ONE request. Every hook
|
|
26
|
-
* that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and
|
|
27
|
-
* {@link WebhookAuthCallback.verifyWebhook}. Four fields, three jobs:
|
|
28
|
-
*
|
|
29
|
-
* - `userId` — WHO the caller is, as the credential proved it.
|
|
30
|
-
* - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check
|
|
31
|
-
* reads, `claims` carries credential facts for the authenticator to validate and publish
|
|
32
|
-
* as trusted application context before AuthorizationHook evaluates policy.
|
|
33
|
-
* - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with
|
|
34
|
-
* {@link RequestContext.putTrusted}, so return only what THIS authenticator
|
|
35
|
-
* derived from the credential it just verified.
|
|
36
|
-
*
|
|
37
|
-
* NAMING, said out loud so it is not "fixed" back: it is deliberately NOT a `TrustedContextMap`.
|
|
38
|
-
* Three of the four fields are not context, and it is not a Map — it is the authenticated caller,
|
|
39
|
-
* and the context is one thing it carries.
|
|
40
|
-
*
|
|
41
|
-
* Data-only structure (a class, per the guidelines).
|
|
42
|
-
*/
|
|
43
|
-
class AuthenticatedCaller {
|
|
44
|
-
userId;
|
|
45
|
-
roles;
|
|
46
|
-
entries;
|
|
47
|
-
claims;
|
|
48
|
-
machine;
|
|
49
|
-
constructor(userId, roles = [], entries = [],
|
|
50
|
-
// webpieces-disable no-any-unknown -- raw JWT claims for app-defined authorization (inOrg, tenant, ...)
|
|
51
|
-
claims = {}, machine) {
|
|
52
|
-
this.userId = userId;
|
|
53
|
-
this.roles = roles;
|
|
54
|
-
this.entries = entries;
|
|
55
|
-
this.claims = claims;
|
|
56
|
-
this.machine = machine;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
exports.AuthenticatedCaller = AuthenticatedCaller;
|
|
60
|
-
/** Verified external machine identity does not grant trusted-header delegation. */
|
|
61
|
-
class AuthenticatedMachineIdentity {
|
|
62
|
-
mechanism;
|
|
63
|
-
identity;
|
|
64
|
-
constructor(mechanism, identity) {
|
|
65
|
-
this.mechanism = mechanism;
|
|
66
|
-
this.identity = identity;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
exports.AuthenticatedMachineIdentity = AuthenticatedMachineIdentity;
|
|
70
30
|
/**
|
|
71
31
|
* The context slot holding the {@link AuthenticatedCaller} the framework {@link AuthFilter} resolved
|
|
72
32
|
* for this request. A real TRUSTED {@link ContextKey}, written with `RequestContext.putTrusted` and
|
|
@@ -83,7 +43,7 @@ exports.AuthenticatedMachineIdentity = AuthenticatedMachineIdentity;
|
|
|
83
43
|
* `isLogged` is FALSE: it is an object carrying the raw credential claims, which has no business
|
|
84
44
|
* being serialized into a log line.
|
|
85
45
|
*/
|
|
86
|
-
exports.AUTHENTICATED_CALLER_KEY = core_util_1.ContextKey.trusted('authenticatedCaller', 'resolved by the framework AuthFilter from a credential an app-bound JwtHook, ApiKeyHook or
|
|
46
|
+
exports.AUTHENTICATED_CALLER_KEY = core_util_1.ContextKey.trusted('authenticatedCaller', 'resolved by the framework AuthFilter from a credential an app-bound JwtHook, ApiKeyHook or VendorAuthHook verified',
|
|
87
47
|
/*httpHeader*/ undefined,
|
|
88
48
|
/*maskInLogs*/ false,
|
|
89
49
|
/*isLogged*/ false);
|
package/src/AuthConfig.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthConfig.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthConfig.ts"],"names":[],"mappings":";;;AAAA,oDAAkD;AAGlD,sGAAsG;AACtG,oGAAoG;AACpG,oEAAoE;AACpE,wDAA4F;AAAnF,mHAAA,mBAAmB,OAAA;AAAE,4HAAA,4BAA4B,OAAA;AAE1D;;;;;;;;;GASG;AACH,MAAa,aAAa;IAEF;IACA;IAFpB,YACoB,OAAe,EACf,OAAe;QADf,YAAO,GAAP,OAAO,CAAQ;QACf,YAAO,GAAP,OAAO,CAAQ;IAChC,CAAC;CACP;AALD,sCAKC;AAED;;;;;;;;;;;;;;;GAeG;AACU,QAAA,wBAAwB,GAAG,sBAAU,CAAC,OAAO,CACtD,qBAAqB,EACrB,oHAAoH;AACpH,cAAc,CAAC,SAAS;AACxB,cAAc,CAAC,KAAK;AACpB,YAAY,CAAC,KAAK,CACrB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAa,UAAU;IACnB,mGAAmG;IAC1F,aAAa,CAAgC;IAEtD,YAAY,gBAA+C,EAAE;QACzD,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACvC,CAAC;CACJ;AAPD,gCAOC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC","sourcesContent":["import { ContextKey } from '@webpieces/core-util';\nimport { AuthenticatedCaller } from '@webpieces/core-context';\n\n// Defined in @webpieces/core-context (beside HttpRequest, which http-routing re-exports the same way)\n// so VendorAuthHook — ONE hook serving the client (mint/refresh) and the server (verify) — can name\n// it from a package both the node client and this router depend on.\nexport { AuthenticatedCaller, AuthenticatedMachineIdentity } from '@webpieces/core-context';\n\n/**\n * SharedSecrets - the accepted values for ONE `sharedSecret(name)`. BOTH secret1 AND secret2\n * are accepted — this is what makes zero-downtime ROTATION possible:\n *\n * to rotate: shift secret2 → secret1, and put the NEW secret in secret2. Callers cut over from\n * the old value to the new during the window; once every caller sends the new one, the stale\n * value falls out on the next shift. At all times EITHER key works, so no request is dropped.\n *\n * Data-only structure (a class, per the guidelines). Leave secret2 empty for a single secret.\n */\nexport class SharedSecrets {\n constructor(\n public readonly secret1: string,\n public readonly secret2: string,\n ) {}\n}\n\n/**\n * The context slot holding the {@link AuthenticatedCaller} the framework {@link AuthFilter} resolved\n * for this request. A real TRUSTED {@link ContextKey}, written with `RequestContext.putTrusted` and\n * read with `RequestContext.getTrusted` — it replaces a raw `'__webpieces_principal__'` string that\n * was the one place in the codebase bypassing the typed context layer.\n *\n * `httpHeader` is deliberately UNDEFINED, so the key is context-only and NEVER travels. Two reasons,\n * and either alone is decisive: the value is an OBJECT, which no HTTP header can carry; and a\n * principal is proof THIS hop's authenticator produced, so forwarding it would hand the next service\n * a \"proven\" caller nothing on that hop verified. The individual facts a downstream service needs\n * (userId, orgId, roles) already propagate as their own transferred keys in\n * {@link WebpiecesCoreHeaders}, gated by the caller-verified rule.\n *\n * `isLogged` is FALSE: it is an object carrying the raw credential claims, which has no business\n * being serialized into a log line.\n */\nexport const AUTHENTICATED_CALLER_KEY = ContextKey.trusted<AuthenticatedCaller>(\n 'authenticatedCaller',\n 'resolved by the framework AuthFilter from a credential an app-bound JwtHook, ApiKeyHook or VendorAuthHook verified',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ false,\n);\n\n/**\n * AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to\n * enforce `sharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —\n * there is no verification code here. The verification MECHANISMS are separate optional hooks the\n * app binds when it needs them:\n *\n * - user JWT → bind a {@link JwtHook} (async parseJwt).\n * - api key → bind an {@link ApiKeyHook} (async verifyApiKey over the request's headers).\n * - OIDC → bind an {@link OidcHook} to override the framework's default verifier; a server that\n * binds nothing still verifies Google OIDC via the built-in {@link DefaultOidcVerifier}.\n *\n * So a zero-wiring server accepts service-to-service OIDC out of the box, and an app only binds the\n * pieces it actually uses. This class is injected `@optional` into AuthFilter (rebindable in tests);\n * when unbound, shared-secret endpoints simply have no accepted secret and fail fast (401).\n */\nexport class AuthConfig {\n /** Accepted shared-secret values keyed by `sharedSecret(name)`. DEFAULT empty — pass to enable. */\n readonly sharedSecrets: Record<string, SharedSecrets>;\n\n constructor(sharedSecrets: Record<string, SharedSecrets> = {}) {\n this.sharedSecrets = sharedSecrets;\n }\n}\n\n/**\n * DI identifier for the optional {@link AuthConfig} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(AUTH_CONFIG)`\n * correct — undefined when unbound. The AuthConfig class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const AUTH_CONFIG = Symbol.for('AuthConfig');\n"]}
|
package/src/AuthHooks.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HttpRequest
|
|
1
|
+
import { HttpRequest } from '@webpieces/core-context';
|
|
2
2
|
import { OidcAuthMethods } from '@webpieces/core-util';
|
|
3
3
|
import { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthConfig';
|
|
4
4
|
/** Framework-normalized result from an application-owned JWT mint operation. */
|
|
@@ -39,65 +39,13 @@ export declare abstract class OidcHook {
|
|
|
39
39
|
* correct — undefined when unbound. The OidcHook class stays the TYPE and the impl base.
|
|
40
40
|
*/
|
|
41
41
|
export declare const OIDC_HOOK: unique symbol;
|
|
42
|
-
/**
|
|
43
|
-
* WebhookAuthCallback - the OPTIONAL mechanism behind `webhook(name)`: prove that an inbound request was
|
|
44
|
-
* really authored by the outside vendor the contract names. Its DI token is the {@link WEBHOOK_AUTH_CALLBACK}
|
|
45
|
-
* Symbol injected via `@inject(WEBHOOK_AUTH_CALLBACK)` (a Symbol, because the app container uses autobind;
|
|
46
|
-
* rebindable in tests). The third hook, symmetric with {@link JwtHook} / {@link OidcHook}:
|
|
47
|
-
*
|
|
48
|
-
* ```typescript
|
|
49
|
-
* // AppModule.ts, beside the CompanyJwtHook binding
|
|
50
|
-
* options.bind(WEBHOOK_AUTH_CALLBACK).to(CompanyWebhookAuthCallback);
|
|
51
|
-
* ```
|
|
52
|
-
*
|
|
53
|
-
* When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every `webhook(...)` endpoint,
|
|
54
|
-
* exactly as it does for an unbound JwtHook. There is no framework default and there never will be
|
|
55
|
-
* one: silently allowing an unverified webhook is the single default that must not exist, and the
|
|
56
|
-
* framework ships no vendor crypto by design (see {@link webhook} for why reimplementing five
|
|
57
|
-
* vendors' schemes is a losing trade).
|
|
58
|
-
*
|
|
59
|
-
* ONE hook serves EVERY vendor: `name` selects which, so an app with a Sentry hook and a Twilio hook
|
|
60
|
-
* switches on it rather than binding a token per vendor. What arrives is enough of the raw request to
|
|
61
|
-
* call the vendor's OWN validator — `request.raw.rawBody` for a body-signing vendor (Sentry, GitHub,
|
|
62
|
-
* Stripe, Slack), `request.raw.absoluteUrl` for one that signs the url instead (Twilio).
|
|
63
|
-
*/
|
|
64
|
-
export declare abstract class WebhookAuthCallback {
|
|
65
|
-
/**
|
|
66
|
-
* Verify ONE inbound request. Return the {@link AuthenticatedCaller} the vendor's signature
|
|
67
|
-
* proved; throw {@link ApiUnauthorizedError} to deny.
|
|
68
|
-
*
|
|
69
|
-
* IT RETURNS A CALLER, not `void`, for the same reason {@link ApiKeyHook.verifyApiKey} does: once
|
|
70
|
-
* the signature checks out, the payload's vendor account is a PROVEN fact, and a hook that could
|
|
71
|
-
* only return `void` had no way to say so. The framework seeds `entries` with
|
|
72
|
-
* `RequestContext.putTrusted` exactly as it does for a jwt or api-key caller, so a controller
|
|
73
|
-
* reads which vendor account this webhook is for off the context instead of re-deriving it.
|
|
74
|
-
*
|
|
75
|
-
* Return only what THIS hook proved from the signature it just verified. `webhook` remains
|
|
76
|
-
* caller-NOT-verified (see `AuthFilter.verifiesCaller`): a vendor is not a peer service, so
|
|
77
|
-
* nothing the vendor merely ASSERTED on the wire is admitted.
|
|
78
|
-
*
|
|
79
|
-
* @param name the string on the contract's `webhook(name)` — which vendor this route is.
|
|
80
|
-
* @param request the transport-neutral request, narrowed to {@link RawHttpRequest}: `request.raw`
|
|
81
|
-
* holds the verbatim bytes + absolute url and is PRESENT, never optional.
|
|
82
|
-
* `webhook(...)` requires `@Endpoint(..., { rawBody: true })` at wiring time, and
|
|
83
|
-
* AuthFilter 401s rather than calling this hook with nothing to check — so an
|
|
84
|
-
* implementation never writes `raw!` or a guard of its own.
|
|
85
|
-
*/
|
|
86
|
-
abstract verifyWebhook(name: string, request: RawHttpRequest): Promise<AuthenticatedCaller>;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* DI identifier for the optional {@link WebhookAuthCallback} binding. It is a Symbol (not the class) so the app
|
|
90
|
-
* container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(WEBHOOK_AUTH_CALLBACK)`
|
|
91
|
-
* correct — undefined when unbound. The WebhookAuthCallback class stays the TYPE and the impl base.
|
|
92
|
-
*/
|
|
93
|
-
export declare const WEBHOOK_AUTH_CALLBACK: unique symbol;
|
|
94
42
|
/**
|
|
95
43
|
* ApiKeyHook - the OPTIONAL mechanism behind `apiKey(regime, credentials)`: authenticate a
|
|
96
44
|
* CUSTOMER-held api key
|
|
97
45
|
* against the app's own datastore and return the context to seed. Its DI token is the
|
|
98
46
|
* {@link API_KEY_HOOK} Symbol injected via `@inject(API_KEY_HOOK)` (a Symbol, because the app container
|
|
99
47
|
* uses autobind; rebindable in tests). The fourth hook, symmetric with {@link JwtHook} /
|
|
100
|
-
* {@link OidcHook} /
|
|
48
|
+
* {@link OidcHook} / `VendorAuthHook` (@webpieces/core-context):
|
|
101
49
|
*
|
|
102
50
|
* ```typescript
|
|
103
51
|
* // AppModule.ts, beside the CompanyJwtHook binding
|
package/src/AuthHooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.API_KEY_HOOK = exports.ApiKeyHook = exports.
|
|
3
|
+
exports.API_KEY_HOOK = exports.ApiKeyHook = exports.OIDC_HOOK = exports.OidcHook = exports.JWT_HOOK = exports.JwtHook = exports.MintedJwt = void 0;
|
|
4
4
|
/** Framework-normalized result from an application-owned JWT mint operation. */
|
|
5
5
|
class MintedJwt {
|
|
6
6
|
token;
|
|
@@ -49,45 +49,13 @@ exports.OidcHook = OidcHook;
|
|
|
49
49
|
*/
|
|
50
50
|
// 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)
|
|
51
51
|
exports.OIDC_HOOK = Symbol.for('OidcHook');
|
|
52
|
-
/**
|
|
53
|
-
* WebhookAuthCallback - the OPTIONAL mechanism behind `webhook(name)`: prove that an inbound request was
|
|
54
|
-
* really authored by the outside vendor the contract names. Its DI token is the {@link WEBHOOK_AUTH_CALLBACK}
|
|
55
|
-
* Symbol injected via `@inject(WEBHOOK_AUTH_CALLBACK)` (a Symbol, because the app container uses autobind;
|
|
56
|
-
* rebindable in tests). The third hook, symmetric with {@link JwtHook} / {@link OidcHook}:
|
|
57
|
-
*
|
|
58
|
-
* ```typescript
|
|
59
|
-
* // AppModule.ts, beside the CompanyJwtHook binding
|
|
60
|
-
* options.bind(WEBHOOK_AUTH_CALLBACK).to(CompanyWebhookAuthCallback);
|
|
61
|
-
* ```
|
|
62
|
-
*
|
|
63
|
-
* When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every `webhook(...)` endpoint,
|
|
64
|
-
* exactly as it does for an unbound JwtHook. There is no framework default and there never will be
|
|
65
|
-
* one: silently allowing an unverified webhook is the single default that must not exist, and the
|
|
66
|
-
* framework ships no vendor crypto by design (see {@link webhook} for why reimplementing five
|
|
67
|
-
* vendors' schemes is a losing trade).
|
|
68
|
-
*
|
|
69
|
-
* ONE hook serves EVERY vendor: `name` selects which, so an app with a Sentry hook and a Twilio hook
|
|
70
|
-
* switches on it rather than binding a token per vendor. What arrives is enough of the raw request to
|
|
71
|
-
* call the vendor's OWN validator — `request.raw.rawBody` for a body-signing vendor (Sentry, GitHub,
|
|
72
|
-
* Stripe, Slack), `request.raw.absoluteUrl` for one that signs the url instead (Twilio).
|
|
73
|
-
*/
|
|
74
|
-
class WebhookAuthCallback {
|
|
75
|
-
}
|
|
76
|
-
exports.WebhookAuthCallback = WebhookAuthCallback;
|
|
77
|
-
/**
|
|
78
|
-
* DI identifier for the optional {@link WebhookAuthCallback} binding. It is a Symbol (not the class) so the app
|
|
79
|
-
* container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(WEBHOOK_AUTH_CALLBACK)`
|
|
80
|
-
* correct — undefined when unbound. The WebhookAuthCallback class stays the TYPE and the impl base.
|
|
81
|
-
*/
|
|
82
|
-
// 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)
|
|
83
|
-
exports.WEBHOOK_AUTH_CALLBACK = Symbol.for('WebhookAuthCallback');
|
|
84
52
|
/**
|
|
85
53
|
* ApiKeyHook - the OPTIONAL mechanism behind `apiKey(regime, credentials)`: authenticate a
|
|
86
54
|
* CUSTOMER-held api key
|
|
87
55
|
* against the app's own datastore and return the context to seed. Its DI token is the
|
|
88
56
|
* {@link API_KEY_HOOK} Symbol injected via `@inject(API_KEY_HOOK)` (a Symbol, because the app container
|
|
89
57
|
* uses autobind; rebindable in tests). The fourth hook, symmetric with {@link JwtHook} /
|
|
90
|
-
* {@link OidcHook} /
|
|
58
|
+
* {@link OidcHook} / `VendorAuthHook` (@webpieces/core-context):
|
|
91
59
|
*
|
|
92
60
|
* ```typescript
|
|
93
61
|
* // AppModule.ts, beside the CompanyJwtHook binding
|
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":";;;AAIA,gFAAgF;AAChF,MAAa,SAAS;IAEE;IACA;IAFpB,YACoB,KAAa,EACb,qBAA6B;QAD7B,UAAK,GAAL,KAAK,CAAQ;QACb,0BAAqB,GAArB,qBAAqB,CAAQ;QAE7C,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QAChF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC9E,CAAC;IACL,CAAC;CACJ;AAVD,8BAUC;AAED,+FAA+F;AAC/F,MAAsB,OAAO;CAG5B;AAHD,0BAGC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAE9C;;;;;;;;;;;GAWG;AACH,MAAsB,QAAQ;CAE7B;AAFD,4BAEC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAsB,mBAAmB;CAuBxC;AAvBD,kDAuBC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAsB,UAAU;CAiB/B;AAjBD,gCAiBC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC","sourcesContent":["import { HttpRequest, RawHttpRequest } from '@webpieces/core-context';\nimport { OidcAuthMethods } from '@webpieces/core-util';\nimport { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthConfig';\n\n/** Framework-normalized result from an application-owned JWT mint operation. */\nexport class MintedJwt {\n constructor(\n public readonly token: string,\n public readonly expiresAtEpochSeconds: number,\n ) {\n if (token.trim() === '') throw new Error('Minted JWT token must be non-empty.');\n if (!Number.isFinite(expiresAtEpochSeconds)) {\n throw new Error('Minted JWT expiry must be a finite epoch-second value.');\n }\n }\n}\n\n/** Optional application JWT credential authority. Authorization lives in AuthorizationHook. */\nexport abstract class JwtHook<TMintRequest> {\n abstract mint(request: TMintRequest): Promise<MintedJwt>;\n abstract parseJwt(token: string): Promise<AuthenticatedCaller>;\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 the endpoint's caller\n * scopes. `verifyOidc` receives EVERY OIDC method the endpoint declares ({@link OidcAuthMethods}: one,\n * or one `oidcSameProject()` (`callerScope: 'same-project'`) plus one `oidcSvcs([...])`\n * (`callerScope: 'services'` with its `callers`)), because they are one bearer credential: verify\n * the token once, admit the call when ANY of them admits the caller, and throw otherwise.\n */\nexport abstract class OidcHook {\n abstract verifyOidc(token: string, methods: OidcAuthMethods): Promise<AuthenticatedMachineIdentity>;\n}\n\n/**\n * DI identifier for the optional {@link OidcHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(OIDC_HOOK)`\n * correct — undefined when unbound. The OidcHook class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const OIDC_HOOK = Symbol.for('OidcHook');\n\n/**\n * WebhookAuthCallback - the OPTIONAL mechanism behind `webhook(name)`: prove that an inbound request was\n * really authored by the outside vendor the contract names. Its DI token is the {@link WEBHOOK_AUTH_CALLBACK}\n * Symbol injected via `@inject(WEBHOOK_AUTH_CALLBACK)` (a Symbol, because the app container uses autobind;\n * rebindable in tests). The third hook, symmetric with {@link JwtHook} / {@link OidcHook}:\n *\n * ```typescript\n * // AppModule.ts, beside the CompanyJwtHook binding\n * options.bind(WEBHOOK_AUTH_CALLBACK).to(CompanyWebhookAuthCallback);\n * ```\n *\n * When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every `webhook(...)` 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 webhook} for why reimplementing five\n * vendors' schemes is a losing trade).\n *\n * ONE hook serves EVERY vendor: `name` selects which, so an app with a Sentry hook and a Twilio hook\n * switches on it rather than binding a token per vendor. What arrives is enough of the raw request to\n * call the vendor's OWN validator — `request.raw.rawBody` for a body-signing vendor (Sentry, GitHub,\n * Stripe, Slack), `request.raw.absoluteUrl` for one that signs the url instead (Twilio).\n */\nexport abstract class WebhookAuthCallback {\n /**\n * Verify ONE inbound request. Return the {@link AuthenticatedCaller} the vendor's signature\n * proved; throw {@link ApiUnauthorizedError} to deny.\n *\n * IT RETURNS A CALLER, not `void`, for the same reason {@link ApiKeyHook.verifyApiKey} does: once\n * the signature checks out, the payload's vendor account is a PROVEN fact, and a hook that could\n * only return `void` had no way to say so. The framework seeds `entries` with\n * `RequestContext.putTrusted` exactly as it does for a jwt or api-key caller, so a controller\n * reads which vendor account this webhook is for off the context instead of re-deriving it.\n *\n * Return only what THIS hook proved from the signature it just verified. `webhook` remains\n * caller-NOT-verified (see `AuthFilter.verifiesCaller`): a vendor is not a peer service, so\n * nothing the vendor merely ASSERTED on the wire is admitted.\n *\n * @param name the string on the contract's `webhook(name)` — which vendor this route is.\n * @param request the transport-neutral request, narrowed to {@link RawHttpRequest}: `request.raw`\n * holds the verbatim bytes + absolute url and is PRESENT, never optional.\n * `webhook(...)` requires `@Endpoint(..., { rawBody: true })` at wiring time, and\n * AuthFilter 401s rather than calling this hook with nothing to check — so an\n * implementation never writes `raw!` or a guard of its own.\n */\n abstract verifyWebhook(name: string, request: RawHttpRequest): Promise<AuthenticatedCaller>;\n}\n\n/**\n * DI identifier for the optional {@link WebhookAuthCallback} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(WEBHOOK_AUTH_CALLBACK)`\n * correct — undefined when unbound. The WebhookAuthCallback class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const WEBHOOK_AUTH_CALLBACK = Symbol.for('WebhookAuthCallback');\n\n/**\n * ApiKeyHook - the OPTIONAL mechanism behind `apiKey(regime, credentials)`: authenticate a\n * CUSTOMER-held api key\n * against the app's own datastore and return the context to seed. Its DI token is the\n * {@link API_KEY_HOOK} Symbol injected via `@inject(API_KEY_HOOK)` (a Symbol, because the app container\n * uses autobind; rebindable in tests). The fourth hook, symmetric with {@link JwtHook} /\n * {@link OidcHook} / {@link WebhookAuthCallback}:\n *\n * ```typescript\n * // AppModule.ts, beside the CompanyJwtHook binding\n * options.bind(API_KEY_HOOK).to(OneTabletApiKeyHook);\n * ```\n *\n * When NO ApiKeyHook is bound, the framework {@link AuthFilter} 401s every `apiKey(...)` endpoint,\n * exactly as it does for an unbound JwtHook. There is no framework default and there never will be\n * one: the key regime lives in the app's datastore, under the app's hashing scheme, behind the app's\n * choice of header names.\n *\n * THE ONE THING THIS HAS THAT `JwtHook.parseJwt` DOES NOT: it receives the whole {@link HttpRequest},\n * not one pre-extracted token. A real key regime validates the key TOGETHER WITH a second header — the\n * organization the caller is acting for — and a hook handed one header's value physically cannot do\n * that cross-check. The framework therefore EXTRACTS no api-key header: which headers carry the\n * credential is the app's business, and `getHeader` / `getHeaderValues` read as many as the regime\n * needs. The contract's `credentials` list DECLARES those header names for readers and spec\n * generators — it is documentation of the regime, never an instruction to this hook, so the hook\n * stays the single place the pair is actually validated. (Being ASYNC is no longer a difference — every hook here is, and for the same reason: an\n * app's strategy reaches the network.)\n *\n * ONE hook serves EVERY regime: `regime` selects which, so a server with a partner-api regime and an\n * internal-tooling regime switches on it rather than binding a token per regime.\n */\nexport abstract class ApiKeyHook {\n /**\n * AUTHENTICATE one inbound request. Return who the caller is plus the {@link AuthenticatedCaller.entries}\n * the framework seeds into `RequestContext` via `putTrusted`, or throw\n * {@link ApiUnauthorizedError} to deny.\n *\n * NOTE the seeded entries are TRUSTED context keys, so return only what THIS hook proved from the\n * credential it just verified. Anything the caller merely asserted on the wire is not admitted by\n * `apiKey(...)` — the mode is deliberately caller-NOT-verified (see `AuthFilter.verifiesCaller`),\n * because a customer is not an internal service.\n *\n * @param regime the first argument of the contract's `apiKey(regime, credentials)` — which key\n * regime this route belongs to.\n * @param request the inbound request; read as many headers as the regime needs with\n * `getHeader` / `getHeaderValues`, either by raw name or by {@link ContextKey}.\n */\n abstract verifyApiKey(regime: string, request: HttpRequest): Promise<AuthenticatedCaller>;\n}\n\n/**\n * DI identifier for the optional {@link ApiKeyHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(API_KEY_HOOK)`\n * correct — undefined when unbound. The ApiKeyHook class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const API_KEY_HOOK = Symbol.for('ApiKeyHook');\n"]}
|
|
1
|
+
{"version":3,"file":"AuthHooks.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooks.ts"],"names":[],"mappings":";;;AAIA,gFAAgF;AAChF,MAAa,SAAS;IAEE;IACA;IAFpB,YACoB,KAAa,EACb,qBAA6B;QAD7B,UAAK,GAAL,KAAK,CAAQ;QACb,0BAAqB,GAArB,qBAAqB,CAAQ;QAE7C,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QAChF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC9E,CAAC;IACL,CAAC;CACJ;AAVD,8BAUC;AAED,+FAA+F;AAC/F,MAAsB,OAAO;CAG5B;AAHD,0BAGC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;AAE9C;;;;;;;;;;;GAWG;AACH,MAAsB,QAAQ;CAE7B;AAFD,4BAEC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAsB,UAAU;CAiB/B;AAjBD,gCAiBC;AAED;;;;GAIG;AACH,mNAAmN;AACtM,QAAA,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC","sourcesContent":["import { HttpRequest, RawHttpRequest } from '@webpieces/core-context';\nimport { OidcAuthMethods } from '@webpieces/core-util';\nimport { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthConfig';\n\n/** Framework-normalized result from an application-owned JWT mint operation. */\nexport class MintedJwt {\n constructor(\n public readonly token: string,\n public readonly expiresAtEpochSeconds: number,\n ) {\n if (token.trim() === '') throw new Error('Minted JWT token must be non-empty.');\n if (!Number.isFinite(expiresAtEpochSeconds)) {\n throw new Error('Minted JWT expiry must be a finite epoch-second value.');\n }\n }\n}\n\n/** Optional application JWT credential authority. Authorization lives in AuthorizationHook. */\nexport abstract class JwtHook<TMintRequest> {\n abstract mint(request: TMintRequest): Promise<MintedJwt>;\n abstract parseJwt(token: string): Promise<AuthenticatedCaller>;\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 the endpoint's caller\n * scopes. `verifyOidc` receives EVERY OIDC method the endpoint declares ({@link OidcAuthMethods}: one,\n * or one `oidcSameProject()` (`callerScope: 'same-project'`) plus one `oidcSvcs([...])`\n * (`callerScope: 'services'` with its `callers`)), because they are one bearer credential: verify\n * the token once, admit the call when ANY of them admits the caller, and throw otherwise.\n */\nexport abstract class OidcHook {\n abstract verifyOidc(token: string, methods: OidcAuthMethods): Promise<AuthenticatedMachineIdentity>;\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 * ApiKeyHook - the OPTIONAL mechanism behind `apiKey(regime, credentials)`: authenticate a\n * CUSTOMER-held api key\n * against the app's own datastore and return the context to seed. Its DI token is the\n * {@link API_KEY_HOOK} Symbol injected via `@inject(API_KEY_HOOK)` (a Symbol, because the app container\n * uses autobind; rebindable in tests). The fourth hook, symmetric with {@link JwtHook} /\n * {@link OidcHook} / `VendorAuthHook` (@webpieces/core-context):\n *\n * ```typescript\n * // AppModule.ts, beside the CompanyJwtHook binding\n * options.bind(API_KEY_HOOK).to(OneTabletApiKeyHook);\n * ```\n *\n * When NO ApiKeyHook is bound, the framework {@link AuthFilter} 401s every `apiKey(...)` endpoint,\n * exactly as it does for an unbound JwtHook. There is no framework default and there never will be\n * one: the key regime lives in the app's datastore, under the app's hashing scheme, behind the app's\n * choice of header names.\n *\n * THE ONE THING THIS HAS THAT `JwtHook.parseJwt` DOES NOT: it receives the whole {@link HttpRequest},\n * not one pre-extracted token. A real key regime validates the key TOGETHER WITH a second header — the\n * organization the caller is acting for — and a hook handed one header's value physically cannot do\n * that cross-check. The framework therefore EXTRACTS no api-key header: which headers carry the\n * credential is the app's business, and `getHeader` / `getHeaderValues` read as many as the regime\n * needs. The contract's `credentials` list DECLARES those header names for readers and spec\n * generators — it is documentation of the regime, never an instruction to this hook, so the hook\n * stays the single place the pair is actually validated. (Being ASYNC is no longer a difference — every hook here is, and for the same reason: an\n * app's strategy reaches the network.)\n *\n * ONE hook serves EVERY regime: `regime` selects which, so a server with a partner-api regime and an\n * internal-tooling regime switches on it rather than binding a token per regime.\n */\nexport abstract class ApiKeyHook {\n /**\n * AUTHENTICATE one inbound request. Return who the caller is plus the {@link AuthenticatedCaller.entries}\n * the framework seeds into `RequestContext` via `putTrusted`, or throw\n * {@link ApiUnauthorizedError} to deny.\n *\n * NOTE the seeded entries are TRUSTED context keys, so return only what THIS hook proved from the\n * credential it just verified. Anything the caller merely asserted on the wire is not admitted by\n * `apiKey(...)` — the mode is deliberately caller-NOT-verified (see `AuthFilter.verifiesCaller`),\n * because a customer is not an internal service.\n *\n * @param regime the first argument of the contract's `apiKey(regime, credentials)` — which key\n * regime this route belongs to.\n * @param request the inbound request; read as many headers as the regime needs with\n * `getHeader` / `getHeaderValues`, either by raw name or by {@link ContextKey}.\n */\n abstract verifyApiKey(regime: string, request: HttpRequest): Promise<AuthenticatedCaller>;\n}\n\n/**\n * DI identifier for the optional {@link ApiKeyHook} binding. It is a Symbol (not the class) so the app\n * container's inversify autobind never auto-constructs this token, keeping `@optional() @inject(API_KEY_HOOK)`\n * correct — undefined when unbound. The ApiKeyHook class stays the TYPE and the impl base.\n */\n// webpieces-disable no-symbol-di-tokens -- optional DI token: must be a Symbol so the app container's autobind never auto-constructs this token, keeping @optional() @inject(...) correct (undefined when unbound)\nexport const API_KEY_HOOK = Symbol.for('ApiKeyHook');\n"]}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AuthHooksCompileAssertions = void 0;
|
|
4
4
|
const core_context_1 = require("@webpieces/core-context");
|
|
5
|
+
const core_context_2 = require("@webpieces/core-context");
|
|
5
6
|
const AuthHooks_1 = require("./AuthHooks");
|
|
6
7
|
const AuthConfig_1 = require("./AuthConfig");
|
|
7
8
|
/**
|
|
@@ -28,13 +29,21 @@ class AuthHooksCompileAssertions {
|
|
|
28
29
|
deletedNames() {
|
|
29
30
|
const headerReaderIsGone = undefined;
|
|
30
31
|
const authValuesIsGone = undefined;
|
|
32
|
+
const webhookAuthCallbackIsGone = undefined;
|
|
31
33
|
void headerReaderIsGone;
|
|
32
34
|
void authValuesIsGone;
|
|
35
|
+
void webhookAuthCallbackIsGone;
|
|
33
36
|
}
|
|
34
37
|
/** The ALIGNED spellings must keep compiling; asserted by the ABSENCE of an error. */
|
|
35
38
|
legitimate() {
|
|
36
|
-
void class extends
|
|
37
|
-
async
|
|
39
|
+
void class extends core_context_2.VendorAuthHook {
|
|
40
|
+
async mint() {
|
|
41
|
+
return new Map([['Authorization', 'Bearer t']]);
|
|
42
|
+
}
|
|
43
|
+
async refresh() {
|
|
44
|
+
// renew the vendor credential
|
|
45
|
+
}
|
|
46
|
+
async verify(_name, request) {
|
|
38
47
|
// No `raw!`, no `if (!raw) throw`: AuthFilter checked once and the TYPE carries it.
|
|
39
48
|
const bytes = request.raw.rawBody;
|
|
40
49
|
return new AuthConfig_1.AuthenticatedCaller(`sentry:${bytes.length}`);
|
|
@@ -50,18 +59,27 @@ class AuthHooksCompileAssertions {
|
|
|
50
59
|
}
|
|
51
60
|
/** Every spelling this change deleted must now be UNWRITABLE. */
|
|
52
61
|
rejected() {
|
|
53
|
-
void class extends
|
|
54
|
-
async
|
|
55
|
-
return new
|
|
62
|
+
void class extends core_context_2.VendorAuthHook {
|
|
63
|
+
async mint() {
|
|
64
|
+
return new Map();
|
|
65
|
+
}
|
|
66
|
+
async refresh() {
|
|
67
|
+
// renew the vendor credential
|
|
56
68
|
}
|
|
57
|
-
// @ts-expect-error
|
|
69
|
+
// @ts-expect-error verify takes (name, RawHttpRequest): the three-parameter spelling is gone
|
|
58
70
|
async verify(_name, _request, _raw) {
|
|
59
|
-
|
|
71
|
+
return new AuthConfig_1.AuthenticatedCaller('u1');
|
|
60
72
|
}
|
|
61
73
|
};
|
|
62
|
-
void class extends
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
void class extends core_context_2.VendorAuthHook {
|
|
75
|
+
async mint() {
|
|
76
|
+
return new Map();
|
|
77
|
+
}
|
|
78
|
+
async refresh() {
|
|
79
|
+
// renew the vendor credential
|
|
80
|
+
}
|
|
81
|
+
// @ts-expect-error verify returns the caller it proved: a void override must not compile
|
|
82
|
+
async verify(_name, _request) {
|
|
65
83
|
// a hook that verifies and then has no way to say what it proved
|
|
66
84
|
}
|
|
67
85
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthHooksCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooksCompileAssertions.ts"],"names":[],"mappings":"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","sourcesContent":["import { HttpRequest, RawHttpRequest, RawRequest } from '@webpieces/core-context';\n// @ts-expect-error HeaderReader is DELETED — an ApiKeyHook now receives the whole HttpRequest, so it\n// reaches getHeaderValues and the ContextKey overload of getHeader that the narrow reader hid.\nimport type { HeaderReader } from './AuthHooks';\n// @ts-expect-error AuthValues is RENAMED to AuthenticatedCaller — there is no alias, by policy.\nimport type { AuthValues } from './AuthConfig';\nimport { OidcAuthMethod, OidcAuthMethods } from '@webpieces/core-util';\nimport { ApiKeyHook, OidcHook, WebhookAuthCallback } from './AuthHooks';\nimport { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthConfig';\n\n/**\n * COMPILE-TIME assertions that the four auth hooks share ONE shape, and that every spelling this\n * change deleted has actually stopped compiling. Each `@ts-expect-error` FAILS THE BUILD (TS2578,\n * \"unused '@ts-expect-error' directive\") the moment the thing it guards starts compiling again.\n *\n * WHY THIS IS NOT A `.spec.ts` FILE — the same reason as {@link JwtHookCompileAssertions}:\n * tsconfig.lib.json EXCLUDES specs and vitest strips types with esbuild, so a `@ts-expect-error` in a\n * spec is inert and the suite passes either way. A type-level guarantee has to be asserted in a file\n * the type-checker actually compiles.\n *\n * WHY IT MATTERS HERE. This repo ships no backwards-compatibility shims: the compile error IS the\n * migration. An implementor that kept `verify(name, request, raw)` or a `HeaderReader` parameter\n * would otherwise sit there compiling, and an accepted shape is never migrated.\n */\nexport class AuthHooksCompileAssertions {\n /**\n * The DELETED type names, kept referenced so the two `@ts-expect-error`s above are load-bearing\n * rather than decorative. Both resolve to `any` under the suppressed error; the assertion is the\n * IMPORT failing, not what these aliases denote.\n */\n // webpieces-disable no-any-unknown -- a suppressed import of a deleted symbol resolves to any; the assertion is the import error itself\n deletedNames(): void {\n const headerReaderIsGone: HeaderReader | undefined = undefined;\n const authValuesIsGone: AuthValues | undefined = undefined;\n void headerReaderIsGone;\n void authValuesIsGone;\n }\n\n /** The ALIGNED spellings must keep compiling; asserted by the ABSENCE of an error. */\n legitimate(): void {\n void class extends WebhookAuthCallback {\n override async verifyWebhook(_name: string, request: RawHttpRequest): Promise<AuthenticatedCaller> {\n // No `raw!`, no `if (!raw) throw`: AuthFilter checked once and the TYPE carries it.\n const bytes: Buffer = request.raw.rawBody;\n return new AuthenticatedCaller(`sentry:${bytes.length}`);\n }\n };\n void class extends ApiKeyHook {\n override async verifyApiKey(_name: string, request: HttpRequest): Promise<AuthenticatedCaller> {\n // Both of these were UNREACHABLE through the deleted one-method HeaderReader.\n const all: string[] | undefined = request.getHeaderValues('x-api-key');\n return new AuthenticatedCaller(all?.[0] ?? 'anonymous');\n }\n };\n }\n\n /** Every spelling this change deleted must now be UNWRITABLE. */\n rejected(): void {\n void class extends WebhookAuthCallback {\n override async verifyWebhook(_name: string, _request: RawHttpRequest): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('u1');\n }\n\n // @ts-expect-error `verify` was renamed to `verifyWebhook`: the old name overrides nothing\n override async verify(_name: string, _request: HttpRequest, _raw: RawRequest): Promise<void> {\n // the three-parameter, void-returning spelling this change deletes\n }\n };\n void class extends WebhookAuthCallback {\n // @ts-expect-error verifyWebhook returns the caller it proved: a void override must not compile\n override async verifyWebhook(_name: string, _request: RawHttpRequest): Promise<void> {\n // a hook that verifies and then has no way to say what it proved\n }\n };\n }\n\n /**\n * {@link OidcHook.verifyOidc} receives EVERY OIDC method the endpoint declares ({@link OidcAuthMethods}),\n * so a hook verifies the token once and can tell `oidcSameProject()` from `oidcSvcs([...])`. The\n * caller-list override this change deletes (`callers: string[]`, where an EMPTY list meant \"any\n * Google-signed caller\") and a one-method override, which could not see the other scope of a pair,\n * must not compile.\n */\n oidcHookTakesTheMethods(): void {\n void class extends OidcHook {\n override async verifyOidc(_token: string, methods: OidcAuthMethods): Promise<AuthenticatedMachineIdentity> {\n const first = methods[0];\n const named = first.callerScope === 'services' ? first.callers[0] : 'same-project';\n return new AuthenticatedMachineIdentity('oidc', named);\n }\n };\n void class extends OidcHook {\n // @ts-expect-error verifyOidc takes the endpoint's OidcAuthMethods; the deleted caller-list override must not compile\n override async verifyOidc(_token: string, _callers: string[]): Promise<AuthenticatedMachineIdentity> {\n return new AuthenticatedMachineIdentity('oidc', 'caller@example.test');\n }\n };\n void class extends OidcHook {\n // @ts-expect-error verifyOidc takes every OIDC method of the endpoint, not one, so a pair is verified once\n override async verifyOidc(_token: string, _method: OidcAuthMethod): Promise<AuthenticatedMachineIdentity> {\n return new AuthenticatedMachineIdentity('oidc', 'caller@example.test');\n }\n };\n }\n\n /**\n * An {@link HttpRequest} whose `raw` is merely OPTIONAL is NOT a {@link RawHttpRequest}. This is\n * the assignment that makes the narrowing real: without it, `RawHttpRequest` would be a comment.\n */\n rawIsNotOptional(): void {\n const maybeRaw = new HttpRequest('POST', '/hook/sentry/issue', new Map<string, string[]>());\n // @ts-expect-error HttpRequest.raw is optional, so it is not assignable to RawHttpRequest\n const narrowed: RawHttpRequest = maybeRaw;\n void narrowed;\n\n // The other direction is free: a RawHttpRequest IS an HttpRequest everywhere one is wanted.\n const proven = new HttpRequest(\n 'POST',\n '/hook/sentry/issue',\n new Map<string, string[]>(),\n new RawRequest('https://example.com/hook', Buffer.from('{}', 'utf8'), '1.2.3.4'),\n ) as RawHttpRequest;\n const widened: HttpRequest = proven;\n void widened;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AuthHooksCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthHooksCompileAssertions.ts"],"names":[],"mappings":"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","sourcesContent":["import { HttpRequest, RawHttpRequest, RawRequest } from '@webpieces/core-context';\n// @ts-expect-error HeaderReader is DELETED — an ApiKeyHook now receives the whole HttpRequest, so it\n// reaches getHeaderValues and the ContextKey overload of getHeader that the narrow reader hid.\nimport type { HeaderReader } from './AuthHooks';\n// @ts-expect-error AuthValues is RENAMED to AuthenticatedCaller — there is no alias, by policy.\nimport type { AuthValues } from './AuthConfig';\nimport { OidcAuthMethod, OidcAuthMethods } from '@webpieces/core-util';\nimport { VendorAuthHook } from '@webpieces/core-context';\n// @ts-expect-error WebhookAuthCallback is REPLACED by VendorAuthHook (mint/refresh/verify) — no alias, by policy.\nimport type { WebhookAuthCallback } from './AuthHooks';\nimport { ApiKeyHook, OidcHook } from './AuthHooks';\nimport { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthConfig';\n\n/**\n * COMPILE-TIME assertions that the four auth hooks share ONE shape, and that every spelling this\n * change deleted has actually stopped compiling. Each `@ts-expect-error` FAILS THE BUILD (TS2578,\n * \"unused '@ts-expect-error' directive\") the moment the thing it guards starts compiling again.\n *\n * WHY THIS IS NOT A `.spec.ts` FILE — the same reason as {@link JwtHookCompileAssertions}:\n * tsconfig.lib.json EXCLUDES specs and vitest strips types with esbuild, so a `@ts-expect-error` in a\n * spec is inert and the suite passes either way. A type-level guarantee has to be asserted in a file\n * the type-checker actually compiles.\n *\n * WHY IT MATTERS HERE. This repo ships no backwards-compatibility shims: the compile error IS the\n * migration. An implementor that kept `verify(name, request, raw)` or a `HeaderReader` parameter\n * would otherwise sit there compiling, and an accepted shape is never migrated.\n */\nexport class AuthHooksCompileAssertions {\n /**\n * The DELETED type names, kept referenced so the two `@ts-expect-error`s above are load-bearing\n * rather than decorative. Both resolve to `any` under the suppressed error; the assertion is the\n * IMPORT failing, not what these aliases denote.\n */\n // webpieces-disable no-any-unknown -- a suppressed import of a deleted symbol resolves to any; the assertion is the import error itself\n deletedNames(): void {\n const headerReaderIsGone: HeaderReader | undefined = undefined;\n const authValuesIsGone: AuthValues | undefined = undefined;\n const webhookAuthCallbackIsGone: WebhookAuthCallback | undefined = undefined;\n void headerReaderIsGone;\n void authValuesIsGone;\n void webhookAuthCallbackIsGone;\n }\n\n /** The ALIGNED spellings must keep compiling; asserted by the ABSENCE of an error. */\n legitimate(): void {\n void class extends VendorAuthHook {\n override async mint(): Promise<Map<string, string>> {\n return new Map([['Authorization', 'Bearer t']]);\n }\n\n override async refresh(): Promise<void> {\n // renew the vendor credential\n }\n\n override async verify(_name: string, request: RawHttpRequest): Promise<AuthenticatedCaller> {\n // No `raw!`, no `if (!raw) throw`: AuthFilter checked once and the TYPE carries it.\n const bytes: Buffer = request.raw.rawBody;\n return new AuthenticatedCaller(`sentry:${bytes.length}`);\n }\n };\n void class extends ApiKeyHook {\n override async verifyApiKey(_name: string, request: HttpRequest): Promise<AuthenticatedCaller> {\n // Both of these were UNREACHABLE through the deleted one-method HeaderReader.\n const all: string[] | undefined = request.getHeaderValues('x-api-key');\n return new AuthenticatedCaller(all?.[0] ?? 'anonymous');\n }\n };\n }\n\n /** Every spelling this change deleted must now be UNWRITABLE. */\n rejected(): void {\n void class extends VendorAuthHook {\n override async mint(): Promise<Map<string, string>> {\n return new Map();\n }\n\n override async refresh(): Promise<void> {\n // renew the vendor credential\n }\n\n // @ts-expect-error verify takes (name, RawHttpRequest): the three-parameter spelling is gone\n override async verify(_name: string, _request: HttpRequest, _raw: RawRequest): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller('u1');\n }\n };\n void class extends VendorAuthHook {\n override async mint(): Promise<Map<string, string>> {\n return new Map();\n }\n\n override async refresh(): Promise<void> {\n // renew the vendor credential\n }\n\n // @ts-expect-error verify returns the caller it proved: a void override must not compile\n override async verify(_name: string, _request: RawHttpRequest): Promise<void> {\n // a hook that verifies and then has no way to say what it proved\n }\n };\n }\n\n /**\n * {@link OidcHook.verifyOidc} receives EVERY OIDC method the endpoint declares ({@link OidcAuthMethods}),\n * so a hook verifies the token once and can tell `oidcSameProject()` from `oidcSvcs([...])`. The\n * caller-list override this change deletes (`callers: string[]`, where an EMPTY list meant \"any\n * Google-signed caller\") and a one-method override, which could not see the other scope of a pair,\n * must not compile.\n */\n oidcHookTakesTheMethods(): void {\n void class extends OidcHook {\n override async verifyOidc(_token: string, methods: OidcAuthMethods): Promise<AuthenticatedMachineIdentity> {\n const first = methods[0];\n const named = first.callerScope === 'services' ? first.callers[0] : 'same-project';\n return new AuthenticatedMachineIdentity('oidc', named);\n }\n };\n void class extends OidcHook {\n // @ts-expect-error verifyOidc takes the endpoint's OidcAuthMethods; the deleted caller-list override must not compile\n override async verifyOidc(_token: string, _callers: string[]): Promise<AuthenticatedMachineIdentity> {\n return new AuthenticatedMachineIdentity('oidc', 'caller@example.test');\n }\n };\n void class extends OidcHook {\n // @ts-expect-error verifyOidc takes every OIDC method of the endpoint, not one, so a pair is verified once\n override async verifyOidc(_token: string, _method: OidcAuthMethod): Promise<AuthenticatedMachineIdentity> {\n return new AuthenticatedMachineIdentity('oidc', 'caller@example.test');\n }\n };\n }\n\n /**\n * An {@link HttpRequest} whose `raw` is merely OPTIONAL is NOT a {@link RawHttpRequest}. This is\n * the assignment that makes the narrowing real: without it, `RawHttpRequest` would be a comment.\n */\n rawIsNotOptional(): void {\n const maybeRaw = new HttpRequest('POST', '/hook/sentry/issue', new Map<string, string[]>());\n // @ts-expect-error HttpRequest.raw is optional, so it is not assignable to RawHttpRequest\n const narrowed: RawHttpRequest = maybeRaw;\n void narrowed;\n\n // The other direction is free: a RawHttpRequest IS an HttpRequest everywhere one is wanted.\n const proven = new HttpRequest(\n 'POST',\n '/hook/sentry/issue',\n new Map<string, string[]>(),\n new RawRequest('https://example.com/hook', Buffer.from('{}', 'utf8'), '1.2.3.4'),\n ) as RawHttpRequest;\n const widened: HttpRequest = proven;\n void widened;\n }\n}\n"]}
|
package/src/RouteBuilderImpl.js
CHANGED
|
@@ -230,9 +230,12 @@ let RouteBuilderImpl = class RouteBuilderImpl {
|
|
|
230
230
|
const controllerService = {
|
|
231
231
|
invoke: async (meta) => {
|
|
232
232
|
const result = await routeWithMeta.invokeControllerHandler.execute(meta);
|
|
233
|
-
// A
|
|
234
|
-
//
|
|
235
|
-
//
|
|
233
|
+
// A `responseBody: NONE` endpoint (Promise<void>) has NO response: every transport
|
|
234
|
+
// sends 0 bytes (HTTP 204) and every client resolves undefined, so the in-process
|
|
235
|
+
// client must too. LogApiCall logs an undefined response as `response=undefined`.
|
|
236
|
+
if (routeMeta.noResponseBody)
|
|
237
|
+
return new WpResponse_1.WpResponse(undefined);
|
|
238
|
+
// Any other route that yields undefined/null still answers a JSON body: coerce to {}.
|
|
236
239
|
return new WpResponse_1.WpResponse(result ?? {});
|
|
237
240
|
},
|
|
238
241
|
};
|