@webpieces/http-routing 0.4.863 → 0.4.864
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/README.md +3 -1
- package/package.json +4 -4
- package/src/ApiClientFactory.js +1 -1
- package/src/ApiClientFactory.js.map +1 -1
- package/src/ApiFactory.d.ts +3 -0
- package/src/ApiFactory.js.map +1 -1
- package/src/ApiRoutingFactory.js +3 -3
- package/src/ApiRoutingFactory.js.map +1 -1
- package/src/AuthConfig.d.ts +15 -8
- package/src/AuthConfig.js +20 -8
- package/src/AuthConfig.js.map +1 -1
- package/src/AuthHooks.d.ts +13 -61
- package/src/AuthHooks.js +7 -44
- package/src/AuthHooks.js.map +1 -1
- package/src/AuthenticatedCallerContext.d.ts +6 -0
- package/src/AuthenticatedCallerContext.js +44 -0
- package/src/AuthenticatedCallerContext.js.map +1 -1
- package/src/AuthorizationHook.d.ts +34 -0
- package/src/AuthorizationHook.js +121 -0
- package/src/AuthorizationHook.js.map +1 -0
- package/src/AuthorizedApiDocument.d.ts +18 -0
- package/src/AuthorizedApiDocument.js +137 -0
- package/src/AuthorizedApiDocument.js.map +1 -0
- package/src/DefaultJwtHook.d.ts +4 -4
- package/src/DefaultJwtHook.js +10 -7
- package/src/DefaultJwtHook.js.map +1 -1
- package/src/DefaultOidcVerifier.d.ts +4 -3
- package/src/DefaultOidcVerifier.js +6 -4
- package/src/DefaultOidcVerifier.js.map +1 -1
- package/src/InvocationAuthentication.d.ts +7 -7
- package/src/InvocationAuthentication.js +18 -17
- package/src/InvocationAuthentication.js.map +1 -1
- package/src/JwtHookCompileAssertions.d.ts +1 -1
- package/src/JwtHookCompileAssertions.js +2 -5
- package/src/JwtHookCompileAssertions.js.map +1 -1
- package/src/RouteBuilderImpl.js +7 -0
- package/src/RouteBuilderImpl.js.map +1 -1
- package/src/WebpiecesRouter.d.ts +2 -0
- package/src/WebpiecesRouter.js +6 -0
- package/src/WebpiecesRouter.js.map +1 -1
- package/src/filters/AuthFilter.d.ts +11 -22
- package/src/filters/AuthFilter.js +75 -80
- package/src/filters/AuthFilter.js.map +1 -1
- package/src/filters/AuthorizationFilter.d.ts +10 -0
- package/src/filters/AuthorizationFilter.js +34 -0
- package/src/filters/AuthorizationFilter.js.map +1 -0
- package/src/index.d.ts +5 -2
- package/src/index.js +25 -10
- package/src/index.js.map +1 -1
- package/src/setupRuntime.d.ts +2 -2
- package/src/setupRuntime.js +2 -2
- package/src/setupRuntime.js.map +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npm install @webpieces/http-routing
|
|
|
12
12
|
|
|
13
13
|
## OIDC on private Cloud Run
|
|
14
14
|
|
|
15
|
-
`@
|
|
15
|
+
`@WpAuth([oidc()])` primarily selects client-side Google ID-token generation. With invocation
|
|
16
16
|
authentication and invoker IAM enabled, Cloud Run checks the invocation before it reaches
|
|
17
17
|
`AuthFilter`; the framework retains supplementary server validation. No callers means delegate
|
|
18
18
|
caller authorization to the edge; explicit callers add an application allow-list. Local tokens
|
|
@@ -27,3 +27,5 @@ See the main [WebPieces README](https://github.com/deanhiller/webpieces-ts#readm
|
|
|
27
27
|
## License
|
|
28
28
|
|
|
29
29
|
Apache-2.0
|
|
30
|
+
|
|
31
|
+
Authentication and operation authorization are separate declarations. See [the migration guide](../../../docs/authorization-migration.md) for policies, canonical context, token-free MCP invocation, and validator release ordering.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-routing",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.864",
|
|
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.864",
|
|
26
|
+
"@webpieces/core-util": "0.4.864",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.864",
|
|
28
28
|
"inversify": "7.10.4",
|
|
29
29
|
"jsonwebtoken": "9.0.2",
|
|
30
30
|
"minimatch": "10.0.1"
|
package/src/ApiClientFactory.js
CHANGED
|
@@ -78,7 +78,7 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
78
78
|
for (const entry of routesByApi.entries()) {
|
|
79
79
|
const api = entry[0];
|
|
80
80
|
const routes = entry[1];
|
|
81
|
-
// Build only the routes that were actually registered. A @
|
|
81
|
+
// Build only the routes that were actually registered. A @WpLocalOnly method is
|
|
82
82
|
// deliberately absent off-local and must not make mounting the rest of its API fail.
|
|
83
83
|
const client = this.buildProxy(api, routes);
|
|
84
84
|
clients.push(new ApiClient_1.ApiClient(api, client, routes));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiClientFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiClientFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import { InvocationAuthentication } from './InvocationAuthentication';\nimport { inject } from 'inversify';\nimport {\n ApiErrorCodec,\n DtoValue,\n getEndpoints,\n HttpContractMapper,\n RequestStream,\n ResponseStream,\n StreamDirection,\n RouteMetadata,\n RouteMetadataFactory,\n StreamEnvelope,\n StreamEventValidator,\n StreamTransportError,\n StreamWriter,\n} from '@webpieces/core-util';\nimport {\n provideFrameworkSingleton,\n RequestContext,\n StreamEventContext,\n} from '@webpieces/core-context';\nimport { MethodMeta } from './MethodMeta';\nimport { Service } from '@webpieces/core-util';\nimport { WpResponse } from './WpResponse';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiClient, ApiClientProxy } from './ApiClient';\nimport { ClassType } from './ApiRoutingFactory';\nimport { InProcessStreamExchange } from './InProcessStreamExchange';\n\n/**\n * Every call through the proxy needs an ambient RequestContext, established ABOVE the api boundary.\n * It is NOT auto-created here: manufacturing one silently would hide a missing top-level filter, and\n * every log line, outbound call, and enqueued task under it would quietly lose its request id.\n */\n// webpieces-disable no-function-outside-class -- a guard over ambient state; a class would own nothing\nfunction requireActiveContext(routeMeta: RouteMetadata): void {\n if (RequestContext.isActive()) {\n return;\n }\n throw new Error(\n `${routeMeta.controllerClassName}.${routeMeta.methodName} was called with no active RequestContext. ` +\n `A server transport must wrap each request in RequestContext.run(...) (WebpiecesMiddleware does). ` +\n `In a test, wrap the call yourself: await RequestContext.run(async () => api.foo(req));`,\n );\n}\n\n/**\n * ApiClientFactory - THE piece that wires api → Proxy → filters → controller.\n *\n * For an API prototype (its @ApiPath/@Endpoint decorators) it builds a proxy whose methods\n * invoke the composed filter chain (via RouteBuilder.createRouteInvoker) — that proxy IS what\n * createApiClient() returns. {@link apiClients} reuses the SAME proxy per registered api, so the\n * express layer binds each method through it. There is no express dependency here, so the proxy\n * is the single invocation path for BOTH in-process (tests) and HTTP.\n *\n * Establishing the request scope is a PRECONDITION of calling in here, never this class's job. The\n * caller above the api boundary opens `RequestContext.run(...)`, publishes the inbound\n * `HttpRequest`, and calls `RequestContextHeaders.fillFromRequest()` to move its headers into the\n * context. `WebpiecesMiddleware` does all three for you; a non-webpieces transport (or a test\n * driving `createApiClient` directly) must do the same. This proxy only CHECKS that it happened —\n * manufacturing a context here would hide a missing filter and silently strip every request id.\n *\n * @provideFrameworkSingleton so WebpiecesRouter can inject it (it shares the one RouteBuilder).\n */\n@provideFrameworkSingleton()\nexport class ApiClientFactory {\n constructor(@inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl) {}\n\n /**\n * Create an API client proxy (cast to the API interface T). The proxy's methods run the full\n * filter chain + controller; used by tests in-process AND driven by the express adapter.\n */\n // webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args\n createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T {\n return this.buildProxy(apiPrototype) as T;\n }\n\n /** In-process transport bridge; each proxy captures its own credential without ambient mutation. */\n // webpieces-disable no-any-unknown -- abstract API constructor signature\n createInvocationApiClient<T>(\n apiPrototype: abstract new (...args: any[]) => T,\n authentication: InvocationAuthentication,\n ): T {\n return this.buildProxy(apiPrototype, undefined, authentication) as T;\n }\n\n /**\n * Reify every registered API as an {@link ApiClient} — the contract + its proxy (the\n * createApiClient object). The transport reads the api's decorators to bind each endpoint to\n * the proxy's matching method, so no route metadata needs to leave here.\n */\n apiClients(): ApiClient[] {\n const routesByApi = new Map<ClassType, RouteMetadata[]>();\n for (const route of this.routeBuilder.getRoutes()) {\n const api = route.definition.apiClass as ClassType;\n const routes = routesByApi.get(api) ?? [];\n routes.push(route.definition.routeMeta);\n routesByApi.set(api, routes);\n }\n // apiClients() just loops createApiClient — the EXACT method tests call — so the platform\n // (HTTP) and tests (in-process) bind the identical proxy, 1-to-1.\n const clients: ApiClient[] = [];\n for (const entry of routesByApi.entries()) {\n const api = entry[0];\n const routes = entry[1];\n // Build only the routes that were actually registered. A @WpAuthLocalOnly method is\n // deliberately absent off-local and must not make mounting the rest of its API fail.\n const client = this.buildProxy(api, routes);\n clients.push(new ApiClient(api, client, routes));\n }\n return clients;\n }\n\n /** Build the proxy record (method name → invoker) from the API prototype's decorators. */\n private buildProxy(\n // webpieces-disable no-any-unknown -- accepts any ClassType / abstract-constructor API prototype\n apiPrototype: any,\n registeredRoutes?: readonly RouteMetadata[],\n authentication?: InvocationAuthentication,\n ): ApiClientProxy {\n const endpoints = getEndpoints(apiPrototype) || {};\n const proxy: ApiClientProxy = {};\n const methodNames = registeredRoutes\n ? registeredRoutes.map((route: RouteMetadata) => route.methodName)\n : Object.keys(endpoints);\n\n for (const methodName of methodNames) {\n const contractRoute =\n registeredRoutes?.find((route: RouteMetadata) => route.methodName === methodName) ??\n RouteMetadataFactory.create(apiPrototype, methodName);\n const httpMethod = contractRoute.httpMethod;\n const path = contractRoute.path;\n\n // Use the REGISTERED route's metadata — it carries the real controller name AND api\n // name (so logging/recording read the right one); createRouteInvoker composes its chain.\n const routeMeta = this.routeBuilder.getRouteMeta(httpMethod, path);\n if (!routeMeta) {\n throw new Error(\n `No registered route for ${apiPrototype.name}.${methodName} (${httpMethod} ${path}) — call addRoutes(api, controller) first.`,\n );\n }\n const service = this.routeBuilder.createRouteInvoker(httpMethod, path);\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n proxy[methodName] = async (...args: unknown[]): Promise<unknown> => {\n requireActiveContext(routeMeta);\n if (routeMeta.streaming) {\n return this.runStreamingMethod(routeMeta, args, service, authentication);\n }\n // Run the shared mapping even in-process. It validates the exact path/query/body\n // shape production clients use, then the controller receives the original typed args.\n const mapped = HttpContractMapper.toWire(\n routeMeta.path,\n routeMeta.parameterBindings,\n routeMeta.bodyParameterIndex,\n args,\n );\n return this.runMethod(\n routeMeta,\n mapped.body === undefined ? args : mapped.body,\n args,\n service,\n authentication,\n );\n };\n }\n\n return proxy;\n }\n\n /**\n * Join the two in-memory halves through the same envelopes used by wire transports. This is\n * intentionally not a controller shortcut: opening the stream still invokes `service`, so the\n * ordinary logging/auth/application filter chain runs before a writer is returned.\n */\n // webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary\n private async runStreamingMethod(\n routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary\n requestArgs: readonly unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses\n service: Service<MethodMeta, WpResponse<unknown>>,\n authentication?: InvocationAuthentication,\n // webpieces-disable no-any-unknown -- one of the three streaming contract return shapes\n ): Promise<unknown> {\n const streaming = routeMeta.streaming;\n if (!streaming) throw new Error('Streaming route metadata is required.');\n if (streaming.direction === StreamDirection.RESPONSE) {\n return this.runResponseStreamingMethod(routeMeta, requestArgs, service, authentication);\n }\n if (streaming.direction === StreamDirection.REQUEST) {\n return this.runRequestStreamingMethod(routeMeta, requestArgs, service, authentication);\n }\n if (streaming.initialRequestSchema) {\n new StreamEventValidator().validate(\n streaming.initialRequestSchema,\n requestArgs[0],\n 'request',\n );\n }\n const clientResponse = this.requireResponseStream(routeMeta, requestArgs[1]);\n const validator = new StreamEventValidator();\n const exchange = new InProcessStreamExchange(clientResponse, streaming.responseSchema!);\n const serverResponse = exchange.response;\n\n // A rejected invocation is the open/handshake failure. It deliberately escapes unchanged,\n // matching unary in-process calls and allowing an HTTP adapter to apply its normal mapper.\n const responseWrapper = await service.invoke(\n new MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse], authentication),\n );\n const serverRequest = this.requireRequestStream(routeMeta, responseWrapper.response);\n const initialResponse = serverRequest.getInitialResponse();\n const events = new StreamEventContext();\n if (streaming.initialResponseSchema) {\n validator.validate(streaming.initialResponseSchema, initialResponse, 'response');\n }\n exchange.accept(serverRequest);\n const clientRequest = new StreamWriter<DtoValue>(\n async (envelope: StreamEnvelope<DtoValue>): Promise<void> =>\n events.run(\n (): Promise<void> => this.deliverRequestEnvelope(serverRequest, envelope),\n ),\n (value: DtoValue): void =>\n validator.validate(streaming.requestSchema!, value, 'request'),\n initialResponse,\n (error?: Error): Promise<void> => exchange.cancel(error),\n );\n exchange.clientRequest = clientRequest;\n return clientRequest;\n }\n\n // webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary\n private async runRequestStreamingMethod(\n routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- stream initial DTO is erased at the routing boundary\n requestArgs: readonly unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses\n service: Service<MethodMeta, WpResponse<unknown>>,\n authentication?: InvocationAuthentication,\n ): Promise<RequestStream<DtoValue, DtoValue>> {\n const streaming = routeMeta.streaming!;\n const validator = new StreamEventValidator();\n if (streaming.initialRequestSchema) {\n validator.validate(streaming.initialRequestSchema, requestArgs[0], 'request');\n }\n const wrapper = await service.invoke(\n new MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0]], authentication),\n );\n const serverRequest = this.requireRequestStream(routeMeta, wrapper.response);\n const initialResponse = serverRequest.getInitialResponse();\n const events = new StreamEventContext();\n if (streaming.initialResponseSchema) {\n validator.validate(streaming.initialResponseSchema, initialResponse, 'response');\n }\n return new StreamWriter<DtoValue>(\n async (envelope: StreamEnvelope<DtoValue>): Promise<void> =>\n events.run(\n (): Promise<void> => this.deliverRequestEnvelope(serverRequest, envelope),\n ),\n (value: DtoValue): void =>\n validator.validate(streaming.requestSchema!, value, 'request'),\n initialResponse,\n (error?: Error): Promise<void> => serverRequest.cancel(error),\n );\n }\n\n // webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary\n private async runResponseStreamingMethod(\n routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary\n requestArgs: readonly unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses\n service: Service<MethodMeta, WpResponse<unknown>>,\n authentication?: InvocationAuthentication,\n // webpieces-disable no-any-unknown -- response initial DTO is contract-erased\n ): Promise<unknown> {\n if (requestArgs.length !== 2) {\n throw new StreamTransportError(\n `${routeMeta.apiName}.${routeMeta.methodName} RESPONSE streaming requires (InitialRequest, ResponseStream).`,\n );\n }\n const streaming = routeMeta.streaming!;\n const validator = new StreamEventValidator();\n if (streaming.initialRequestSchema) {\n validator.validate(streaming.initialRequestSchema, requestArgs[0], 'request');\n }\n const clientResponse = this.requireResponseStream(routeMeta, requestArgs[1]);\n const exchange = new InProcessStreamExchange(clientResponse, streaming.responseSchema!);\n const serverResponse = exchange.response;\n const wrapper = await service.invoke(\n new MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse], authentication),\n );\n if (streaming.initialResponseSchema) {\n validator.validate(streaming.initialResponseSchema, wrapper.response, 'response');\n }\n exchange.accept();\n return wrapper.response;\n }\n\n private async deliverRequestEnvelope(\n destination: RequestStream<DtoValue, DtoValue>,\n envelope: StreamEnvelope<DtoValue>,\n ): Promise<void> {\n switch (envelope.kind) {\n case 'event':\n if (envelope.value === undefined) {\n throw new StreamTransportError('Request event envelope has no value.');\n }\n return destination.event(envelope.value, envelope.correlation);\n case 'failure':\n if (!envelope.error) {\n throw new StreamTransportError('Request failure envelope has no error.');\n }\n return destination.cancel(ApiErrorCodec.decode(envelope.error));\n case 'complete':\n return destination.close();\n }\n }\n\n // webpieces-disable no-any-unknown -- runtime structural check narrows an erased API argument\n private requireResponseStream(\n routeMeta: RouteMetadata,\n candidate: unknown,\n ): ResponseStream<DtoValue> {\n if (\n typeof candidate === 'object' &&\n candidate !== null &&\n typeof Reflect.get(candidate, 'event') === 'function' &&\n typeof Reflect.get(candidate, 'close') === 'function' &&\n typeof Reflect.get(candidate, 'cancel') === 'function'\n ) {\n return candidate as ResponseStream<DtoValue>;\n }\n throw new StreamTransportError(\n `${routeMeta.apiName}.${routeMeta.methodName} requires a ResponseStream argument.`,\n );\n }\n\n // webpieces-disable no-any-unknown -- runtime structural check narrows an erased controller result\n private requireRequestStream(\n routeMeta: RouteMetadata,\n candidate: unknown,\n ): RequestStream<DtoValue, DtoValue> {\n if (\n typeof candidate === 'object' &&\n candidate !== null &&\n typeof Reflect.get(candidate, 'event') === 'function' &&\n typeof Reflect.get(candidate, 'getInitialResponse') === 'function' &&\n typeof Reflect.get(candidate, 'close') === 'function' &&\n typeof Reflect.get(candidate, 'cancel') === 'function'\n ) {\n return candidate as RequestStream<DtoValue, DtoValue>;\n }\n throw new StreamTransportError(\n `${routeMeta.controllerClassName}.${routeMeta.methodName} did not return a RequestStream.`,\n );\n }\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n private async runMethod(\n routeMeta: RouteMetadata,\n requestDto: unknown,\n requestArgs: unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract response DTOs\n service: Service<MethodMeta, WpResponse<unknown>>,\n authentication?: InvocationAuthentication,\n // webpieces-disable no-any-unknown -- API return type is erased at the routing proxy boundary\n ): Promise<unknown> {\n const responseWrapper = await service.invoke(\n new MethodMeta(routeMeta, requestDto, undefined, requestArgs, authentication),\n );\n return responseWrapper.response;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ApiClientFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiClientFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import { InvocationAuthentication } from './InvocationAuthentication';\nimport { inject } from 'inversify';\nimport {\n ApiErrorCodec,\n DtoValue,\n getEndpoints,\n HttpContractMapper,\n RequestStream,\n ResponseStream,\n StreamDirection,\n RouteMetadata,\n RouteMetadataFactory,\n StreamEnvelope,\n StreamEventValidator,\n StreamTransportError,\n StreamWriter,\n} from '@webpieces/core-util';\nimport {\n provideFrameworkSingleton,\n RequestContext,\n StreamEventContext,\n} from '@webpieces/core-context';\nimport { MethodMeta } from './MethodMeta';\nimport { Service } from '@webpieces/core-util';\nimport { WpResponse } from './WpResponse';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiClient, ApiClientProxy } from './ApiClient';\nimport { ClassType } from './ApiRoutingFactory';\nimport { InProcessStreamExchange } from './InProcessStreamExchange';\n\n/**\n * Every call through the proxy needs an ambient RequestContext, established ABOVE the api boundary.\n * It is NOT auto-created here: manufacturing one silently would hide a missing top-level filter, and\n * every log line, outbound call, and enqueued task under it would quietly lose its request id.\n */\n// webpieces-disable no-function-outside-class -- a guard over ambient state; a class would own nothing\nfunction requireActiveContext(routeMeta: RouteMetadata): void {\n if (RequestContext.isActive()) {\n return;\n }\n throw new Error(\n `${routeMeta.controllerClassName}.${routeMeta.methodName} was called with no active RequestContext. ` +\n `A server transport must wrap each request in RequestContext.run(...) (WebpiecesMiddleware does). ` +\n `In a test, wrap the call yourself: await RequestContext.run(async () => api.foo(req));`,\n );\n}\n\n/**\n * ApiClientFactory - THE piece that wires api → Proxy → filters → controller.\n *\n * For an API prototype (its @ApiPath/@Endpoint decorators) it builds a proxy whose methods\n * invoke the composed filter chain (via RouteBuilder.createRouteInvoker) — that proxy IS what\n * createApiClient() returns. {@link apiClients} reuses the SAME proxy per registered api, so the\n * express layer binds each method through it. There is no express dependency here, so the proxy\n * is the single invocation path for BOTH in-process (tests) and HTTP.\n *\n * Establishing the request scope is a PRECONDITION of calling in here, never this class's job. The\n * caller above the api boundary opens `RequestContext.run(...)`, publishes the inbound\n * `HttpRequest`, and calls `RequestContextHeaders.fillFromRequest()` to move its headers into the\n * context. `WebpiecesMiddleware` does all three for you; a non-webpieces transport (or a test\n * driving `createApiClient` directly) must do the same. This proxy only CHECKS that it happened —\n * manufacturing a context here would hide a missing filter and silently strip every request id.\n *\n * @provideFrameworkSingleton so WebpiecesRouter can inject it (it shares the one RouteBuilder).\n */\n@provideFrameworkSingleton()\nexport class ApiClientFactory {\n constructor(@inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl) {}\n\n /**\n * Create an API client proxy (cast to the API interface T). The proxy's methods run the full\n * filter chain + controller; used by tests in-process AND driven by the express adapter.\n */\n // webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args\n createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T {\n return this.buildProxy(apiPrototype) as T;\n }\n\n /** In-process transport bridge; each proxy captures its own credential without ambient mutation. */\n // webpieces-disable no-any-unknown -- abstract API constructor signature\n createInvocationApiClient<T>(\n apiPrototype: abstract new (...args: any[]) => T,\n authentication: InvocationAuthentication,\n ): T {\n return this.buildProxy(apiPrototype, undefined, authentication) as T;\n }\n\n /**\n * Reify every registered API as an {@link ApiClient} — the contract + its proxy (the\n * createApiClient object). The transport reads the api's decorators to bind each endpoint to\n * the proxy's matching method, so no route metadata needs to leave here.\n */\n apiClients(): ApiClient[] {\n const routesByApi = new Map<ClassType, RouteMetadata[]>();\n for (const route of this.routeBuilder.getRoutes()) {\n const api = route.definition.apiClass as ClassType;\n const routes = routesByApi.get(api) ?? [];\n routes.push(route.definition.routeMeta);\n routesByApi.set(api, routes);\n }\n // apiClients() just loops createApiClient — the EXACT method tests call — so the platform\n // (HTTP) and tests (in-process) bind the identical proxy, 1-to-1.\n const clients: ApiClient[] = [];\n for (const entry of routesByApi.entries()) {\n const api = entry[0];\n const routes = entry[1];\n // Build only the routes that were actually registered. A @WpLocalOnly method is\n // deliberately absent off-local and must not make mounting the rest of its API fail.\n const client = this.buildProxy(api, routes);\n clients.push(new ApiClient(api, client, routes));\n }\n return clients;\n }\n\n /** Build the proxy record (method name → invoker) from the API prototype's decorators. */\n private buildProxy(\n // webpieces-disable no-any-unknown -- accepts any ClassType / abstract-constructor API prototype\n apiPrototype: any,\n registeredRoutes?: readonly RouteMetadata[],\n authentication?: InvocationAuthentication,\n ): ApiClientProxy {\n const endpoints = getEndpoints(apiPrototype) || {};\n const proxy: ApiClientProxy = {};\n const methodNames = registeredRoutes\n ? registeredRoutes.map((route: RouteMetadata) => route.methodName)\n : Object.keys(endpoints);\n\n for (const methodName of methodNames) {\n const contractRoute =\n registeredRoutes?.find((route: RouteMetadata) => route.methodName === methodName) ??\n RouteMetadataFactory.create(apiPrototype, methodName);\n const httpMethod = contractRoute.httpMethod;\n const path = contractRoute.path;\n\n // Use the REGISTERED route's metadata — it carries the real controller name AND api\n // name (so logging/recording read the right one); createRouteInvoker composes its chain.\n const routeMeta = this.routeBuilder.getRouteMeta(httpMethod, path);\n if (!routeMeta) {\n throw new Error(\n `No registered route for ${apiPrototype.name}.${methodName} (${httpMethod} ${path}) — call addRoutes(api, controller) first.`,\n );\n }\n const service = this.routeBuilder.createRouteInvoker(httpMethod, path);\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n proxy[methodName] = async (...args: unknown[]): Promise<unknown> => {\n requireActiveContext(routeMeta);\n if (routeMeta.streaming) {\n return this.runStreamingMethod(routeMeta, args, service, authentication);\n }\n // Run the shared mapping even in-process. It validates the exact path/query/body\n // shape production clients use, then the controller receives the original typed args.\n const mapped = HttpContractMapper.toWire(\n routeMeta.path,\n routeMeta.parameterBindings,\n routeMeta.bodyParameterIndex,\n args,\n );\n return this.runMethod(\n routeMeta,\n mapped.body === undefined ? args : mapped.body,\n args,\n service,\n authentication,\n );\n };\n }\n\n return proxy;\n }\n\n /**\n * Join the two in-memory halves through the same envelopes used by wire transports. This is\n * intentionally not a controller shortcut: opening the stream still invokes `service`, so the\n * ordinary logging/auth/application filter chain runs before a writer is returned.\n */\n // webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary\n private async runStreamingMethod(\n routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary\n requestArgs: readonly unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses\n service: Service<MethodMeta, WpResponse<unknown>>,\n authentication?: InvocationAuthentication,\n // webpieces-disable no-any-unknown -- one of the three streaming contract return shapes\n ): Promise<unknown> {\n const streaming = routeMeta.streaming;\n if (!streaming) throw new Error('Streaming route metadata is required.');\n if (streaming.direction === StreamDirection.RESPONSE) {\n return this.runResponseStreamingMethod(routeMeta, requestArgs, service, authentication);\n }\n if (streaming.direction === StreamDirection.REQUEST) {\n return this.runRequestStreamingMethod(routeMeta, requestArgs, service, authentication);\n }\n if (streaming.initialRequestSchema) {\n new StreamEventValidator().validate(\n streaming.initialRequestSchema,\n requestArgs[0],\n 'request',\n );\n }\n const clientResponse = this.requireResponseStream(routeMeta, requestArgs[1]);\n const validator = new StreamEventValidator();\n const exchange = new InProcessStreamExchange(clientResponse, streaming.responseSchema!);\n const serverResponse = exchange.response;\n\n // A rejected invocation is the open/handshake failure. It deliberately escapes unchanged,\n // matching unary in-process calls and allowing an HTTP adapter to apply its normal mapper.\n const responseWrapper = await service.invoke(\n new MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse], authentication),\n );\n const serverRequest = this.requireRequestStream(routeMeta, responseWrapper.response);\n const initialResponse = serverRequest.getInitialResponse();\n const events = new StreamEventContext();\n if (streaming.initialResponseSchema) {\n validator.validate(streaming.initialResponseSchema, initialResponse, 'response');\n }\n exchange.accept(serverRequest);\n const clientRequest = new StreamWriter<DtoValue>(\n async (envelope: StreamEnvelope<DtoValue>): Promise<void> =>\n events.run(\n (): Promise<void> => this.deliverRequestEnvelope(serverRequest, envelope),\n ),\n (value: DtoValue): void =>\n validator.validate(streaming.requestSchema!, value, 'request'),\n initialResponse,\n (error?: Error): Promise<void> => exchange.cancel(error),\n );\n exchange.clientRequest = clientRequest;\n return clientRequest;\n }\n\n // webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary\n private async runRequestStreamingMethod(\n routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- stream initial DTO is erased at the routing boundary\n requestArgs: readonly unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses\n service: Service<MethodMeta, WpResponse<unknown>>,\n authentication?: InvocationAuthentication,\n ): Promise<RequestStream<DtoValue, DtoValue>> {\n const streaming = routeMeta.streaming!;\n const validator = new StreamEventValidator();\n if (streaming.initialRequestSchema) {\n validator.validate(streaming.initialRequestSchema, requestArgs[0], 'request');\n }\n const wrapper = await service.invoke(\n new MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0]], authentication),\n );\n const serverRequest = this.requireRequestStream(routeMeta, wrapper.response);\n const initialResponse = serverRequest.getInitialResponse();\n const events = new StreamEventContext();\n if (streaming.initialResponseSchema) {\n validator.validate(streaming.initialResponseSchema, initialResponse, 'response');\n }\n return new StreamWriter<DtoValue>(\n async (envelope: StreamEnvelope<DtoValue>): Promise<void> =>\n events.run(\n (): Promise<void> => this.deliverRequestEnvelope(serverRequest, envelope),\n ),\n (value: DtoValue): void =>\n validator.validate(streaming.requestSchema!, value, 'request'),\n initialResponse,\n (error?: Error): Promise<void> => serverRequest.cancel(error),\n );\n }\n\n // webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary\n private async runResponseStreamingMethod(\n routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary\n requestArgs: readonly unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses\n service: Service<MethodMeta, WpResponse<unknown>>,\n authentication?: InvocationAuthentication,\n // webpieces-disable no-any-unknown -- response initial DTO is contract-erased\n ): Promise<unknown> {\n if (requestArgs.length !== 2) {\n throw new StreamTransportError(\n `${routeMeta.apiName}.${routeMeta.methodName} RESPONSE streaming requires (InitialRequest, ResponseStream).`,\n );\n }\n const streaming = routeMeta.streaming!;\n const validator = new StreamEventValidator();\n if (streaming.initialRequestSchema) {\n validator.validate(streaming.initialRequestSchema, requestArgs[0], 'request');\n }\n const clientResponse = this.requireResponseStream(routeMeta, requestArgs[1]);\n const exchange = new InProcessStreamExchange(clientResponse, streaming.responseSchema!);\n const serverResponse = exchange.response;\n const wrapper = await service.invoke(\n new MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse], authentication),\n );\n if (streaming.initialResponseSchema) {\n validator.validate(streaming.initialResponseSchema, wrapper.response, 'response');\n }\n exchange.accept();\n return wrapper.response;\n }\n\n private async deliverRequestEnvelope(\n destination: RequestStream<DtoValue, DtoValue>,\n envelope: StreamEnvelope<DtoValue>,\n ): Promise<void> {\n switch (envelope.kind) {\n case 'event':\n if (envelope.value === undefined) {\n throw new StreamTransportError('Request event envelope has no value.');\n }\n return destination.event(envelope.value, envelope.correlation);\n case 'failure':\n if (!envelope.error) {\n throw new StreamTransportError('Request failure envelope has no error.');\n }\n return destination.cancel(ApiErrorCodec.decode(envelope.error));\n case 'complete':\n return destination.close();\n }\n }\n\n // webpieces-disable no-any-unknown -- runtime structural check narrows an erased API argument\n private requireResponseStream(\n routeMeta: RouteMetadata,\n candidate: unknown,\n ): ResponseStream<DtoValue> {\n if (\n typeof candidate === 'object' &&\n candidate !== null &&\n typeof Reflect.get(candidate, 'event') === 'function' &&\n typeof Reflect.get(candidate, 'close') === 'function' &&\n typeof Reflect.get(candidate, 'cancel') === 'function'\n ) {\n return candidate as ResponseStream<DtoValue>;\n }\n throw new StreamTransportError(\n `${routeMeta.apiName}.${routeMeta.methodName} requires a ResponseStream argument.`,\n );\n }\n\n // webpieces-disable no-any-unknown -- runtime structural check narrows an erased controller result\n private requireRequestStream(\n routeMeta: RouteMetadata,\n candidate: unknown,\n ): RequestStream<DtoValue, DtoValue> {\n if (\n typeof candidate === 'object' &&\n candidate !== null &&\n typeof Reflect.get(candidate, 'event') === 'function' &&\n typeof Reflect.get(candidate, 'getInitialResponse') === 'function' &&\n typeof Reflect.get(candidate, 'close') === 'function' &&\n typeof Reflect.get(candidate, 'cancel') === 'function'\n ) {\n return candidate as RequestStream<DtoValue, DtoValue>;\n }\n throw new StreamTransportError(\n `${routeMeta.controllerClassName}.${routeMeta.methodName} did not return a RequestStream.`,\n );\n }\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n private async runMethod(\n routeMeta: RouteMetadata,\n requestDto: unknown,\n requestArgs: unknown[],\n // webpieces-disable no-any-unknown -- filter service carries arbitrary contract response DTOs\n service: Service<MethodMeta, WpResponse<unknown>>,\n authentication?: InvocationAuthentication,\n // webpieces-disable no-any-unknown -- API return type is erased at the routing proxy boundary\n ): Promise<unknown> {\n const responseWrapper = await service.invoke(\n new MethodMeta(routeMeta, requestDto, undefined, requestArgs, authentication),\n );\n return responseWrapper.response;\n }\n}\n"]}
|
package/src/ApiFactory.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AuthorizationService } from './AuthorizationHook';
|
|
1
2
|
import { InvocationAuthentication } from './InvocationAuthentication';
|
|
2
3
|
import { ApiClient } from './ApiClient';
|
|
3
4
|
/**
|
|
@@ -16,6 +17,8 @@ import { ApiClient } from './ApiClient';
|
|
|
16
17
|
* addRoutes/addFilter). Hand an ApiFactory to WebpiecesExpressRouter to serve it over HTTP.
|
|
17
18
|
*/
|
|
18
19
|
export interface ApiFactory {
|
|
20
|
+
/** The receiving chain policy service, shared by every local projection and dispatch. */
|
|
21
|
+
authorizationService(): AuthorizationService;
|
|
19
22
|
apiClients(): ApiClient[];
|
|
20
23
|
/** Internal transports supply an immutable credential to the ordinary route chain. */
|
|
21
24
|
createInvocationApiClient<T>(apiPrototype: abstract new (...args: any[]) => T, authentication: InvocationAuthentication): T;
|
package/src/ApiFactory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiFactory.ts"],"names":[],"mappings":"","sourcesContent":["import { InvocationAuthentication } from './InvocationAuthentication';\nimport { ApiClient } from './ApiClient';\n\n/**\n * ApiFactory - the node-only, EXPRESS-FREE CONSUMER surface of a built app. It is exactly the\n * two things something downstream of the router needs, and nothing else (no RouteBuilder, no\n * container):\n *\n * - {@link apiClients} — the PLATFORM path: each registered api as an {@link ApiClient}\n * (api + its client proxy). The express layer (WebpiecesExpressRouter) binds these.\n * - {@link createApiClient} — the TEST path: an in-process proxy for one api (no HTTP, no ports).\n *\n * apiClients() is literally a loop over createApiClient, so the two are 1-to-1: what the platform\n * serves over HTTP is the exact same proxy a test drives in-process.\n *\n * Implemented by {@link WebpiecesRouter} (the node-only heart, which also owns the BUILD surface\n * addRoutes/addFilter). Hand an ApiFactory to WebpiecesExpressRouter to serve it over HTTP.\n */\nexport interface ApiFactory {\n apiClients(): ApiClient[];\n\n /** Internal transports supply an immutable credential to the ordinary route chain. */\n // webpieces-disable no-any-unknown -- abstract API constructor signature\n createInvocationApiClient<T>(\n apiPrototype: abstract new (...args: any[]) => T,\n authentication: InvocationAuthentication,\n ): T;\n\n // webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args\n createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ApiFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiFactory.ts"],"names":[],"mappings":"","sourcesContent":["import { AuthorizationService } from './AuthorizationHook';\nimport { InvocationAuthentication } from './InvocationAuthentication';\nimport { ApiClient } from './ApiClient';\n\n/**\n * ApiFactory - the node-only, EXPRESS-FREE CONSUMER surface of a built app. It is exactly the\n * two things something downstream of the router needs, and nothing else (no RouteBuilder, no\n * container):\n *\n * - {@link apiClients} — the PLATFORM path: each registered api as an {@link ApiClient}\n * (api + its client proxy). The express layer (WebpiecesExpressRouter) binds these.\n * - {@link createApiClient} — the TEST path: an in-process proxy for one api (no HTTP, no ports).\n *\n * apiClients() is literally a loop over createApiClient, so the two are 1-to-1: what the platform\n * serves over HTTP is the exact same proxy a test drives in-process.\n *\n * Implemented by {@link WebpiecesRouter} (the node-only heart, which also owns the BUILD surface\n * addRoutes/addFilter). Hand an ApiFactory to WebpiecesExpressRouter to serve it over HTTP.\n */\nexport interface ApiFactory {\n /** The receiving chain policy service, shared by every local projection and dispatch. */\n authorizationService(): AuthorizationService;\n\n apiClients(): ApiClient[];\n\n /** Internal transports supply an immutable credential to the ordinary route chain. */\n // webpieces-disable no-any-unknown -- abstract API constructor signature\n createInvocationApiClient<T>(\n apiPrototype: abstract new (...args: any[]) => T,\n authentication: InvocationAuthentication,\n ): T;\n\n // webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args\n createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;\n}\n"]}
|
package/src/ApiRoutingFactory.js
CHANGED
|
@@ -79,13 +79,13 @@ class ApiRoutingFactory {
|
|
|
79
79
|
throw new Error(`Endpoint '${methodName}' in ${apiName} has no auth decorator. ` +
|
|
80
80
|
core_util_1.MISSING_AUTH_DECORATOR_FIX);
|
|
81
81
|
}
|
|
82
|
-
// @
|
|
82
|
+
// @WpLocalOnly: off-local the route is never registered, so the endpoint does not
|
|
83
83
|
// exist rather than existing-and-refusing. This is the PRIMARY gate; AuthFilter's 404 is
|
|
84
84
|
// the backstop for routes added by hand through RouteBuilder. One decorator drives both
|
|
85
85
|
// — the point of moving this into the framework was that apps were hand-syncing exactly
|
|
86
86
|
// these two halves across two files with a comment.
|
|
87
|
-
if (
|
|
88
|
-
log.info(`Skipping @
|
|
87
|
+
if ((0, core_util_1.isLocalOnly)(this.apiMetaClass, methodName) && !core_util_1.RuntimeLocality.isLocalDevelopment()) {
|
|
88
|
+
log.info(`Skipping @WpLocalOnly endpoint ${apiName}.${methodName} — this process is not ` +
|
|
89
89
|
`a local developer machine, so the route is not registered at all.`);
|
|
90
90
|
continue;
|
|
91
91
|
}
|
|
@@ -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} 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 // @WpAuthLocalOnly: off-local the route is never registered, so the endpoint does not\n // exist rather than existing-and-refusing. This is the PRIMARY gate; AuthFilter's 404 is\n // the backstop for routes added by hand through RouteBuilder. One decorator drives both\n // — the point of moving this into the framework was that apps were hand-syncing exactly\n // these two halves across two files with a comment.\n if (authMeta.mode.kind === 'local-only' && !RuntimeLocality.isLocalDevelopment()) {\n log.info(\n `Skipping @WpAuthLocalOnly endpoint ${apiName}.${methodName} — this process is not ` +\n `a local developer machine, so the route is not registered at all.`,\n );\n continue;\n }\n\n const 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 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"]}
|
package/src/AuthConfig.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ContextKey, ContextTuple } from '@webpieces/core-util';
|
|
2
2
|
/**
|
|
3
|
-
* SharedSecrets - the accepted values for ONE
|
|
3
|
+
* SharedSecrets - the accepted values for ONE `sharedSecret(name)`. BOTH secret1 AND secret2
|
|
4
4
|
* are accepted — this is what makes zero-downtime ROTATION possible:
|
|
5
5
|
*
|
|
6
6
|
* to rotate: shift secret2 → secret1, and put the NEW secret in secret2. Callers cut over from
|
|
@@ -21,8 +21,8 @@ export declare class SharedSecrets {
|
|
|
21
21
|
*
|
|
22
22
|
* - `userId` — WHO the caller is, as the credential proved it.
|
|
23
23
|
* - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check
|
|
24
|
-
* reads, `claims`
|
|
25
|
-
*
|
|
24
|
+
* reads, `claims` carries credential facts for the authenticator to validate and publish
|
|
25
|
+
* as trusted application context before AuthorizationHook evaluates policy.
|
|
26
26
|
* - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with
|
|
27
27
|
* {@link RequestContext.putTrusted}, so return only what THIS authenticator
|
|
28
28
|
* derived from the credential it just verified.
|
|
@@ -34,11 +34,18 @@ export declare class SharedSecrets {
|
|
|
34
34
|
* Data-only structure (a class, per the guidelines).
|
|
35
35
|
*/
|
|
36
36
|
export declare class AuthenticatedCaller {
|
|
37
|
-
readonly userId: string;
|
|
37
|
+
readonly userId: string | undefined;
|
|
38
38
|
readonly roles: string[];
|
|
39
39
|
readonly entries: ContextTuple[];
|
|
40
40
|
readonly claims: Record<string, unknown>;
|
|
41
|
-
|
|
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);
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
44
51
|
* The context slot holding the {@link AuthenticatedCaller} the framework {@link AuthFilter} resolved
|
|
@@ -59,11 +66,11 @@ export declare class AuthenticatedCaller {
|
|
|
59
66
|
export declare const AUTHENTICATED_CALLER_KEY: ContextKey<AuthenticatedCaller, "trusted">;
|
|
60
67
|
/**
|
|
61
68
|
* AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to
|
|
62
|
-
* enforce
|
|
69
|
+
* enforce `sharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —
|
|
63
70
|
* there is no verification code here. The verification MECHANISMS are separate optional hooks the
|
|
64
71
|
* app binds when it needs them:
|
|
65
72
|
*
|
|
66
|
-
* - user JWT → bind a {@link JwtHook} (async parseJwt
|
|
73
|
+
* - user JWT → bind a {@link JwtHook} (async parseJwt).
|
|
67
74
|
* - api key → bind an {@link ApiKeyHook} (async verifyApiKey over the request's headers).
|
|
68
75
|
* - OIDC → bind an {@link OidcHook} to override the framework's default verifier; a server that
|
|
69
76
|
* binds nothing still verifies Google OIDC via the built-in {@link DefaultOidcVerifier}.
|
|
@@ -73,7 +80,7 @@ export declare const AUTHENTICATED_CALLER_KEY: ContextKey<AuthenticatedCaller, "
|
|
|
73
80
|
* when unbound, shared-secret endpoints simply have no accepted secret and fail fast (401).
|
|
74
81
|
*/
|
|
75
82
|
export declare class AuthConfig {
|
|
76
|
-
/** Accepted shared-secret values keyed by
|
|
83
|
+
/** Accepted shared-secret values keyed by `sharedSecret(name)`. DEFAULT empty — pass to enable. */
|
|
77
84
|
readonly sharedSecrets: Record<string, SharedSecrets>;
|
|
78
85
|
constructor(sharedSecrets?: Record<string, SharedSecrets>);
|
|
79
86
|
}
|
package/src/AuthConfig.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AUTH_CONFIG = exports.AuthConfig = exports.AUTHENTICATED_CALLER_KEY = exports.AuthenticatedCaller = exports.SharedSecrets = void 0;
|
|
3
|
+
exports.AUTH_CONFIG = exports.AuthConfig = exports.AUTHENTICATED_CALLER_KEY = exports.AuthenticatedMachineIdentity = exports.AuthenticatedCaller = exports.SharedSecrets = void 0;
|
|
4
4
|
const core_util_1 = require("@webpieces/core-util");
|
|
5
5
|
/**
|
|
6
|
-
* SharedSecrets - the accepted values for ONE
|
|
6
|
+
* SharedSecrets - the accepted values for ONE `sharedSecret(name)`. BOTH secret1 AND secret2
|
|
7
7
|
* are accepted — this is what makes zero-downtime ROTATION possible:
|
|
8
8
|
*
|
|
9
9
|
* to rotate: shift secret2 → secret1, and put the NEW secret in secret2. Callers cut over from
|
|
@@ -28,8 +28,8 @@ exports.SharedSecrets = SharedSecrets;
|
|
|
28
28
|
*
|
|
29
29
|
* - `userId` — WHO the caller is, as the credential proved it.
|
|
30
30
|
* - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check
|
|
31
|
-
* reads, `claims`
|
|
32
|
-
*
|
|
31
|
+
* reads, `claims` carries credential facts for the authenticator to validate and publish
|
|
32
|
+
* as trusted application context before AuthorizationHook evaluates policy.
|
|
33
33
|
* - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with
|
|
34
34
|
* {@link RequestContext.putTrusted}, so return only what THIS authenticator
|
|
35
35
|
* derived from the credential it just verified.
|
|
@@ -45,16 +45,28 @@ class AuthenticatedCaller {
|
|
|
45
45
|
roles;
|
|
46
46
|
entries;
|
|
47
47
|
claims;
|
|
48
|
+
machine;
|
|
48
49
|
constructor(userId, roles = [], entries = [],
|
|
49
50
|
// webpieces-disable no-any-unknown -- raw JWT claims for app-defined authorization (inOrg, tenant, ...)
|
|
50
|
-
claims = {}) {
|
|
51
|
+
claims = {}, machine) {
|
|
51
52
|
this.userId = userId;
|
|
52
53
|
this.roles = roles;
|
|
53
54
|
this.entries = entries;
|
|
54
55
|
this.claims = claims;
|
|
56
|
+
this.machine = machine;
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
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;
|
|
58
70
|
/**
|
|
59
71
|
* The context slot holding the {@link AuthenticatedCaller} the framework {@link AuthFilter} resolved
|
|
60
72
|
* for this request. A real TRUSTED {@link ContextKey}, written with `RequestContext.putTrusted` and
|
|
@@ -77,11 +89,11 @@ exports.AUTHENTICATED_CALLER_KEY = core_util_1.ContextKey.trusted('authenticated
|
|
|
77
89
|
/*isLogged*/ false);
|
|
78
90
|
/**
|
|
79
91
|
* AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to
|
|
80
|
-
* enforce
|
|
92
|
+
* enforce `sharedSecret(name)` endpoints. It holds ONLY the accepted secret values (STATE) —
|
|
81
93
|
* there is no verification code here. The verification MECHANISMS are separate optional hooks the
|
|
82
94
|
* app binds when it needs them:
|
|
83
95
|
*
|
|
84
|
-
* - user JWT → bind a {@link JwtHook} (async parseJwt
|
|
96
|
+
* - user JWT → bind a {@link JwtHook} (async parseJwt).
|
|
85
97
|
* - api key → bind an {@link ApiKeyHook} (async verifyApiKey over the request's headers).
|
|
86
98
|
* - OIDC → bind an {@link OidcHook} to override the framework's default verifier; a server that
|
|
87
99
|
* binds nothing still verifies Google OIDC via the built-in {@link DefaultOidcVerifier}.
|
|
@@ -91,7 +103,7 @@ exports.AUTHENTICATED_CALLER_KEY = core_util_1.ContextKey.trusted('authenticated
|
|
|
91
103
|
* when unbound, shared-secret endpoints simply have no accepted secret and fail fast (401).
|
|
92
104
|
*/
|
|
93
105
|
class AuthConfig {
|
|
94
|
-
/** Accepted shared-secret values keyed by
|
|
106
|
+
/** Accepted shared-secret values keyed by `sharedSecret(name)`. DEFAULT empty — pass to enable. */
|
|
95
107
|
sharedSecrets;
|
|
96
108
|
constructor(sharedSecrets = {}) {
|
|
97
109
|
this.sharedSecrets = sharedSecrets;
|
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,oDAAgE;AAEhE;;;;;;;;;GASG;AACH,MAAa,aAAa;IAEF;IACA;IAFpB,YACoB,OAAe,EACf,OAAe;QADf,YAAO,GAAP,OAAO,CAAQ;QACf,YAAO,GAAP,OAAO,CAAQ;IAChC,CAAC;CACP;AALD,sCAKC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,mBAAmB;IAER;IACA;IACA;IAEA;
|
|
1
|
+
{"version":3,"file":"AuthConfig.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthConfig.ts"],"names":[],"mappings":";;;AAAA,oDAAgE;AAEhE;;;;;;;;;GASG;AACH,MAAa,aAAa;IAEF;IACA;IAFpB,YACoB,OAAe,EACf,OAAe;QADf,YAAO,GAAP,OAAO,CAAQ;QACf,YAAO,GAAP,OAAO,CAAQ;IAChC,CAAC;CACP;AALD,sCAKC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,mBAAmB;IAER;IACA;IACA;IAEA;IACA;IANpB,YACoB,MAA0B,EAC1B,QAAkB,EAAE,EACpB,UAA0B,EAAE;IAC5C,wGAAwG;IACxF,SAAkC,EAAE,EACpC,OAAsC;QALtC,WAAM,GAAN,MAAM,CAAoB;QAC1B,UAAK,GAAL,KAAK,CAAe;QACpB,YAAO,GAAP,OAAO,CAAqB;QAE5B,WAAM,GAAN,MAAM,CAA8B;QACpC,YAAO,GAAP,OAAO,CAA+B;IACvD,CAAC;CACP;AATD,kDASC;AAED,mFAAmF;AACnF,MAAa,4BAA4B;IACT;IAAmC;IAA/D,YAA4B,SAAiB,EAAkB,QAAgB;QAAnD,cAAS,GAAT,SAAS,CAAQ;QAAkB,aAAQ,GAAR,QAAQ,CAAQ;IAAG,CAAC;CACtF;AAFD,oEAEC;AAED;;;;;;;;;;;;;;;GAeG;AACU,QAAA,wBAAwB,GAAG,sBAAU,CAAC,OAAO,CACtD,qBAAqB,EACrB,yHAAyH;AACzH,cAAc,CAAC,SAAS;AACxB,cAAc,CAAC,KAAK;AACpB,YAAY,CAAC,KAAK,CACrB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAa,UAAU;IACnB,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, ContextTuple } from '@webpieces/core-util';\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 * AuthenticatedCaller - what an authenticator PROVED about the caller of ONE request. Every hook\n * that authenticates resolves to this: {@link JwtHook.parseJwt}, {@link ApiKeyHook.verifyApiKey} and\n * {@link WebhookAuthCallback.verifyWebhook}. Four fields, three jobs:\n *\n * - `userId` — WHO the caller is, as the credential proved it.\n * - `roles` / `claims` — the AUTHORIZATION inputs: `roles` is what the framework's own any-of check\n * reads, `claims` carries credential facts for the authenticator to validate and publish\n * as trusted application context before AuthorizationHook evaluates policy.\n * - `entries` — the TRUSTED CONTEXT to seed. The framework writes each one with\n * {@link RequestContext.putTrusted}, so return only what THIS authenticator\n * derived from the credential it just verified.\n *\n * NAMING, said out loud so it is not \"fixed\" back: it is deliberately NOT a `TrustedContextMap`.\n * Three of the four fields are not context, and it is not a Map — it is the authenticated caller,\n * and the context is one thing it carries.\n *\n * Data-only structure (a class, per the guidelines).\n */\nexport class AuthenticatedCaller {\n constructor(\n public readonly userId: string | undefined,\n public readonly roles: string[] = [],\n public readonly entries: ContextTuple[] = [],\n // webpieces-disable no-any-unknown -- raw JWT claims for app-defined authorization (inOrg, tenant, ...)\n public readonly claims: Record<string, unknown> = {},\n public readonly machine?: AuthenticatedMachineIdentity,\n ) {}\n}\n\n/** Verified external machine identity does not grant trusted-header delegation. */\nexport class AuthenticatedMachineIdentity {\n constructor(public readonly mechanism: string, public readonly identity: string) {}\n}\n\n/**\n * The context slot holding the {@link AuthenticatedCaller} the framework {@link AuthFilter} resolved\n * for this request. A real TRUSTED {@link ContextKey}, written with `RequestContext.putTrusted` and\n * read with `RequestContext.getTrusted` — it replaces a raw `'__webpieces_principal__'` string that\n * was the one place in the codebase bypassing the typed context layer.\n *\n * `httpHeader` is deliberately UNDEFINED, so the key is context-only and NEVER travels. Two reasons,\n * and either alone is decisive: the value is an OBJECT, which no HTTP header can carry; and a\n * principal is proof THIS hop's authenticator produced, so forwarding it would hand the next service\n * a \"proven\" caller nothing on that hop verified. The individual facts a downstream service needs\n * (userId, orgId, roles) already propagate as their own transferred keys in\n * {@link WebpiecesCoreHeaders}, gated by the caller-verified rule.\n *\n * `isLogged` is FALSE: it is an object carrying the raw credential claims, which has no business\n * being serialized into a log line.\n */\nexport const AUTHENTICATED_CALLER_KEY = ContextKey.trusted<AuthenticatedCaller>(\n 'authenticatedCaller',\n 'resolved by the framework AuthFilter from a credential an app-bound JwtHook, ApiKeyHook or WebhookAuthCallback verified',\n /*httpHeader*/ undefined,\n /*maskInLogs*/ false,\n /*isLogged*/ false,\n);\n\n/**\n * AuthConfig - the app-provided SHARED-SECRET state the framework {@link AuthFilter} reads to\n * enforce `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,63 +1,15 @@
|
|
|
1
|
-
import { JwtRequirement } from '@webpieces/core-util';
|
|
2
1
|
import { HttpRequest, RawHttpRequest } from '@webpieces/core-context';
|
|
3
|
-
import { AuthenticatedCaller } from './AuthConfig';
|
|
2
|
+
import { AuthenticatedCaller, AuthenticatedMachineIdentity } from './AuthConfig';
|
|
4
3
|
/** Framework-normalized result from an application-owned JWT mint operation. */
|
|
5
4
|
export declare class MintedJwt {
|
|
6
5
|
readonly token: string;
|
|
7
6
|
readonly expiresAtEpochSeconds: number;
|
|
8
7
|
constructor(token: string, expiresAtEpochSeconds: number);
|
|
9
8
|
}
|
|
10
|
-
/**
|
|
11
|
-
* JwtHook - the OPTIONAL user-JWT mechanism. Its DI token is the {@link JWT_HOOK} Symbol injected via
|
|
12
|
-
* `@inject(JWT_HOOK)` (a Symbol, because the app container uses autobind; rebindable in tests). Bind one
|
|
13
|
-
* to turn on `@WpAuthJwt({...})` endpoints. When NO JwtHook is bound, the framework
|
|
14
|
-
* {@link AuthFilter} treats every jwt endpoint as "not enabled" and fails fast (401) — there is no
|
|
15
|
-
* default JWT verification because it needs an app secret + payload shape the framework can't guess.
|
|
16
|
-
*
|
|
17
|
-
* - `mint` — ISSUANCE: construct/sign a JWT from an application-owned request shape.
|
|
18
|
-
* - `parseJwt` — AUTHENTICATION: decode/verify a user JWT into {@link AuthenticatedCaller}, or throw. The
|
|
19
|
-
* app owns the strategy (HS256 secret, RS256 + JWKS, a provider SDK, ...).
|
|
20
|
-
* - `authorizeJwt` — AUTHORIZATION: check the authenticated user against the endpoint's
|
|
21
|
-
* {@link JwtRequirement}. The DEFAULT enforces the roles any-of; override for
|
|
22
|
-
* app-defined requirements carried by the SAME decorator, e.g.
|
|
23
|
-
* `@WpAuthJwt({allRolesAllowed: true, inOrg: true})` →
|
|
24
|
-
* `if (requirement['inOrg'] && !values.claims['orgId']) ...`.
|
|
25
|
-
*
|
|
26
|
-
* ALL THREE ARE ASYNC for the same reason: the strategy is the app's, and an app's strategy reaches
|
|
27
|
-
* the network. `mint` may call KMS/HSM, `parseJwt` may fetch a JWKS or call a provider SDK, and `authorizeJwt`'s
|
|
28
|
-
* motivating example — `@WpAuthJwt({allRolesAllowed: true, inOrg: true})` — is a membership question a
|
|
29
|
-
* real app answers from a datastore. A sync signature makes both of those unwritable, and it made
|
|
30
|
-
* `JwtHook` the last sync hook: {@link OidcHook.verifyOidc}, {@link WebhookAuthCallback.verifyWebhook} and
|
|
31
|
-
* {@link ApiKeyHook.verifyApiKey} all return promises. An implementation that needs no I/O simply has
|
|
32
|
-
* no `await` in its body — {@link DefaultJwtHook} is exactly that and pays nothing for it.
|
|
33
|
-
*/
|
|
9
|
+
/** Optional application JWT credential authority. Authorization lives in AuthorizationHook. */
|
|
34
10
|
export declare abstract class JwtHook<TMintRequest> {
|
|
35
|
-
/**
|
|
36
|
-
* Mint an application endpoint JWT. The application owns the request/claim shape while the
|
|
37
|
-
* framework constrains the returned token and expiry metadata. Always async so the authority
|
|
38
|
-
* may use a remote signer, KMS, or HSM.
|
|
39
|
-
*/
|
|
40
11
|
abstract mint(request: TMintRequest): Promise<MintedJwt>;
|
|
41
|
-
/**
|
|
42
|
-
* Parse a user JWT (kind:'jwt') — AUTHENTICATION only. Return who the user is, or throw.
|
|
43
|
-
* ASYNC so an app can reach a JWKS endpoint or a provider SDK; see the class doc.
|
|
44
|
-
*
|
|
45
|
-
* IT TAKES THE TOKEN, NOT THE REQUEST — the one deliberate asymmetry among the four hooks, and
|
|
46
|
-
* NOT an oversight to be "fixed". {@link ApiKeyHook.verifyApiKey} and
|
|
47
|
-
* {@link WebhookAuthCallback.verifyWebhook} take the whole {@link HttpRequest} because their
|
|
48
|
-
* credential regime is the APP's: which headers carry an api key, and how a vendor signs, are
|
|
49
|
-
* things the framework cannot know. A user JWT is different — the framework owns the
|
|
50
|
-
* `Authorization: Bearer` scheme and has already extracted the token from it. Widening this to
|
|
51
|
-
* the request would only invite a JwtHook to authenticate off some OTHER header, which is a
|
|
52
|
-
* second, ungoverned credential path on the mode that guards browser traffic.
|
|
53
|
-
*/
|
|
54
12
|
abstract parseJwt(token: string): Promise<AuthenticatedCaller>;
|
|
55
|
-
/**
|
|
56
|
-
* DEFAULT authorization: enforce the endpoint's roles (any-of). Override to enforce app-defined
|
|
57
|
-
* requirements. Throw ApiForbiddenError to deny; return to allow. ASYNC so an app-defined
|
|
58
|
-
* requirement can be answered from a datastore; see the class doc.
|
|
59
|
-
*/
|
|
60
|
-
authorizeJwt(caller: AuthenticatedCaller, requirement: JwtRequirement): Promise<void>;
|
|
61
13
|
}
|
|
62
14
|
/**
|
|
63
15
|
* DI identifier for the optional {@link JwtHook} binding. It is a Symbol (not the class) so the app
|
|
@@ -71,12 +23,12 @@ export declare const JWT_HOOK: unique symbol;
|
|
|
71
23
|
* autobind; rebindable in tests). Bind one ONLY to customize the caller policy — e.g. an app that reads an
|
|
72
24
|
* `ALLOWED_OIDC_CALLERS` env var at its composition root and enforces that allow-list. When NO
|
|
73
25
|
* OidcHook is bound, the framework {@link AuthFilter} runs the built-in {@link DefaultOidcVerifier}
|
|
74
|
-
* directly, so a server that wires nothing still verifies Google OIDC against its
|
|
26
|
+
* directly, so a server that wires nothing still verifies Google OIDC against its `oidc(...callers)`
|
|
75
27
|
* (else trusts the edge — any Google-signed caller). `verifyOidc` verifies the token against `callers`;
|
|
76
28
|
* throw on failure.
|
|
77
29
|
*/
|
|
78
30
|
export declare abstract class OidcHook {
|
|
79
|
-
abstract verifyOidc(token: string, callers: string[]): Promise<
|
|
31
|
+
abstract verifyOidc(token: string, callers: string[]): Promise<AuthenticatedMachineIdentity>;
|
|
80
32
|
}
|
|
81
33
|
/**
|
|
82
34
|
* DI identifier for the optional {@link OidcHook} binding. It is a Symbol (not the class) so the app
|
|
@@ -85,7 +37,7 @@ export declare abstract class OidcHook {
|
|
|
85
37
|
*/
|
|
86
38
|
export declare const OIDC_HOOK: unique symbol;
|
|
87
39
|
/**
|
|
88
|
-
* WebhookAuthCallback - the OPTIONAL mechanism behind
|
|
40
|
+
* WebhookAuthCallback - the OPTIONAL mechanism behind `webhook(name)`: prove that an inbound request was
|
|
89
41
|
* really authored by the outside vendor the contract names. Its DI token is the {@link WEBHOOK_AUTH_CALLBACK}
|
|
90
42
|
* Symbol injected via `@inject(WEBHOOK_AUTH_CALLBACK)` (a Symbol, because the app container uses autobind;
|
|
91
43
|
* rebindable in tests). The third hook, symmetric with {@link JwtHook} / {@link OidcHook}:
|
|
@@ -95,10 +47,10 @@ export declare const OIDC_HOOK: unique symbol;
|
|
|
95
47
|
* options.bind(WEBHOOK_AUTH_CALLBACK).to(CompanyWebhookAuthCallback);
|
|
96
48
|
* ```
|
|
97
49
|
*
|
|
98
|
-
* When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every
|
|
50
|
+
* When NO WebhookAuthCallback is bound, the framework {@link AuthFilter} 401s every `webhook(...)` endpoint,
|
|
99
51
|
* exactly as it does for an unbound JwtHook. There is no framework default and there never will be
|
|
100
52
|
* one: silently allowing an unverified webhook is the single default that must not exist, and the
|
|
101
|
-
* framework ships no vendor crypto by design (see {@link
|
|
53
|
+
* framework ships no vendor crypto by design (see {@link webhook} for why reimplementing five
|
|
102
54
|
* vendors' schemes is a losing trade).
|
|
103
55
|
*
|
|
104
56
|
* ONE hook serves EVERY vendor: `name` selects which, so an app with a Sentry hook and a Twilio hook
|
|
@@ -121,10 +73,10 @@ export declare abstract class WebhookAuthCallback {
|
|
|
121
73
|
* caller-NOT-verified (see `AuthFilter.verifiesCaller`): a vendor is not a peer service, so
|
|
122
74
|
* nothing the vendor merely ASSERTED on the wire is admitted.
|
|
123
75
|
*
|
|
124
|
-
* @param name the string on the contract's
|
|
76
|
+
* @param name the string on the contract's `webhook(name)` — which vendor this route is.
|
|
125
77
|
* @param request the transport-neutral request, narrowed to {@link RawHttpRequest}: `request.raw`
|
|
126
78
|
* holds the verbatim bytes + absolute url and is PRESENT, never optional.
|
|
127
|
-
*
|
|
79
|
+
* `webhook(...)` requires `@Endpoint(..., { rawBody: true })` at wiring time, and
|
|
128
80
|
* AuthFilter 401s rather than calling this hook with nothing to check — so an
|
|
129
81
|
* implementation never writes `raw!` or a guard of its own.
|
|
130
82
|
*/
|
|
@@ -137,7 +89,7 @@ export declare abstract class WebhookAuthCallback {
|
|
|
137
89
|
*/
|
|
138
90
|
export declare const WEBHOOK_AUTH_CALLBACK: unique symbol;
|
|
139
91
|
/**
|
|
140
|
-
* ApiKeyHook - the OPTIONAL mechanism behind
|
|
92
|
+
* ApiKeyHook - the OPTIONAL mechanism behind `apiKey(regime, credentials)`: authenticate a
|
|
141
93
|
* CUSTOMER-held api key
|
|
142
94
|
* against the app's own datastore and return the context to seed. Its DI token is the
|
|
143
95
|
* {@link API_KEY_HOOK} Symbol injected via `@inject(API_KEY_HOOK)` (a Symbol, because the app container
|
|
@@ -149,7 +101,7 @@ export declare const WEBHOOK_AUTH_CALLBACK: unique symbol;
|
|
|
149
101
|
* options.bind(API_KEY_HOOK).to(OneTabletApiKeyHook);
|
|
150
102
|
* ```
|
|
151
103
|
*
|
|
152
|
-
* When NO ApiKeyHook is bound, the framework {@link AuthFilter} 401s every
|
|
104
|
+
* When NO ApiKeyHook is bound, the framework {@link AuthFilter} 401s every `apiKey(...)` endpoint,
|
|
153
105
|
* exactly as it does for an unbound JwtHook. There is no framework default and there never will be
|
|
154
106
|
* one: the key regime lives in the app's datastore, under the app's hashing scheme, behind the app's
|
|
155
107
|
* choice of header names.
|
|
@@ -175,10 +127,10 @@ export declare abstract class ApiKeyHook {
|
|
|
175
127
|
*
|
|
176
128
|
* NOTE the seeded entries are TRUSTED context keys, so return only what THIS hook proved from the
|
|
177
129
|
* credential it just verified. Anything the caller merely asserted on the wire is not admitted by
|
|
178
|
-
*
|
|
130
|
+
* `apiKey(...)` — the mode is deliberately caller-NOT-verified (see `AuthFilter.verifiesCaller`),
|
|
179
131
|
* because a customer is not an internal service.
|
|
180
132
|
*
|
|
181
|
-
* @param regime the first argument of the contract's
|
|
133
|
+
* @param regime the first argument of the contract's `apiKey(regime, credentials)` — which key
|
|
182
134
|
* regime this route belongs to.
|
|
183
135
|
* @param request the inbound request; read as many headers as the regime needs with
|
|
184
136
|
* `getHeader` / `getHeaderValues`, either by raw name or by {@link ContextKey}.
|