@webpieces/http-routing 0.4.840 → 0.4.841
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/ApiClientFactory.d.ts +3 -0
- package/src/ApiClientFactory.js +18 -13
- package/src/ApiClientFactory.js.map +1 -1
- package/src/ApiFactory.d.ts +3 -0
- package/src/ApiFactory.js.map +1 -1
- package/src/AuthenticatedCallerContext.d.ts +23 -0
- package/src/AuthenticatedCallerContext.js +62 -0
- package/src/AuthenticatedCallerContext.js.map +1 -0
- package/src/InvocationAuthentication.d.ts +9 -0
- package/src/InvocationAuthentication.js +28 -0
- package/src/InvocationAuthentication.js.map +1 -0
- package/src/MethodMeta.d.ts +3 -1
- package/src/MethodMeta.js +3 -1
- package/src/MethodMeta.js.map +1 -1
- package/src/WebpiecesRouter.d.ts +3 -0
- package/src/WebpiecesRouter.js +8 -1
- package/src/WebpiecesRouter.js.map +1 -1
- package/src/filters/AuthFilter.d.ts +1 -41
- package/src/filters/AuthFilter.js +15 -81
- package/src/filters/AuthFilter.js.map +1 -1
- package/src/index.d.ts +2 -0
- package/src/index.js +5 -1
- 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.841",
|
|
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.841",
|
|
26
|
+
"@webpieces/core-util": "0.4.841",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.841",
|
|
28
28
|
"inversify": "7.10.4",
|
|
29
29
|
"jsonwebtoken": "9.0.2",
|
|
30
30
|
"minimatch": "10.0.1"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { InvocationAuthentication } from './InvocationAuthentication';
|
|
1
2
|
import { RouteBuilderImpl } from './RouteBuilderImpl';
|
|
2
3
|
import { ApiClient } from './ApiClient';
|
|
3
4
|
/**
|
|
@@ -26,6 +27,8 @@ export declare class ApiClientFactory {
|
|
|
26
27
|
* filter chain + controller; used by tests in-process AND driven by the express adapter.
|
|
27
28
|
*/
|
|
28
29
|
createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;
|
|
30
|
+
/** In-process transport bridge; each proxy captures its own credential without ambient mutation. */
|
|
31
|
+
createInvocationApiClient<T>(apiPrototype: abstract new (...args: any[]) => T, authentication: InvocationAuthentication): T;
|
|
29
32
|
/**
|
|
30
33
|
* Reify every registered API as an {@link ApiClient} — the contract + its proxy (the
|
|
31
34
|
* createApiClient object). The transport reads the api's decorators to bind each endpoint to
|
package/src/ApiClientFactory.js
CHANGED
|
@@ -54,6 +54,11 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
54
54
|
createApiClient(apiPrototype) {
|
|
55
55
|
return this.buildProxy(apiPrototype);
|
|
56
56
|
}
|
|
57
|
+
/** In-process transport bridge; each proxy captures its own credential without ambient mutation. */
|
|
58
|
+
// webpieces-disable no-any-unknown -- abstract API constructor signature
|
|
59
|
+
createInvocationApiClient(apiPrototype, authentication) {
|
|
60
|
+
return this.buildProxy(apiPrototype, undefined, authentication);
|
|
61
|
+
}
|
|
57
62
|
/**
|
|
58
63
|
* Reify every registered API as an {@link ApiClient} — the contract + its proxy (the
|
|
59
64
|
* createApiClient object). The transport reads the api's decorators to bind each endpoint to
|
|
@@ -83,7 +88,7 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
83
88
|
/** Build the proxy record (method name → invoker) from the API prototype's decorators. */
|
|
84
89
|
buildProxy(
|
|
85
90
|
// webpieces-disable no-any-unknown -- accepts any ClassType / abstract-constructor API prototype
|
|
86
|
-
apiPrototype, registeredRoutes) {
|
|
91
|
+
apiPrototype, registeredRoutes, authentication) {
|
|
87
92
|
const endpoints = (0, core_util_1.getEndpoints)(apiPrototype) || {};
|
|
88
93
|
const proxy = {};
|
|
89
94
|
const methodNames = registeredRoutes
|
|
@@ -105,12 +110,12 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
105
110
|
proxy[methodName] = async (...args) => {
|
|
106
111
|
requireActiveContext(routeMeta);
|
|
107
112
|
if (routeMeta.streaming) {
|
|
108
|
-
return this.runStreamingMethod(routeMeta, args, service);
|
|
113
|
+
return this.runStreamingMethod(routeMeta, args, service, authentication);
|
|
109
114
|
}
|
|
110
115
|
// Run the shared mapping even in-process. It validates the exact path/query/body
|
|
111
116
|
// shape production clients use, then the controller receives the original typed args.
|
|
112
117
|
const mapped = core_util_1.HttpContractMapper.toWire(routeMeta.path, routeMeta.parameterBindings, routeMeta.bodyParameterIndex, args);
|
|
113
|
-
return this.runMethod(routeMeta, mapped.body === undefined ? args : mapped.body, args, service);
|
|
118
|
+
return this.runMethod(routeMeta, mapped.body === undefined ? args : mapped.body, args, service, authentication);
|
|
114
119
|
};
|
|
115
120
|
}
|
|
116
121
|
return proxy;
|
|
@@ -125,15 +130,15 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
125
130
|
// webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary
|
|
126
131
|
requestArgs,
|
|
127
132
|
// webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses
|
|
128
|
-
service) {
|
|
133
|
+
service, authentication) {
|
|
129
134
|
const streaming = routeMeta.streaming;
|
|
130
135
|
if (!streaming)
|
|
131
136
|
throw new Error('Streaming route metadata is required.');
|
|
132
137
|
if (streaming.direction === core_util_1.StreamDirection.RESPONSE) {
|
|
133
|
-
return this.runResponseStreamingMethod(routeMeta, requestArgs, service);
|
|
138
|
+
return this.runResponseStreamingMethod(routeMeta, requestArgs, service, authentication);
|
|
134
139
|
}
|
|
135
140
|
if (streaming.direction === core_util_1.StreamDirection.REQUEST) {
|
|
136
|
-
return this.runRequestStreamingMethod(routeMeta, requestArgs, service);
|
|
141
|
+
return this.runRequestStreamingMethod(routeMeta, requestArgs, service, authentication);
|
|
137
142
|
}
|
|
138
143
|
if (streaming.initialRequestSchema) {
|
|
139
144
|
new core_util_1.StreamEventValidator().validate(streaming.initialRequestSchema, requestArgs[0], 'request');
|
|
@@ -144,7 +149,7 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
144
149
|
const serverResponse = exchange.response;
|
|
145
150
|
// A rejected invocation is the open/handshake failure. It deliberately escapes unchanged,
|
|
146
151
|
// matching unary in-process calls and allowing an HTTP adapter to apply its normal mapper.
|
|
147
|
-
const responseWrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse]));
|
|
152
|
+
const responseWrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse], authentication));
|
|
148
153
|
const serverRequest = this.requireRequestStream(routeMeta, responseWrapper.response);
|
|
149
154
|
const initialResponse = serverRequest.getInitialResponse();
|
|
150
155
|
const events = new core_context_1.StreamEventContext();
|
|
@@ -161,13 +166,13 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
161
166
|
// webpieces-disable no-any-unknown -- stream initial DTO is erased at the routing boundary
|
|
162
167
|
requestArgs,
|
|
163
168
|
// webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses
|
|
164
|
-
service) {
|
|
169
|
+
service, authentication) {
|
|
165
170
|
const streaming = routeMeta.streaming;
|
|
166
171
|
const validator = new core_util_1.StreamEventValidator();
|
|
167
172
|
if (streaming.initialRequestSchema) {
|
|
168
173
|
validator.validate(streaming.initialRequestSchema, requestArgs[0], 'request');
|
|
169
174
|
}
|
|
170
|
-
const wrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0]]));
|
|
175
|
+
const wrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0]], authentication));
|
|
171
176
|
const serverRequest = this.requireRequestStream(routeMeta, wrapper.response);
|
|
172
177
|
const initialResponse = serverRequest.getInitialResponse();
|
|
173
178
|
const events = new core_context_1.StreamEventContext();
|
|
@@ -181,7 +186,7 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
181
186
|
// webpieces-disable no-any-unknown -- stream event DTOs are erased at the routing boundary
|
|
182
187
|
requestArgs,
|
|
183
188
|
// webpieces-disable no-any-unknown -- filter service carries arbitrary contract responses
|
|
184
|
-
service) {
|
|
189
|
+
service, authentication) {
|
|
185
190
|
if (requestArgs.length !== 2) {
|
|
186
191
|
throw new core_util_1.StreamTransportError(`${routeMeta.apiName}.${routeMeta.methodName} RESPONSE streaming requires (InitialRequest, ResponseStream).`);
|
|
187
192
|
}
|
|
@@ -193,7 +198,7 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
193
198
|
const clientResponse = this.requireResponseStream(routeMeta, requestArgs[1]);
|
|
194
199
|
const exchange = new InProcessStreamExchange_1.InProcessStreamExchange(clientResponse, streaming.responseSchema);
|
|
195
200
|
const serverResponse = exchange.response;
|
|
196
|
-
const wrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse]));
|
|
201
|
+
const wrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestArgs[0], undefined, [requestArgs[0], serverResponse], authentication));
|
|
197
202
|
if (streaming.initialResponseSchema) {
|
|
198
203
|
validator.validate(streaming.initialResponseSchema, wrapper.response, 'response');
|
|
199
204
|
}
|
|
@@ -242,8 +247,8 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
242
247
|
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
243
248
|
async runMethod(routeMeta, requestDto, requestArgs,
|
|
244
249
|
// webpieces-disable no-any-unknown -- filter service carries arbitrary contract response DTOs
|
|
245
|
-
service) {
|
|
246
|
-
const responseWrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestDto, undefined, requestArgs));
|
|
250
|
+
service, authentication) {
|
|
251
|
+
const responseWrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestDto, undefined, requestArgs, authentication));
|
|
247
252
|
return responseWrapper.response;
|
|
248
253
|
}
|
|
249
254
|
};
|
|
@@ -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 { 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 /**\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 ): 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);\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 );\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 // 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);\n }\n if (streaming.direction === StreamDirection.REQUEST) {\n return this.runRequestStreamingMethod(routeMeta, requestArgs, service);\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]),\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 ): 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]]),\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 // 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]),\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 // 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),\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 @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"]}
|
package/src/ApiFactory.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { InvocationAuthentication } from './InvocationAuthentication';
|
|
1
2
|
import { ApiClient } from './ApiClient';
|
|
2
3
|
/**
|
|
3
4
|
* ApiFactory - the node-only, EXPRESS-FREE CONSUMER surface of a built app. It is exactly the
|
|
@@ -16,5 +17,7 @@ import { ApiClient } from './ApiClient';
|
|
|
16
17
|
*/
|
|
17
18
|
export interface ApiFactory {
|
|
18
19
|
apiClients(): ApiClient[];
|
|
20
|
+
/** Internal transports supply an immutable credential to the ordinary route chain. */
|
|
21
|
+
createInvocationApiClient<T>(apiPrototype: abstract new (...args: any[]) => T, authentication: InvocationAuthentication): T;
|
|
19
22
|
createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;
|
|
20
23
|
}
|
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 { 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 // 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 { 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"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AuthenticatedCaller } from './AuthConfig';
|
|
2
|
+
/** Shared trusted principal publication and inbound header reconciliation for HTTP and MCP. */
|
|
3
|
+
export declare class AuthenticatedCallerContext {
|
|
4
|
+
publish(caller: AuthenticatedCaller): void;
|
|
5
|
+
reconcileWireTrust(callerVerified: boolean): void;
|
|
6
|
+
/**
|
|
7
|
+
* On a browser-reachable route, an inbound trusted header must match what the authenticator
|
|
8
|
+
* itself derived, or the request dies. Both failure shapes are rejections, not repairs:
|
|
9
|
+
*
|
|
10
|
+
* - DIFFERENT value — the caller said `alice`, the credential says `bob`. Silently letting the
|
|
11
|
+
* credential win is not safe, because upstream rate limiters commonly bucket on the header
|
|
12
|
+
* rather than the token: the request was already counted against the wrong principal, so
|
|
13
|
+
* every forged header would be a free rate-limit bypass. No honest caller contradicts its own
|
|
14
|
+
* credential.
|
|
15
|
+
* - NOTHING vouched for it — nobody derived this key at all, so there is no evidence behind a
|
|
16
|
+
* value a stranger typed. This is the common case, not the exotic one: the framework's
|
|
17
|
+
* {@link DefaultJwtHook} stamps NO entries, and an app hook (jwt or api-key) only stamps the keys it can prove,
|
|
18
|
+
* so any other trusted key a caller sends lands here.
|
|
19
|
+
*
|
|
20
|
+
* The pending value is discarded either way — the throw is what leaves the request.
|
|
21
|
+
*/
|
|
22
|
+
private requireVouched;
|
|
23
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthenticatedCallerContext = void 0;
|
|
4
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
5
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
6
|
+
const AuthConfig_1 = require("./AuthConfig");
|
|
7
|
+
const log = core_util_1.LogManager.getLogger('AuthenticatedCallerContext');
|
|
8
|
+
/** Shared trusted principal publication and inbound header reconciliation for HTTP and MCP. */
|
|
9
|
+
class AuthenticatedCallerContext {
|
|
10
|
+
publish(caller) {
|
|
11
|
+
for (const entry of caller.entries) {
|
|
12
|
+
// ContextTuple.key is a TRUSTED key by type, so this is the one sanctioned write of a
|
|
13
|
+
// proven identity: the app's hook derived it from a credential we just verified.
|
|
14
|
+
core_context_1.RequestContext.putTrusted(entry.key, entry.value);
|
|
15
|
+
}
|
|
16
|
+
// A real TRUSTED ContextKey, not a raw string slot: the caller IS the framework's own proof,
|
|
17
|
+
// so it is written with the same typed verb every other proven value goes through.
|
|
18
|
+
core_context_1.RequestContext.putTrusted(AuthConfig_1.AUTHENTICATED_CALLER_KEY, caller);
|
|
19
|
+
}
|
|
20
|
+
reconcileWireTrust(callerVerified) {
|
|
21
|
+
const pending = core_context_1.PendingWireTrust.takeAll();
|
|
22
|
+
for (const item of pending) {
|
|
23
|
+
if (callerVerified) {
|
|
24
|
+
core_context_1.RequestContext.putTrusted(item.key, item.value);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
this.requireVouched(item);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* On a browser-reachable route, an inbound trusted header must match what the authenticator
|
|
33
|
+
* itself derived, or the request dies. Both failure shapes are rejections, not repairs:
|
|
34
|
+
*
|
|
35
|
+
* - DIFFERENT value — the caller said `alice`, the credential says `bob`. Silently letting the
|
|
36
|
+
* credential win is not safe, because upstream rate limiters commonly bucket on the header
|
|
37
|
+
* rather than the token: the request was already counted against the wrong principal, so
|
|
38
|
+
* every forged header would be a free rate-limit bypass. No honest caller contradicts its own
|
|
39
|
+
* credential.
|
|
40
|
+
* - NOTHING vouched for it — nobody derived this key at all, so there is no evidence behind a
|
|
41
|
+
* value a stranger typed. This is the common case, not the exotic one: the framework's
|
|
42
|
+
* {@link DefaultJwtHook} stamps NO entries, and an app hook (jwt or api-key) only stamps the keys it can prove,
|
|
43
|
+
* so any other trusted key a caller sends lands here.
|
|
44
|
+
*
|
|
45
|
+
* The pending value is discarded either way — the throw is what leaves the request.
|
|
46
|
+
*/
|
|
47
|
+
requireVouched(item) {
|
|
48
|
+
const vouched = core_context_1.RequestContext.getTrusted(item.key);
|
|
49
|
+
if (vouched === item.value) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
log.error(`Rejecting inbound '${item.key.httpHeader}': it is a TRUSTED context key, this route does ` +
|
|
53
|
+
`not authenticate its caller, and the credential ` +
|
|
54
|
+
(vouched === undefined
|
|
55
|
+
? 'vouched for no such value'
|
|
56
|
+
: 'derived a different value') +
|
|
57
|
+
'.');
|
|
58
|
+
throw new core_util_1.ApiUnauthorizedError(`Header '${item.key.httpHeader}' cannot be supplied by the caller on this endpoint`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.AuthenticatedCallerContext = AuthenticatedCallerContext;
|
|
62
|
+
//# sourceMappingURL=AuthenticatedCallerContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthenticatedCallerContext.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/AuthenticatedCallerContext.ts"],"names":[],"mappings":";;;AAAA,0DAAgG;AAChG,oDAAwE;AACxE,6CAA6E;AAE7E,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,4BAA4B,CAAC,CAAC;AAE/D,+FAA+F;AAC/F,MAAa,0BAA0B;IACnC,OAAO,CAAC,MAA2B;QAC/B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACjC,sFAAsF;YACtF,iFAAiF;YACjF,6BAAc,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QACtD,CAAC;QACD,6FAA6F;QAC7F,mFAAmF;QACnF,6BAAc,CAAC,UAAU,CAAC,qCAAwB,EAAE,MAAM,CAAC,CAAC;IAChE,CAAC;IAED,kBAAkB,CAAC,cAAuB;QACtC,MAAM,OAAO,GAAG,+BAAgB,CAAC,OAAO,EAAE,CAAC;QAC3C,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YACzB,IAAI,cAAc,EAAE,CAAC;gBACjB,6BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YACpD,CAAC;iBAAM,CAAC;gBACJ,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;YAC9B,CAAC;QACL,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACK,cAAc,CAAC,IAAyB;QAC5C,MAAM,OAAO,GAAG,6BAAc,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpD,IAAI,OAAO,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;YACzB,OAAO;QACX,CAAC;QACD,GAAG,CAAC,KAAK,CACL,sBAAsB,IAAI,CAAC,GAAG,CAAC,UAAU,kDAAkD;YACvF,kDAAkD;YAClD,CAAC,OAAO,KAAK,SAAS;gBAClB,CAAC,CAAC,2BAA2B;gBAC7B,CAAC,CAAC,2BAA2B,CAAC;YAClC,GAAG,CACV,CAAC;QACF,MAAM,IAAI,gCAAoB,CAC1B,WAAW,IAAI,CAAC,GAAG,CAAC,UAAU,qDAAqD,CACtF,CAAC;IACN,CAAC;CACJ;AAxDD,gEAwDC","sourcesContent":["import { PendingWireTrust, PendingTrustedValue, RequestContext } from '@webpieces/core-context';\nimport { ApiUnauthorizedError, LogManager } from '@webpieces/core-util';\nimport { AuthenticatedCaller, AUTHENTICATED_CALLER_KEY } from './AuthConfig';\n\nconst log = LogManager.getLogger('AuthenticatedCallerContext');\n\n/** Shared trusted principal publication and inbound header reconciliation for HTTP and MCP. */\nexport class AuthenticatedCallerContext {\n publish(caller: AuthenticatedCaller): void {\n for (const entry of caller.entries) {\n // ContextTuple.key is a TRUSTED key by type, so this is the one sanctioned write of a\n // proven identity: the app's hook derived it from a credential we just verified.\n RequestContext.putTrusted(entry.key, entry.value);\n }\n // A real TRUSTED ContextKey, not a raw string slot: the caller IS the framework's own proof,\n // so it is written with the same typed verb every other proven value goes through.\n RequestContext.putTrusted(AUTHENTICATED_CALLER_KEY, caller);\n }\n\n reconcileWireTrust(callerVerified: boolean): void {\n const pending = PendingWireTrust.takeAll();\n for (const item of pending) {\n if (callerVerified) {\n RequestContext.putTrusted(item.key, item.value);\n } else {\n this.requireVouched(item);\n }\n }\n }\n\n /**\n * On a browser-reachable route, an inbound trusted header must match what the authenticator\n * itself derived, or the request dies. Both failure shapes are rejections, not repairs:\n *\n * - DIFFERENT value — the caller said `alice`, the credential says `bob`. Silently letting the\n * credential win is not safe, because upstream rate limiters commonly bucket on the header\n * rather than the token: the request was already counted against the wrong principal, so\n * every forged header would be a free rate-limit bypass. No honest caller contradicts its own\n * credential.\n * - NOTHING vouched for it — nobody derived this key at all, so there is no evidence behind a\n * value a stranger typed. This is the common case, not the exotic one: the framework's\n * {@link DefaultJwtHook} stamps NO entries, and an app hook (jwt or api-key) only stamps the keys it can prove,\n * so any other trusted key a caller sends lands here.\n *\n * The pending value is discarded either way — the throw is what leaves the request.\n */\n private requireVouched(item: PendingTrustedValue): void {\n const vouched = RequestContext.getTrusted(item.key);\n if (vouched === item.value) {\n return;\n }\n log.error(\n `Rejecting inbound '${item.key.httpHeader}': it is a TRUSTED context key, this route does ` +\n `not authenticate its caller, and the credential ` +\n (vouched === undefined\n ? 'vouched for no such value'\n : 'derived a different value') +\n '.',\n );\n throw new ApiUnauthorizedError(\n `Header '${item.key.httpHeader}' cannot be supplied by the caller on this endpoint`,\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AuthenticatedCaller } from './AuthConfig';
|
|
2
|
+
/** In-process invocation credential. Never read from the wire or installed in ambient request state. */
|
|
3
|
+
export declare class InvocationAuthentication {
|
|
4
|
+
readonly token: string;
|
|
5
|
+
private readonly principal;
|
|
6
|
+
constructor(token: string, principal: AuthenticatedCaller);
|
|
7
|
+
/** Endpoint JWT verification must reproduce the ingress identity before endpoint policy runs. */
|
|
8
|
+
assertCaller(caller: AuthenticatedCaller): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InvocationAuthentication = void 0;
|
|
4
|
+
const node_util_1 = require("node:util");
|
|
5
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
6
|
+
/** In-process invocation credential. Never read from the wire or installed in ambient request state. */
|
|
7
|
+
class InvocationAuthentication {
|
|
8
|
+
token;
|
|
9
|
+
principal;
|
|
10
|
+
constructor(token, principal) {
|
|
11
|
+
this.token = token;
|
|
12
|
+
this.principal = principal;
|
|
13
|
+
}
|
|
14
|
+
/** Endpoint JWT verification must reproduce the ingress identity before endpoint policy runs. */
|
|
15
|
+
assertCaller(caller) {
|
|
16
|
+
const expected = this.principal;
|
|
17
|
+
const sameRoles = caller.roles.length === expected.roles.length &&
|
|
18
|
+
caller.roles.every((role) => expected.roles.includes(role));
|
|
19
|
+
const sameEntries = caller.entries.length === expected.entries.length &&
|
|
20
|
+
caller.entries.every((entry) => expected.entries.some((item) => item.key === entry.key && (0, node_util_1.isDeepStrictEqual)(item.value, entry.value)));
|
|
21
|
+
const sameClaims = Object.keys(expected.claims).every((key) => (0, node_util_1.isDeepStrictEqual)(expected.claims[key], caller.claims[key]));
|
|
22
|
+
if (caller.userId !== expected.userId || !sameRoles || !sameEntries || !sameClaims) {
|
|
23
|
+
throw new core_util_1.ApiImplementationError('Invocation JWT disagrees with the verified ingress principal.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.InvocationAuthentication = InvocationAuthentication;
|
|
28
|
+
//# sourceMappingURL=InvocationAuthentication.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InvocationAuthentication.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/InvocationAuthentication.ts"],"names":[],"mappings":";;;AAAA,yCAA8C;AAC9C,oDAA8D;AAG9D,wGAAwG;AACxG,MAAa,wBAAwB;IAEb;IACC;IAFrB,YACoB,KAAa,EACZ,SAA8B;QAD/B,UAAK,GAAL,KAAK,CAAQ;QACZ,cAAS,GAAT,SAAS,CAAqB;IAChD,CAAC;IAEJ,iGAAiG;IACjG,YAAY,CAAC,MAA2B;QACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,MAAM,SAAS,GACX,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,KAAK,CAAC,MAAM;YAC7C,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QACxE,MAAM,WAAW,GACb,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,OAAO,CAAC,MAAM;YACjD,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAC3B,QAAQ,CAAC,OAAO,CAAC,IAAI,CACjB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,KAAK,KAAK,CAAC,GAAG,IAAI,IAAA,6BAAiB,EAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CACjF,CACJ,CAAC;QACN,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,GAAW,EAAE,EAAE,CAClE,IAAA,6BAAiB,EAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAC9D,CAAC;QACF,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW,IAAI,CAAC,UAAU,EAAE,CAAC;YACjF,MAAM,IAAI,kCAAsB,CAC5B,+DAA+D,CAClE,CAAC;QACN,CAAC;IACL,CAAC;CACJ;AA5BD,4DA4BC","sourcesContent":["import { isDeepStrictEqual } from 'node:util';\nimport { ApiImplementationError } from '@webpieces/core-util';\nimport { AuthenticatedCaller } from './AuthConfig';\n\n/** In-process invocation credential. Never read from the wire or installed in ambient request state. */\nexport class InvocationAuthentication {\n constructor(\n public readonly token: string,\n private readonly principal: AuthenticatedCaller,\n ) {}\n\n /** Endpoint JWT verification must reproduce the ingress identity before endpoint policy runs. */\n assertCaller(caller: AuthenticatedCaller): void {\n const expected = this.principal;\n const sameRoles =\n caller.roles.length === expected.roles.length &&\n caller.roles.every((role: string) => expected.roles.includes(role));\n const sameEntries =\n caller.entries.length === expected.entries.length &&\n caller.entries.every((entry) =>\n expected.entries.some(\n (item) => item.key === entry.key && isDeepStrictEqual(item.value, entry.value),\n ),\n );\n const sameClaims = Object.keys(expected.claims).every((key: string) =>\n isDeepStrictEqual(expected.claims[key], caller.claims[key]),\n );\n if (caller.userId !== expected.userId || !sameRoles || !sameEntries || !sameClaims) {\n throw new ApiImplementationError(\n 'Invocation JWT disagrees with the verified ingress principal.',\n );\n }\n }\n}\n"]}
|
package/src/MethodMeta.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { InvocationAuthentication } from './InvocationAuthentication';
|
|
1
2
|
import { RouteMetadata } from '@webpieces/core-util';
|
|
2
3
|
/**
|
|
3
4
|
* Metadata about the method being invoked.
|
|
@@ -24,6 +25,7 @@ import { RouteMetadata } from '@webpieces/core-util';
|
|
|
24
25
|
export declare class MethodMeta {
|
|
25
26
|
/** API method arguments in declaration order; defaults to the historical one-body call. */
|
|
26
27
|
readonly requestArgs: readonly unknown[];
|
|
28
|
+
readonly invocationAuthentication?: InvocationAuthentication | undefined;
|
|
27
29
|
/**
|
|
28
30
|
* Route metadata (httpMethod, path, methodName, parameterTypes)
|
|
29
31
|
*/
|
|
@@ -39,7 +41,7 @@ export declare class MethodMeta {
|
|
|
39
41
|
metadata: Map<string, unknown>;
|
|
40
42
|
constructor(routeMeta: RouteMetadata, requestDto?: unknown, metadata?: Map<string, unknown>,
|
|
41
43
|
/** API method arguments in declaration order; defaults to the historical one-body call. */
|
|
42
|
-
requestArgs?: readonly unknown[]);
|
|
44
|
+
requestArgs?: readonly unknown[], invocationAuthentication?: InvocationAuthentication | undefined);
|
|
43
45
|
/**
|
|
44
46
|
* Get the HTTP method (convenience accessor).
|
|
45
47
|
*/
|
package/src/MethodMeta.js
CHANGED
|
@@ -25,6 +25,7 @@ exports.MethodMeta = void 0;
|
|
|
25
25
|
*/
|
|
26
26
|
class MethodMeta {
|
|
27
27
|
requestArgs;
|
|
28
|
+
invocationAuthentication;
|
|
28
29
|
/**
|
|
29
30
|
* Route metadata (httpMethod, path, methodName, parameterTypes)
|
|
30
31
|
*/
|
|
@@ -48,8 +49,9 @@ class MethodMeta {
|
|
|
48
49
|
/** API method arguments in declaration order; defaults to the historical one-body call. */
|
|
49
50
|
requestArgs = requestDto === undefined
|
|
50
51
|
? []
|
|
51
|
-
: [requestDto]) {
|
|
52
|
+
: [requestDto], invocationAuthentication) {
|
|
52
53
|
this.requestArgs = requestArgs;
|
|
54
|
+
this.invocationAuthentication = invocationAuthentication;
|
|
53
55
|
this.routeMeta = routeMeta;
|
|
54
56
|
this.requestDto = requestDto;
|
|
55
57
|
this.metadata = metadata ?? new Map();
|
package/src/MethodMeta.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MethodMeta.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/MethodMeta.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"MethodMeta.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/MethodMeta.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,UAAU;IA0BC;IAGA;IA5BpB;;OAEG;IACH,SAAS,CAAgB;IAEzB;;OAEG;IACH,wFAAwF;IACxF,UAAU,CAAW;IAErB;;;OAGG;IACH,yFAAyF;IACzF,QAAQ,CAAuB;IAE/B,YACI,SAAwB;IACxB,wFAAwF;IACxF,UAAoB;IACpB,yFAAyF;IACzF,QAA+B;IAC/B,2FAA2F;IAC3E,cAAkC,UAAU,KAAK,SAAS;QACtE,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,CAAC,UAAU,CAAC,EACF,wBAAmD;QAHnD,gBAAW,GAAX,WAAW,CAET;QACF,6BAAwB,GAAxB,wBAAwB,CAA2B;QAEnE,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,IAAI,IAAI,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,IAAI,UAAU;QACV,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;IACrC,CAAC;IAED;;OAEG;IACH,IAAI,IAAI;QACJ,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,IAAI,UAAU;QACV,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC;IACrC,CAAC;CACJ;AAxDD,gCAwDC","sourcesContent":["import { InvocationAuthentication } from './InvocationAuthentication';\nimport { RouteMetadata } from '@webpieces/core-util';\n\n/**\n * Metadata about the method being invoked.\n * Passed to filters and contains request information.\n *\n * MethodMeta is DTO-only - it does NOT contain Express req/res, nor the raw headers. The raw\n * inbound request (headers/method/path) lives on the transport-neutral {@link HttpRequest} in\n * RequestContext (read via `RequestContext.getRequest()`); MethodMeta carries only the typed\n * body + route/auth metadata that flow as the chain's call argument.\n *\n * It is the meta type every `Filter<MethodMeta, …>` is parameterized over. It lives in\n * @webpieces/http-routing and is express-free, so filter authors reference it without pulling\n * in any express dependency.\n *\n * Fields:\n * - routeMeta: Static route information (httpMethod, path, methodName, authMeta)\n * - requestDto: The deserialized request body\n * - metadata: Request-scoped data for filters to communicate\n *\n * Auth mode lives on {@link RouteMetadata.authMeta} (the one source of truth for fixed route\n * metadata); read it via `methodMeta.routeMeta.authMeta`. MethodMeta itself carries only the\n * static route reference + the request-scoped body/bag.\n */\nexport class MethodMeta {\n /**\n * Route metadata (httpMethod, path, methodName, parameterTypes)\n */\n routeMeta: RouteMetadata;\n\n /**\n * The deserialized request DTO.\n */\n // webpieces-disable no-any-unknown -- request DTO type is erased at the filter boundary\n requestDto?: unknown;\n\n /**\n * Additional metadata for storing request-scoped data.\n * Used by filters to pass data to other filters/controllers.\n */\n // webpieces-disable no-any-unknown -- request-scoped bag holds heterogeneous filter data\n metadata: Map<string, unknown>;\n\n constructor(\n routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- request DTO type is erased at the filter boundary\n requestDto?: unknown,\n // webpieces-disable no-any-unknown -- request-scoped bag holds heterogeneous filter data\n metadata?: Map<string, unknown>,\n /** API method arguments in declaration order; defaults to the historical one-body call. */\n public readonly requestArgs: readonly unknown[] = requestDto === undefined\n ? []\n : [requestDto],\n public readonly invocationAuthentication?: InvocationAuthentication,\n ) {\n this.routeMeta = routeMeta;\n this.requestDto = requestDto;\n this.metadata = metadata ?? new Map();\n }\n\n /**\n * Get the HTTP method (convenience accessor).\n */\n get httpMethod(): string {\n return this.routeMeta.httpMethod;\n }\n\n /**\n * Get the request path (convenience accessor).\n */\n get path(): string {\n return this.routeMeta.path;\n }\n\n /**\n * Get the method name (convenience accessor).\n */\n get methodName(): string {\n return this.routeMeta.methodName;\n }\n}\n"]}
|
package/src/WebpiecesRouter.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { InvocationAuthentication } from './InvocationAuthentication';
|
|
1
2
|
import { Container, ContainerModule } from 'inversify';
|
|
2
3
|
import { RouteBuilderImpl } from './RouteBuilderImpl';
|
|
3
4
|
import { ClassType } from './ApiRoutingFactory';
|
|
@@ -88,6 +89,8 @@ export declare class WebpiecesRouter implements ApiFactory {
|
|
|
88
89
|
* with NO express/HTTP. The primary path for tests and node-only callers.
|
|
89
90
|
*/
|
|
90
91
|
createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;
|
|
92
|
+
/** Transport-only credential bridge; ordinary endpoint filters remain mandatory. */
|
|
93
|
+
createInvocationApiClient<T>(apiPrototype: abstract new (...args: any[]) => T, authentication: InvocationAuthentication): T;
|
|
91
94
|
/**
|
|
92
95
|
* Reify the registered APIs as {@link ApiClient}s (contract + the createApiClient proxy) via
|
|
93
96
|
* the shared {@link ApiClientFactory}. This is the ONLY handoff to the express layer — the
|
package/src/WebpiecesRouter.js
CHANGED
|
@@ -121,6 +121,11 @@ let WebpiecesRouter = class WebpiecesRouter {
|
|
|
121
121
|
createApiClient(apiPrototype) {
|
|
122
122
|
return this.apiClientFactory.createApiClient(apiPrototype);
|
|
123
123
|
}
|
|
124
|
+
/** Transport-only credential bridge; ordinary endpoint filters remain mandatory. */
|
|
125
|
+
// webpieces-disable no-any-unknown -- abstract API constructor signature
|
|
126
|
+
createInvocationApiClient(apiPrototype, authentication) {
|
|
127
|
+
return this.apiClientFactory.createInvocationApiClient(apiPrototype, authentication);
|
|
128
|
+
}
|
|
124
129
|
/**
|
|
125
130
|
* Reify the registered APIs as {@link ApiClient}s (contract + the createApiClient proxy) via
|
|
126
131
|
* the shared {@link ApiClientFactory}. This is the ONLY handoff to the express layer — the
|
|
@@ -154,7 +159,9 @@ class WebpiecesRouterFactory {
|
|
|
154
159
|
// singletons (WebpiecesRouter, RouteBuilderImpl) come from the webpieces registry,
|
|
155
160
|
// NOT the client's global one.
|
|
156
161
|
const webpiecesContainer = new inversify_1.Container();
|
|
157
|
-
webpiecesContainer
|
|
162
|
+
webpiecesContainer
|
|
163
|
+
.bind(WebpiecesConfig_1.WEBPIECES_CONFIG_TOKEN)
|
|
164
|
+
.toConstantValue(options.config ?? new WebpiecesConfig_1.WebpiecesConfig());
|
|
158
165
|
await webpiecesContainer.load((0, core_context_1.buildFrameworkModule)());
|
|
159
166
|
// Resolve the router from the container (NOT new'd) so @DocumentDesign + DI hold.
|
|
160
167
|
const router = webpiecesContainer.get(WebpiecesRouter);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/WebpiecesRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Container, ContainerModule, inject } from 'inversify';\nimport { buildProviderModule } from '@inversifyjs/binding-decorators';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { provideFrameworkSingleton, buildFrameworkModule } from '@webpieces/core-context';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\nimport { FilterDefinition } from './WebAppMeta';\nimport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\nimport { ApiClientFactory } from './ApiClientFactory';\nimport { ApiFactory } from './ApiFactory';\nimport { ApiClient } from './ApiClient';\nimport { LogApiFilter } from './filters/LogApiFilter';\nimport { AuthFilter } from './filters/AuthFilter';\n\n/**\n * Options for {@link WebpiecesRouterFactory.create} — one object (config lives inside it).\n *\n * appBindings - DI ContainerModules to load (framework + app). Loaded after the\n * @provideSingleton auto-scan so they can add/override bindings.\n * appOverrides - A single ContainerModule loaded LAST so tests can rebind real\n * controllers/clients to mocks (see @webpieces/core-mock createMock()).\n * config - Optional {@link WebpiecesConfig} (recording flags, etc.); defaults to a fresh one.\n */\nexport interface WebpiecesRouterOptions {\n appBindings: ContainerModule[];\n appOverrides?: ContainerModule;\n config?: WebpiecesConfig;\n}\n\n/**\n * WebpiecesRouter - the node-only heart of a webpieces app: a DI container + a filter\n * chain + an in-process API client. It has NO express dependency, so it runs anywhere\n * node runs and is fully testable with zero HTTP.\n *\n * DI-resolved from the platform container (like the old WebpiecesServerImpl):\n * `@provideSingleton @injectable`, RouteBuilderImpl injected, and the two containers set in\n * initialize(). Built by {@link WebpiecesRouterFactory.create} — never `new`ed by callers.\n *\n * Two-container pattern (mirrors Java WebPieces):\n * - webpiecesContainer : framework bindings (config token, @DocumentDesign design roots)\n * - appContainer : your controllers/filters/modules (a child of the framework one)\n *\n * Usage:\n * ```typescript\n * const router = await WebpiecesRouterFactory.create({ appBindings: [AppModule] });\n * router.addRoutes(SaveApi, SaveController);\n * router.addFilter(new FilterDefinition(1800, MyFilter, '*')); // your own filters\n * // (LogApiFilter + AuthFilter are auto-installed above yours; auth is AuthMode-driven.\n * // LogApiFilter logs request+response for EVERY call — do NOT install it yourself.)\n *\n * // test (no express): runs the SAME filter chain (incl. auth) -> controller\n * const api = router.createApiClient(SaveApi);\n * await api.save(new SaveRequest(...));\n * ```\n *\n * To serve real HTTP, hand this router to the express adapter in @webpieces/http-server\n * (bindExpress / bindAndStartExpress) — express lifecycle lives THERE, never here.\n *\n * @DocumentDesign marks it a design root so it appears in http-routing's designed-lib graph.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\nexport class WebpiecesRouter implements ApiFactory {\n private webpiecesContainer!: Container;\n private appContainer!: Container;\n\n constructor(\n @inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl,\n @inject(ApiClientFactory) private readonly apiClientFactory: ApiClientFactory,\n ) {}\n\n /**\n * Build the app container (child of the framework container), load the @provideSingleton\n * auto-scan + appBindings + appOverrides, and point the RouteBuilder at it. Called once by\n * the factory after this router is resolved from the framework container.\n */\n async initialize(webpiecesContainer: Container, options: WebpiecesRouterOptions): Promise<void> {\n this.webpiecesContainer = webpiecesContainer;\n\n // App container is a child so app bindings see framework bindings while staying separate.\n // autobind:true lets a concrete @injectable(Singleton) app class self-bind on first resolve\n // (inject-by-type, no @provideSingleton). Framework singletons stay explicit in the parent\n // (provideFrameworkSingleton -> buildFrameworkModule), found before child-autobind kicks in.\n this.appContainer = new Container({ parent: webpiecesContainer, autobind: true });\n this.routeBuilder.setContainer(this.appContainer);\n\n await this.loadDIModules(options);\n this.installFixedFilters();\n }\n\n /**\n * Auto-install the two fixed framework filters on every route (apps add only their own\n * filters below these): LogApiFilter outermost (logs request + response/failure for every\n * call and stamps [Controller.method], then re-throws for the transport to translate), then\n * AuthFilter (enforces the endpoint's AuthMode off the HttpRequest). Both run over HTTP AND\n * in-process — there is no transport tier. Because LogApiFilter is outermost, requests that\n * AuthFilter rejects (401) are still logged with their body + controller identity.\n */\n private installFixedFilters(): void {\n this.addFilter(new FilterDefinition(1_000_000, LogApiFilter, '*'));\n this.addFilter(new FilterDefinition(900_000, AuthFilter, '*'));\n }\n\n private async loadDIModules(options: WebpiecesRouterOptions): Promise<void> {\n // Load BOTH registries: framework classes (provideFrameworkSingleton) + the client's\n // own @provideSingleton classes (binding-decorators global). A client's\n // buildProviderModule() only ever contains the client's classes — never framework internals.\n await this.appContainer.load(buildFrameworkModule());\n await this.appContainer.load(buildProviderModule());\n\n // Load all app modules into application container\n // (webpiecesContainer is currently empty, reserved for future framework bindings)\n for (const module of options.appBindings) {\n await this.appContainer.load(module);\n }\n\n // Load appOverrides LAST so they can override existing bindings\n if (options.appOverrides) {\n await this.appContainer.load(options.appOverrides);\n }\n }\n\n /**\n * Wire an API prototype (with @ApiPath/@Endpoint decorators) to its controller.\n * The controller is resolved from the container at request time.\n */\n addRoutes<TApi, TController extends TApi>(\n api: ClassType<TApi>,\n controller: ClassType<TController>,\n ): this {\n new ApiRoutingFactory(api, controller).configure(this.routeBuilder);\n return this;\n }\n\n /**\n * Register a user filter (runs in-process AND over HTTP, below the auto-installed fixed\n * LogApiFilter + AuthFilter).\n */\n addFilter(filter: FilterDefinition): this {\n this.routeBuilder.addFilter(filter);\n return this;\n }\n\n /**\n * Create an in-process API client that runs the api-tier filter chain + controller\n * with NO express/HTTP. The primary path for tests and node-only callers.\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.apiClientFactory.createApiClient(apiPrototype);\n }\n\n /**\n * Reify the registered APIs as {@link ApiClient}s (contract + the createApiClient proxy) via\n * the shared {@link ApiClientFactory}. This is the ONLY handoff to the express layer — the\n * internal RouteBuilder never leaves.\n */\n apiClients(): ApiClient[] {\n return this.apiClientFactory.apiClients();\n }\n\n /** The application DI container (child of the framework container). */\n getContainer(): Container {\n return this.appContainer;\n }\n}\n\n/**\n * Builds a {@link WebpiecesRouter}: constructs the platform container (mirrors\n * WebpiecesServerFactory.create), RESOLVES the router from DI, then initializes its app child\n * container with the @provideSingleton auto-scan + appBindings + optional test overrides.\n */\nexport class WebpiecesRouterFactory {\n static async create(options: WebpiecesRouterOptions): Promise<WebpiecesRouter> {\n // Platform (framework) container — build via buildFrameworkModule so framework\n // singletons (WebpiecesRouter, RouteBuilderImpl) come from the webpieces registry,\n // NOT the client's global one.\n const webpiecesContainer = new Container();\n webpiecesContainer.bind(WEBPIECES_CONFIG_TOKEN).toConstantValue(options.config ?? new WebpiecesConfig());\n await webpiecesContainer.load(buildFrameworkModule());\n\n // Resolve the router from the container (NOT new'd) so @DocumentDesign + DI hold.\n const router = webpiecesContainer.get(WebpiecesRouter);\n await router.initialize(webpiecesContainer, options);\n return router;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"WebpiecesRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/WebpiecesRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { InvocationAuthentication } from './InvocationAuthentication';\nimport { Container, ContainerModule, inject } from 'inversify';\nimport { buildProviderModule } from '@inversifyjs/binding-decorators';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { provideFrameworkSingleton, buildFrameworkModule } from '@webpieces/core-context';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\nimport { FilterDefinition } from './WebAppMeta';\nimport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\nimport { ApiClientFactory } from './ApiClientFactory';\nimport { ApiFactory } from './ApiFactory';\nimport { ApiClient } from './ApiClient';\nimport { LogApiFilter } from './filters/LogApiFilter';\nimport { AuthFilter } from './filters/AuthFilter';\n\n/**\n * Options for {@link WebpiecesRouterFactory.create} — one object (config lives inside it).\n *\n * appBindings - DI ContainerModules to load (framework + app). Loaded after the\n * @provideSingleton auto-scan so they can add/override bindings.\n * appOverrides - A single ContainerModule loaded LAST so tests can rebind real\n * controllers/clients to mocks (see @webpieces/core-mock createMock()).\n * config - Optional {@link WebpiecesConfig} (recording flags, etc.); defaults to a fresh one.\n */\nexport interface WebpiecesRouterOptions {\n appBindings: ContainerModule[];\n appOverrides?: ContainerModule;\n config?: WebpiecesConfig;\n}\n\n/**\n * WebpiecesRouter - the node-only heart of a webpieces app: a DI container + a filter\n * chain + an in-process API client. It has NO express dependency, so it runs anywhere\n * node runs and is fully testable with zero HTTP.\n *\n * DI-resolved from the platform container (like the old WebpiecesServerImpl):\n * `@provideSingleton @injectable`, RouteBuilderImpl injected, and the two containers set in\n * initialize(). Built by {@link WebpiecesRouterFactory.create} — never `new`ed by callers.\n *\n * Two-container pattern (mirrors Java WebPieces):\n * - webpiecesContainer : framework bindings (config token, @DocumentDesign design roots)\n * - appContainer : your controllers/filters/modules (a child of the framework one)\n *\n * Usage:\n * ```typescript\n * const router = await WebpiecesRouterFactory.create({ appBindings: [AppModule] });\n * router.addRoutes(SaveApi, SaveController);\n * router.addFilter(new FilterDefinition(1800, MyFilter, '*')); // your own filters\n * // (LogApiFilter + AuthFilter are auto-installed above yours; auth is AuthMode-driven.\n * // LogApiFilter logs request+response for EVERY call — do NOT install it yourself.)\n *\n * // test (no express): runs the SAME filter chain (incl. auth) -> controller\n * const api = router.createApiClient(SaveApi);\n * await api.save(new SaveRequest(...));\n * ```\n *\n * To serve real HTTP, hand this router to the express adapter in @webpieces/http-server\n * (bindExpress / bindAndStartExpress) — express lifecycle lives THERE, never here.\n *\n * @DocumentDesign marks it a design root so it appears in http-routing's designed-lib graph.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\nexport class WebpiecesRouter implements ApiFactory {\n private webpiecesContainer!: Container;\n private appContainer!: Container;\n\n constructor(\n @inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl,\n @inject(ApiClientFactory) private readonly apiClientFactory: ApiClientFactory,\n ) {}\n\n /**\n * Build the app container (child of the framework container), load the @provideSingleton\n * auto-scan + appBindings + appOverrides, and point the RouteBuilder at it. Called once by\n * the factory after this router is resolved from the framework container.\n */\n async initialize(\n webpiecesContainer: Container,\n options: WebpiecesRouterOptions,\n ): Promise<void> {\n this.webpiecesContainer = webpiecesContainer;\n\n // App container is a child so app bindings see framework bindings while staying separate.\n // autobind:true lets a concrete @injectable(Singleton) app class self-bind on first resolve\n // (inject-by-type, no @provideSingleton). Framework singletons stay explicit in the parent\n // (provideFrameworkSingleton -> buildFrameworkModule), found before child-autobind kicks in.\n this.appContainer = new Container({ parent: webpiecesContainer, autobind: true });\n this.routeBuilder.setContainer(this.appContainer);\n\n await this.loadDIModules(options);\n this.installFixedFilters();\n }\n\n /**\n * Auto-install the two fixed framework filters on every route (apps add only their own\n * filters below these): LogApiFilter outermost (logs request + response/failure for every\n * call and stamps [Controller.method], then re-throws for the transport to translate), then\n * AuthFilter (enforces the endpoint's AuthMode off the HttpRequest). Both run over HTTP AND\n * in-process — there is no transport tier. Because LogApiFilter is outermost, requests that\n * AuthFilter rejects (401) are still logged with their body + controller identity.\n */\n private installFixedFilters(): void {\n this.addFilter(new FilterDefinition(1_000_000, LogApiFilter, '*'));\n this.addFilter(new FilterDefinition(900_000, AuthFilter, '*'));\n }\n\n private async loadDIModules(options: WebpiecesRouterOptions): Promise<void> {\n // Load BOTH registries: framework classes (provideFrameworkSingleton) + the client's\n // own @provideSingleton classes (binding-decorators global). A client's\n // buildProviderModule() only ever contains the client's classes — never framework internals.\n await this.appContainer.load(buildFrameworkModule());\n await this.appContainer.load(buildProviderModule());\n\n // Load all app modules into application container\n // (webpiecesContainer is currently empty, reserved for future framework bindings)\n for (const module of options.appBindings) {\n await this.appContainer.load(module);\n }\n\n // Load appOverrides LAST so they can override existing bindings\n if (options.appOverrides) {\n await this.appContainer.load(options.appOverrides);\n }\n }\n\n /**\n * Wire an API prototype (with @ApiPath/@Endpoint decorators) to its controller.\n * The controller is resolved from the container at request time.\n */\n addRoutes<TApi, TController extends TApi>(\n api: ClassType<TApi>,\n controller: ClassType<TController>,\n ): this {\n new ApiRoutingFactory(api, controller).configure(this.routeBuilder);\n return this;\n }\n\n /**\n * Register a user filter (runs in-process AND over HTTP, below the auto-installed fixed\n * LogApiFilter + AuthFilter).\n */\n addFilter(filter: FilterDefinition): this {\n this.routeBuilder.addFilter(filter);\n return this;\n }\n\n /**\n * Create an in-process API client that runs the api-tier filter chain + controller\n * with NO express/HTTP. The primary path for tests and node-only callers.\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.apiClientFactory.createApiClient(apiPrototype);\n }\n\n /** Transport-only credential bridge; ordinary endpoint filters remain mandatory. */\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.apiClientFactory.createInvocationApiClient(apiPrototype, authentication);\n }\n\n /**\n * Reify the registered APIs as {@link ApiClient}s (contract + the createApiClient proxy) via\n * the shared {@link ApiClientFactory}. This is the ONLY handoff to the express layer — the\n * internal RouteBuilder never leaves.\n */\n apiClients(): ApiClient[] {\n return this.apiClientFactory.apiClients();\n }\n\n /** The application DI container (child of the framework container). */\n getContainer(): Container {\n return this.appContainer;\n }\n}\n\n/**\n * Builds a {@link WebpiecesRouter}: constructs the platform container (mirrors\n * WebpiecesServerFactory.create), RESOLVES the router from DI, then initializes its app child\n * container with the @provideSingleton auto-scan + appBindings + optional test overrides.\n */\nexport class WebpiecesRouterFactory {\n static async create(options: WebpiecesRouterOptions): Promise<WebpiecesRouter> {\n // Platform (framework) container — build via buildFrameworkModule so framework\n // singletons (WebpiecesRouter, RouteBuilderImpl) come from the webpieces registry,\n // NOT the client's global one.\n const webpiecesContainer = new Container();\n webpiecesContainer\n .bind(WEBPIECES_CONFIG_TOKEN)\n .toConstantValue(options.config ?? new WebpiecesConfig());\n await webpiecesContainer.load(buildFrameworkModule());\n\n // Resolve the router from the container (NOT new'd) so @DocumentDesign + DI hold.\n const router = webpiecesContainer.get(WebpiecesRouter);\n await router.initialize(webpiecesContainer, options);\n return router;\n }\n}\n"]}
|
|
@@ -38,6 +38,7 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
|
|
|
38
38
|
private readonly oidcHook?;
|
|
39
39
|
private readonly webhookAuthCallback?;
|
|
40
40
|
private readonly apiKeyHook?;
|
|
41
|
+
private readonly callerContext;
|
|
41
42
|
constructor(oidcVerifier: DefaultOidcVerifier, authConfig?: AuthConfig | undefined, jwtHook?: JwtHook<never> | undefined, oidcHook?: OidcHook | undefined, webhookAuthCallback?: WebhookAuthCallback | undefined, apiKeyHook?: ApiKeyHook | undefined);
|
|
42
43
|
filter(meta: MethodMeta, nextFilter: Service<MethodMeta, WpResponse<unknown>>): Promise<WpResponse<unknown>>;
|
|
43
44
|
/**
|
|
@@ -151,41 +152,6 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
|
|
|
151
152
|
* answer, but arrived at by accident rather than by decision.
|
|
152
153
|
*/
|
|
153
154
|
private static verifiesCaller;
|
|
154
|
-
/**
|
|
155
|
-
* Decide what happens to the trusted keys that arrived on the WIRE and were held back by
|
|
156
|
-
* {@link PendingWireTrust} (read that class for why they are held rather than written).
|
|
157
|
-
*
|
|
158
|
-
* `callerVerified` — the endpoint authenticated the SENDER **as a peer service** (`@WpAuthOidc`,
|
|
159
|
-
* `@WpAuthSharedSecret`).
|
|
160
|
-
* The sender is a service we trust, this is the service-to-service hop, and its forwarded
|
|
161
|
-
* identity is admitted as-is. This is the case that makes propagating a verified userId across
|
|
162
|
-
* internal services work.
|
|
163
|
-
*
|
|
164
|
-
* Otherwise the sender is a browser or anyone else with curl, and the ONLY acceptable inbound
|
|
165
|
-
* trusted value is one the authenticator independently derived to the same value. Everything
|
|
166
|
-
* else is rejected — see {@link requireVouched}.
|
|
167
|
-
*
|
|
168
|
-
* Runs AFTER the mode enforcement above, because that is what stamps the authenticator's own
|
|
169
|
-
* values (`applyAuthenticatedCaller`); comparing before it ran would compare against nothing.
|
|
170
|
-
*/
|
|
171
|
-
private reconcileWireTrust;
|
|
172
|
-
/**
|
|
173
|
-
* On a browser-reachable route, an inbound trusted header must match what the authenticator
|
|
174
|
-
* itself derived, or the request dies. Both failure shapes are rejections, not repairs:
|
|
175
|
-
*
|
|
176
|
-
* - DIFFERENT value — the caller said `alice`, the credential says `bob`. Silently letting the
|
|
177
|
-
* credential win is not safe, because upstream rate limiters commonly bucket on the header
|
|
178
|
-
* rather than the token: the request was already counted against the wrong principal, so
|
|
179
|
-
* every forged header would be a free rate-limit bypass. No honest caller contradicts its own
|
|
180
|
-
* credential.
|
|
181
|
-
* - NOTHING vouched for it — nobody derived this key at all, so there is no evidence behind a
|
|
182
|
-
* value a stranger typed. This is the common case, not the exotic one: the framework's
|
|
183
|
-
* {@link DefaultJwtHook} stamps NO entries, and an app hook (jwt or api-key) only stamps the keys it can prove,
|
|
184
|
-
* so any other trusted key a caller sends lands here.
|
|
185
|
-
*
|
|
186
|
-
* The pending value is discarded either way — the throw is what leaves the request.
|
|
187
|
-
*/
|
|
188
|
-
private requireVouched;
|
|
189
155
|
/**
|
|
190
156
|
* `@WpAuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the
|
|
191
157
|
* endpoint did not exist.
|
|
@@ -211,12 +177,6 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
|
|
|
211
177
|
private matchesEither;
|
|
212
178
|
/** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */
|
|
213
179
|
private bestEffortJwt;
|
|
214
|
-
/**
|
|
215
|
-
* Stamp the authenticated caller's context entries + the caller itself into the RequestContext.
|
|
216
|
-
* ONE path for all three authenticating hooks — jwt, api-key and webhook — so a vendor hook that
|
|
217
|
-
* proved which account a payload belongs to seeds context exactly as a JwtHook does.
|
|
218
|
-
*/
|
|
219
|
-
private applyAuthenticatedCaller;
|
|
220
180
|
/**
|
|
221
181
|
* The credential value IF the header carries the expected scheme, else undefined.
|
|
222
182
|
*
|
|
@@ -10,6 +10,7 @@ const core_util_1 = require("@webpieces/core-util");
|
|
|
10
10
|
const core_util_2 = require("@webpieces/core-util");
|
|
11
11
|
const AuthConfig_1 = require("../AuthConfig");
|
|
12
12
|
const AuthHooks_1 = require("../AuthHooks");
|
|
13
|
+
const AuthenticatedCallerContext_1 = require("../AuthenticatedCallerContext");
|
|
13
14
|
const DefaultOidcVerifier_1 = require("../DefaultOidcVerifier");
|
|
14
15
|
const log = core_util_1.LogManager.getLogger('AuthFilter');
|
|
15
16
|
/**
|
|
@@ -65,6 +66,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
65
66
|
oidcHook;
|
|
66
67
|
webhookAuthCallback;
|
|
67
68
|
apiKeyHook;
|
|
69
|
+
callerContext = new AuthenticatedCallerContext_1.AuthenticatedCallerContext();
|
|
68
70
|
constructor(oidcVerifier, authConfig, jwtHook, oidcHook, webhookAuthCallback, apiKeyHook) {
|
|
69
71
|
super();
|
|
70
72
|
this.oidcVerifier = oidcVerifier;
|
|
@@ -77,17 +79,19 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
77
79
|
// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility
|
|
78
80
|
async filter(meta, nextFilter) {
|
|
79
81
|
const mode = meta.routeMeta.authMeta?.mode;
|
|
80
|
-
const authHeader =
|
|
82
|
+
const authHeader = meta.invocationAuthentication
|
|
83
|
+
? `Bearer ${meta.invocationAuthentication.token}`
|
|
84
|
+
: core_context_1.RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);
|
|
81
85
|
if (!mode || mode.kind === 'public') {
|
|
82
86
|
// Public: best-effort parse so a logged-out page can still know the logged-in user.
|
|
83
87
|
await this.bestEffortJwt(authHeader);
|
|
84
|
-
this.reconcileWireTrust(/*callerVerified*/ false);
|
|
88
|
+
this.callerContext.reconcileWireTrust(/*callerVerified*/ false);
|
|
85
89
|
this.rethrowDeferredBodyError();
|
|
86
90
|
return nextFilter.invoke(meta);
|
|
87
91
|
}
|
|
88
92
|
switch (mode.kind) {
|
|
89
93
|
case 'jwt':
|
|
90
|
-
await this.enforceJwt(authHeader, mode.requirement);
|
|
94
|
+
await this.enforceJwt(authHeader, mode.requirement, meta.invocationAuthentication);
|
|
91
95
|
break;
|
|
92
96
|
case 'oidc':
|
|
93
97
|
await this.enforceOidc(authHeader, mode.callers);
|
|
@@ -106,7 +110,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
106
110
|
break;
|
|
107
111
|
}
|
|
108
112
|
this.applySurface(mode);
|
|
109
|
-
this.reconcileWireTrust(AuthFilter_1.verifiesCaller(mode));
|
|
113
|
+
this.callerContext.reconcileWireTrust(AuthFilter_1.verifiesCaller(mode));
|
|
110
114
|
this.rethrowDeferredBodyError();
|
|
111
115
|
return nextFilter.invoke(meta);
|
|
112
116
|
}
|
|
@@ -197,7 +201,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
197
201
|
// Throws ApiUnauthorizedError to deny. On success the vendor account the signature proved is
|
|
198
202
|
// stamped through the SAME path a jwt or api-key caller takes.
|
|
199
203
|
const caller = await this.webhookAuthCallback.verifyWebhook(name, request);
|
|
200
|
-
this.
|
|
204
|
+
this.callerContext.publish(caller);
|
|
201
205
|
}
|
|
202
206
|
/**
|
|
203
207
|
* The ONE place the framework decides a request carries the verbatim bytes. A TYPE PREDICATE, so
|
|
@@ -240,7 +244,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
240
244
|
// Throws ApiUnauthorizedError to deny. The hook gets the WHOLE request so it can cross-check
|
|
241
245
|
// the key against a second header (the organization the customer is acting for).
|
|
242
246
|
const caller = await this.apiKeyHook.verifyApiKey(regime, request);
|
|
243
|
-
this.
|
|
247
|
+
this.callerContext.publish(caller);
|
|
244
248
|
}
|
|
245
249
|
/**
|
|
246
250
|
* A body that failed to parse is held on the {@link RawRequest} and surfaces HERE, after auth, as
|
|
@@ -307,63 +311,6 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
307
311
|
return false;
|
|
308
312
|
}
|
|
309
313
|
}
|
|
310
|
-
/**
|
|
311
|
-
* Decide what happens to the trusted keys that arrived on the WIRE and were held back by
|
|
312
|
-
* {@link PendingWireTrust} (read that class for why they are held rather than written).
|
|
313
|
-
*
|
|
314
|
-
* `callerVerified` — the endpoint authenticated the SENDER **as a peer service** (`@WpAuthOidc`,
|
|
315
|
-
* `@WpAuthSharedSecret`).
|
|
316
|
-
* The sender is a service we trust, this is the service-to-service hop, and its forwarded
|
|
317
|
-
* identity is admitted as-is. This is the case that makes propagating a verified userId across
|
|
318
|
-
* internal services work.
|
|
319
|
-
*
|
|
320
|
-
* Otherwise the sender is a browser or anyone else with curl, and the ONLY acceptable inbound
|
|
321
|
-
* trusted value is one the authenticator independently derived to the same value. Everything
|
|
322
|
-
* else is rejected — see {@link requireVouched}.
|
|
323
|
-
*
|
|
324
|
-
* Runs AFTER the mode enforcement above, because that is what stamps the authenticator's own
|
|
325
|
-
* values (`applyAuthenticatedCaller`); comparing before it ran would compare against nothing.
|
|
326
|
-
*/
|
|
327
|
-
reconcileWireTrust(callerVerified) {
|
|
328
|
-
const pending = core_context_1.PendingWireTrust.takeAll();
|
|
329
|
-
for (const item of pending) {
|
|
330
|
-
if (callerVerified) {
|
|
331
|
-
core_context_1.RequestContext.putTrusted(item.key, item.value);
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
this.requireVouched(item);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* On a browser-reachable route, an inbound trusted header must match what the authenticator
|
|
340
|
-
* itself derived, or the request dies. Both failure shapes are rejections, not repairs:
|
|
341
|
-
*
|
|
342
|
-
* - DIFFERENT value — the caller said `alice`, the credential says `bob`. Silently letting the
|
|
343
|
-
* credential win is not safe, because upstream rate limiters commonly bucket on the header
|
|
344
|
-
* rather than the token: the request was already counted against the wrong principal, so
|
|
345
|
-
* every forged header would be a free rate-limit bypass. No honest caller contradicts its own
|
|
346
|
-
* credential.
|
|
347
|
-
* - NOTHING vouched for it — nobody derived this key at all, so there is no evidence behind a
|
|
348
|
-
* value a stranger typed. This is the common case, not the exotic one: the framework's
|
|
349
|
-
* {@link DefaultJwtHook} stamps NO entries, and an app hook (jwt or api-key) only stamps the keys it can prove,
|
|
350
|
-
* so any other trusted key a caller sends lands here.
|
|
351
|
-
*
|
|
352
|
-
* The pending value is discarded either way — the throw is what leaves the request.
|
|
353
|
-
*/
|
|
354
|
-
requireVouched(item) {
|
|
355
|
-
const vouched = core_context_1.RequestContext.getTrusted(item.key);
|
|
356
|
-
if (vouched === item.value) {
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
log.error(`Rejecting inbound '${item.key.httpHeader}': it is a TRUSTED context key, this route does ` +
|
|
360
|
-
`not authenticate its caller, and the credential ` +
|
|
361
|
-
(vouched === undefined
|
|
362
|
-
? 'vouched for no such value'
|
|
363
|
-
: 'derived a different value') +
|
|
364
|
-
'.');
|
|
365
|
-
throw new core_util_1.ApiUnauthorizedError(`Header '${item.key.httpHeader}' cannot be supplied by the caller on this endpoint`);
|
|
366
|
-
}
|
|
367
314
|
/**
|
|
368
315
|
* `@WpAuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the
|
|
369
316
|
* endpoint did not exist.
|
|
@@ -392,7 +339,7 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
392
339
|
// Same shape as an unregistered route — see the method doc for why this is not a 403.
|
|
393
340
|
throw new core_util_1.ApiEndpointNotFoundError(`No endpoint at ${meta.routeMeta.path}`);
|
|
394
341
|
}
|
|
395
|
-
async enforceJwt(header, requirement) {
|
|
342
|
+
async enforceJwt(header, requirement, invocation) {
|
|
396
343
|
const token = this.credential(header, BEARER_SCHEME);
|
|
397
344
|
if (!token) {
|
|
398
345
|
throw new core_util_1.ApiUnauthorizedError('Authentication required');
|
|
@@ -401,7 +348,9 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
401
348
|
throw new core_util_1.ApiUnauthorizedError('User-JWT auth is not enabled on this server');
|
|
402
349
|
}
|
|
403
350
|
const caller = await this.jwtHook.parseJwt(token); // AUTHENTICATE — throws ApiUnauthorizedError if invalid
|
|
404
|
-
|
|
351
|
+
invocation?.assertCaller(caller);
|
|
352
|
+
if (!invocation)
|
|
353
|
+
this.callerContext.publish(caller);
|
|
405
354
|
await this.jwtHook.authorizeJwt(caller, requirement); // AUTHORIZE — app policy; throws ApiForbiddenError to deny
|
|
406
355
|
}
|
|
407
356
|
async enforceOidc(header, callers) {
|
|
@@ -437,28 +386,13 @@ let AuthFilter = AuthFilter_1 = class AuthFilter extends core_util_2.Filter {
|
|
|
437
386
|
}
|
|
438
387
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort on a public route: a bad/absent token just means "not logged in", must not fail the request
|
|
439
388
|
try {
|
|
440
|
-
this.
|
|
389
|
+
this.callerContext.publish(await this.jwtHook.parseJwt(token));
|
|
441
390
|
}
|
|
442
391
|
catch (err) {
|
|
443
392
|
const error = (0, core_util_1.toError)(err);
|
|
444
393
|
log.debug('Best-effort JWT parse on a public endpoint failed (treating as anonymous): ', error);
|
|
445
394
|
}
|
|
446
395
|
}
|
|
447
|
-
/**
|
|
448
|
-
* Stamp the authenticated caller's context entries + the caller itself into the RequestContext.
|
|
449
|
-
* ONE path for all three authenticating hooks — jwt, api-key and webhook — so a vendor hook that
|
|
450
|
-
* proved which account a payload belongs to seeds context exactly as a JwtHook does.
|
|
451
|
-
*/
|
|
452
|
-
applyAuthenticatedCaller(caller) {
|
|
453
|
-
for (const entry of caller.entries) {
|
|
454
|
-
// ContextTuple.key is a TRUSTED key by type, so this is the one sanctioned write of a
|
|
455
|
-
// proven identity: the app's hook derived it from a credential we just verified.
|
|
456
|
-
core_context_1.RequestContext.putTrusted(entry.key, entry.value);
|
|
457
|
-
}
|
|
458
|
-
// A real TRUSTED ContextKey, not a raw string slot: the caller IS the framework's own proof,
|
|
459
|
-
// so it is written with the same typed verb every other proven value goes through.
|
|
460
|
-
core_context_1.RequestContext.putTrusted(AuthConfig_1.AUTHENTICATED_CALLER_KEY, caller);
|
|
461
|
-
}
|
|
462
396
|
/**
|
|
463
397
|
* The credential value IF the header carries the expected scheme, else undefined.
|
|
464
398
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-routing/src/filters/AuthFilter.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport {\n provideFrameworkSingleton,\n HttpRequest,\n PendingWireTrust,\n PendingTrustedValue,\n RawHttpRequest,\n RequestContext,\n} from '@webpieces/core-context';\nimport {\n AuthMode,\n ApiEndpointNotFoundError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n JwtRequirement,\n LogManager,\n RuntimeLocality,\n Surface,\n toError,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { Filter, Service } from '@webpieces/core-util';\nimport { WpResponse } from '../WpResponse';\nimport { MethodMeta } from '../MethodMeta';\nimport {\n AuthConfig,\n AUTH_CONFIG,\n AuthenticatedCaller,\n AUTHENTICATED_CALLER_KEY,\n SharedSecrets,\n} from '../AuthConfig';\nimport {\n ApiKeyHook,\n API_KEY_HOOK,\n JwtHook,\n JWT_HOOK,\n OidcHook,\n OIDC_HOOK,\n WebhookAuthCallback,\n WEBHOOK_AUTH_CALLBACK,\n} from '../AuthHooks';\nimport { DefaultOidcVerifier } from '../DefaultOidcVerifier';\n\nconst log = LogManager.getLogger('AuthFilter');\n\n/**\n * The ONE credential header, read straight off the inbound HttpRequest.\n *\n * Deliberately NOT a ContextKey: a ContextKey with an httpHeader is a TRANSFERRED key, which would\n * put the caller's credential into RequestContext and hence onto every outbound call this service\n * makes, and onto every Cloud Task it enqueues. A credential belongs to ONE request hop.\n */\nconst AUTHORIZATION_HEADER = 'authorization';\n\n/**\n * The scheme (first word of the Authorization value) names WHICH credential follows, so a secret\n * can never be mistaken for a token, nor accepted where the other was expected:\n *\n * Authorization: Bearer <user JWT | service OIDC token>\n * Authorization: Webpieces <@WpAuthSharedSecret value>\n *\n * The scheme is REQUIRED. A bare value with no scheme is rejected.\n */\nconst BEARER_SCHEME = 'Bearer';\nconst SHARED_SECRET_SCHEME = 'Webpieces';\n\n/**\n * AuthFilter - the ONE framework auth filter, auto-installed just below the error filter on every\n * route. It is TRANSPORT-NEUTRAL: it reads the raw credential from the {@link HttpRequest} in\n * RequestContext (never express), so the SAME check runs over HTTP and via createApiClient.\n *\n * It enforces the endpoint's AuthMode from separately-bound pieces, each OPTIONAL except the OIDC\n * default:\n * - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). No\n * AuthConfig bound → no accepted secret → fail fast (401).\n * - jwt → the bound {@link JwtHook} (`parseJwt` + `authorizeJwt`, both awaited — an app's\n * strategy may reach a JWKS or a datastore). No JwtHook bound → \"not enabled\"\n * (401): JWT needs an app secret + payload shape.\n * - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}\n * run DIRECTLY — so a server that wires NOTHING still verifies Google OIDC.\n * - webhook → the bound {@link WebhookAuthCallback} verifies the VENDOR's signature over the retained\n * raw request. No WebhookAuthCallback bound → 401, like jwt: an unverified webhook is\n * never waved through because wiring was forgotten.\n * - apikey → the bound {@link ApiKeyHook} looks the CUSTOMER's key up (async, over the whole\n * header set) and returns the context to seed. No ApiKeyHook bound → 401, like jwt.\n * - public → BEST-EFFORT jwt parse (only if a JwtHook is bound): stamp the user's context so\n * a logged-out page still knows who is logged in; never fails.\n * - local-only → serve only when {@link RuntimeLocality} says this process is a developer's\n * machine; otherwise 404, indistinguishable from the route not existing (which,\n * off-local, it does not — `ApiRoutingFactory` never registered it).\n *\n * Zero wiring = OIDC just works; an app only binds the hooks it actually uses.\n */\n@provideFrameworkSingleton()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\nexport class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n constructor(\n // Framework default, always available — verifies Google OIDC with zero app wiring.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- AuthFilter is DI-resolved via the esbuild/vitest path, which elides type-only imports (no design:paramtypes), so every param needs its explicit token\n @inject(DefaultOidcVerifier) private readonly oidcVerifier: DefaultOidcVerifier,\n // @optional: only bind an AuthConfig to enable @WpAuthSharedSecret endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(AUTH_CONFIG) private readonly authConfig?: AuthConfig,\n // @optional: only bind a JwtHook to enable @WpAuthJwt endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(JWT_HOOK) private readonly jwtHook?: JwtHook<never>,\n // @optional: only bind an OidcHook to OVERRIDE the DefaultOidcVerifier caller policy.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(OIDC_HOOK) private readonly oidcHook?: OidcHook,\n // @optional: only bind a WebhookAuthCallback to enable @WpAuthWebhook endpoints. Unbound = every such\n // endpoint 401s, which is the ONE default that must not be the other way round.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional()\n @inject(WEBHOOK_AUTH_CALLBACK)\n private readonly webhookAuthCallback?: WebhookAuthCallback,\n // @optional: only bind an ApiKeyHook to enable @WpAuthApiKey endpoints. Unbound = every such\n // endpoint 401s, for the same reason as the webhook hook above.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(API_KEY_HOOK) private readonly apiKeyHook?: ApiKeyHook,\n ) {\n super();\n }\n\n // webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\n override async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n const mode = meta.routeMeta.authMeta?.mode;\n const authHeader = RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);\n\n if (!mode || mode.kind === 'public') {\n // Public: best-effort parse so a logged-out page can still know the logged-in user.\n await this.bestEffortJwt(authHeader);\n this.reconcileWireTrust(/*callerVerified*/ false);\n this.rethrowDeferredBodyError();\n return nextFilter.invoke(meta);\n }\n\n switch (mode.kind) {\n case 'jwt':\n await this.enforceJwt(authHeader, mode.requirement);\n break;\n case 'oidc':\n await this.enforceOidc(authHeader, mode.callers);\n break;\n case 'shared-secret':\n this.enforceSharedSecret(\n this.credential(authHeader, SHARED_SECRET_SCHEME),\n mode.secretKey,\n );\n break;\n case 'webhook':\n await this.enforceWebhook(mode.name, meta);\n break;\n case 'apikey':\n await this.enforceApiKey(mode.regime, meta);\n break;\n case 'local-only':\n this.enforceLocalOnly(meta);\n break;\n }\n this.applySurface(mode);\n this.reconcileWireTrust(AuthFilter.verifiesCaller(mode));\n this.rethrowDeferredBodyError();\n return nextFilter.invoke(meta);\n }\n\n /**\n * Stamp WHICH KIND OF CALLER this is, from the auth mode that matched — `@WpAuthJwt` is a browser\n * GUI, `@WpAuthApiKey` is an external partner. See {@link Surface}.\n *\n * SET AT THE EDGE ONLY. The surface belongs to the ORIGINAL caller, so a hop that already has one\n * INHERITS it unchanged and this returns immediately. Two different callers put a value there\n * before this runs, and neither may be overwritten:\n *\n * - the MCP bridge, which stamps `llm` before invoking a `@WpMcpAuthJwt` tool through `ApiFactory`\n * — that in-process hop still runs this filter, and its HTTP auth mode says nothing about the\n * model on the other end;\n * - a `@WpAuthOidc` service-to-service hop, whose caller's surface arrived on the wire. OIDC is an\n * internal hop, NOT a surface of its own, so it deliberately maps to `undefined` below and the\n * propagated value survives.\n *\n * Running BEFORE {@link reconcileWireTrust} is what makes the trusted-header rule work for this\n * key: on a `@WpAuthJwt` edge a caller who also SENDS `x-wp-surface` must match the value the\n * framework just derived, and on a public route nothing vouches for it at all, so it is a 401.\n */\n private applySurface(mode: AuthMode): void {\n if (RequestContext.getTrusted(WebpiecesCoreHeaders.SURFACE) !== undefined) {\n return;\n }\n const surface = AuthFilter.surfaceFor(mode);\n if (surface !== undefined) {\n RequestContext.putTrusted(WebpiecesCoreHeaders.SURFACE, surface);\n }\n }\n\n /**\n * The auth mode -> {@link Surface} table, and the ONE place it is written down.\n *\n * `undefined` means \"this mode is not a surface\", which is a different statement from \"unknown\":\n * `oidc` and `shared-secret` are internal hops that RELAY somebody else's surface, `webhook`'s\n * sender is a vendor with no webpieces surface at all, and `local-only` is a developer's own box.\n *\n * An exhaustive switch with NO `default`, like {@link verifiesCaller} and\n * `DestinationTrust.forAuthMode`: a new AuthMode kind must fail to compile here rather than\n * silently landing on `gui` (which would publish 266 to a partner) or on `public-api` (which would\n * publish a real 4xx to a GUI).\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors verifiesCaller)\n private static surfaceFor(mode: AuthMode): Surface | undefined {\n switch (mode.kind) {\n case 'jwt':\n return 'gui';\n case 'apikey':\n return 'public-api';\n case 'oidc':\n case 'shared-secret':\n case 'webhook':\n case 'local-only':\n case 'public':\n return undefined;\n }\n }\n\n /**\n * `@WpAuthWebhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the\n * VENDOR's own validator. Three ways to fail, all 401, all before the controller is entered:\n *\n * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior;\n * an unverified webhook must never be waved through because wiring was forgotten.\n * 2. NO raw request — the transport kept no bytes. `assertEveryWebhookEndpointRetainsRawBody`\n * normally makes this a startup error, so reaching it means either a hand-registered route or\n * an in-process caller (a spec) that published an HttpRequest with no {@link RawRequest}. The\n * message says which fix applies rather than leaving a bare 401.\n * 3. The hook threw — the signature did not verify.\n *\n * Case 2 is checked HERE and nowhere else. {@link hasRawBytes} narrows the request to\n * {@link RawHttpRequest} at this one gate, so the hook's signature promises `raw` is present and\n * no vendor implementation ever writes `raw!` or a guard of its own.\n */\n private async enforceWebhook(name: string, meta: MethodMeta): Promise<void> {\n if (!this.webhookAuthCallback) {\n log.warn(\n `Refusing @WpAuthWebhook('${name}') endpoint ${meta.routeMeta.path}: no WebhookAuthCallback is bound. ` +\n `Bind one (options.bind(WEBHOOK_AUTH_CALLBACK).to(YourWebhookAuthCallback)) to enable webhook verification.`,\n );\n throw new ApiUnauthorizedError('Webhook auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n if (!this.hasRawBytes(request)) {\n log.warn(\n `Refusing @WpAuthWebhook('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +\n `no raw bytes. Declare @Endpoint(POST, path, WRITE, EXTERNAL, { calledBy: '${name}', rawBody: true }); a ` +\n `spec driving this route in-process must publish an HttpRequest built with a RawRequest.`,\n );\n throw new ApiUnauthorizedError(\n 'Webhook signature cannot be verified: no raw request was retained',\n );\n }\n // Throws ApiUnauthorizedError to deny. On success the vendor account the signature proved is\n // stamped through the SAME path a jwt or api-key caller takes.\n const caller = await this.webhookAuthCallback.verifyWebhook(name, request);\n this.applyAuthenticatedCaller(caller);\n }\n\n /**\n * The ONE place the framework decides a request carries the verbatim bytes. A TYPE PREDICATE, so\n * the `true` branch hands {@link enforceWebhook} a {@link RawHttpRequest} with no cast and no\n * non-null assertion — the bad state stops being representable past this line rather than being\n * re-thrown about by every hook.\n */\n private hasRawBytes(request: HttpRequest | undefined): request is RawHttpRequest {\n return request?.raw !== undefined;\n }\n\n /**\n * `@WpAuthApiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound\n * headers and let it look the CUSTOMER's key up. The declared `credentials` are NOT read here — they\n * describe the contract for generators; the hook owns extraction. Three ways to fail, all 401, all\n * before the controller:\n *\n * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior; an\n * unverified partner request must never be waved through because wiring was forgotten.\n * 2. NO inbound request in scope — there are no headers to read, so there is nothing to verify. That\n * means a caller drove this route without publishing an HttpRequest; the message says so rather\n * than leaving a bare 401.\n * 3. The hook threw — the key, or the key/organization pair, did not check out.\n *\n * On success the hook's {@link AuthenticatedCaller} is stamped exactly as a jwt parse's is, which is\n * what puts the resolved organization into `RequestContext` for every downstream repository call.\n */\n private async enforceApiKey(regime: string, meta: MethodMeta): Promise<void> {\n if (!this.apiKeyHook) {\n log.warn(\n `Refusing @WpAuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no ApiKeyHook is bound. ` +\n `Bind one (options.bind(API_KEY_HOOK).to(YourApiKeyHook)) to enable api-key verification.`,\n );\n throw new ApiUnauthorizedError('API-key auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n if (!request) {\n log.warn(\n `Refusing @WpAuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no inbound HttpRequest is ` +\n `in scope, so the hook has no headers to read. A spec driving this route in-process must ` +\n `publish an HttpRequest carrying the api-key headers.`,\n );\n throw new ApiUnauthorizedError(\n 'API key cannot be verified: no inbound request was published',\n );\n }\n // Throws ApiUnauthorizedError to deny. The hook gets the WHOLE request so it can cross-check\n // the key against a second header (the organization the customer is acting for).\n const caller = await this.apiKeyHook.verifyApiKey(regime, request);\n this.applyAuthenticatedCaller(caller);\n }\n\n /**\n * A body that failed to parse is held on the {@link RawRequest} and surfaces HERE, after auth, as\n * the 400 it always was — never before it.\n *\n * The order is the whole point. A malformed body from an unauthenticated caller must answer 401,\n * because \"your JSON was bad\" also says \"I got past auth\", and on a webhook endpoint — whose url\n * is public by construction — that is a free oracle for anyone probing. Parsing first made the\n * framework hand that out for nothing.\n *\n * Only routes that retain raw bytes can defer at all; every other route still fails at parse time\n * in the transport, exactly as before.\n */\n private rethrowDeferredBodyError(): void {\n const parseError = RequestContext.getRequest()?.raw?.bodyParseError;\n if (parseError) {\n throw new ApiBadRequestError(\n 'Request body is not valid JSON',\n undefined,\n undefined,\n parseError,\n );\n }\n }\n\n /**\n * Does this mode authenticate the CALLER ITSELF (as opposed to a user, or nobody)? The INBOUND\n * twin of {@link DestinationTrust.forAuthMode}, and deliberately the same question: the client\n * omits trusted keys for a destination that cannot verify it, and the server rejects trusted keys\n * on a route that cannot verify the sender. One rule, two ends — if they disagreed, every call\n * would fail with a 401 that looks like a framework bug.\n *\n * - `oidc` / `shared-secret` → TRUE. An internal service is on the other end and the trusted\n * context it forwarded may be believed. This is what makes cross-service identity propagation\n * work.\n * - `jwt` / `public` → FALSE. A user JWT proves who the USER is; the SENDER is still whoever\n * holds the token, i.e. a browser.\n * - `local-only` → FALSE. It verifies WHERE WE ARE RUNNING, not who is calling — anything on\n * localhost reaches it, and it has no authenticator, so nothing can ever vouch for an inbound\n * trusted header. Any such header therefore rejects the request, which is exactly right.\n *\n * - `apikey` → FALSE. See the comment on that branch: the sender is a CUSTOMER.\n *\n * An exhaustive switch with NO `default`, returning on every branch: a new AuthMode kind is a\n * COMPILE error here (TS7030, no ending return) rather than silently landing on one posture. The\n * boolean expression this replaced defaulted every future mode to \"not verified\" — the safe\n * answer, but arrived at by accident rather than by decision.\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors DestinationTrust.forAuthMode)\n private static verifiesCaller(mode: AuthMode): boolean {\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return true;\n case 'jwt':\n case 'public':\n // `webhook` DOES authenticate its sender — but the sender is an outside VENDOR, not a\n // peer in this repo, and a vendor neither speaks nor forwards webpieces context headers.\n // So there is no forwarded identity to believe, and admitting one would mean trusting a\n // key a vendor's payload could carry. Same answer as the OUTBOUND half\n // (DestinationTrust.forAuthMode), which is the invariant that keeps the two ends agreeing.\n case 'webhook':\n // `apikey` authenticates the SENDER — but the sender is a CUSTOMER's codebase, not a peer\n // service in this repo, so its forwarded trusted context is exactly what must NOT be\n // believed: admitting it would let a partner assert another customer's org id on the wire.\n // The hook's OWN derived entries still land (applyAuthenticatedCaller), and reconcileWireTrust then\n // admits an inbound trusted header only when the hook independently derived the same value.\n case 'apikey':\n case 'local-only':\n return false;\n }\n }\n\n /**\n * Decide what happens to the trusted keys that arrived on the WIRE and were held back by\n * {@link PendingWireTrust} (read that class for why they are held rather than written).\n *\n * `callerVerified` — the endpoint authenticated the SENDER **as a peer service** (`@WpAuthOidc`,\n * `@WpAuthSharedSecret`).\n * The sender is a service we trust, this is the service-to-service hop, and its forwarded\n * identity is admitted as-is. This is the case that makes propagating a verified userId across\n * internal services work.\n *\n * Otherwise the sender is a browser or anyone else with curl, and the ONLY acceptable inbound\n * trusted value is one the authenticator independently derived to the same value. Everything\n * else is rejected — see {@link requireVouched}.\n *\n * Runs AFTER the mode enforcement above, because that is what stamps the authenticator's own\n * values (`applyAuthenticatedCaller`); comparing before it ran would compare against nothing.\n */\n private reconcileWireTrust(callerVerified: boolean): void {\n const pending = PendingWireTrust.takeAll();\n for (const item of pending) {\n if (callerVerified) {\n RequestContext.putTrusted(item.key, item.value);\n } else {\n this.requireVouched(item);\n }\n }\n }\n\n /**\n * On a browser-reachable route, an inbound trusted header must match what the authenticator\n * itself derived, or the request dies. Both failure shapes are rejections, not repairs:\n *\n * - DIFFERENT value — the caller said `alice`, the credential says `bob`. Silently letting the\n * credential win is not safe, because upstream rate limiters commonly bucket on the header\n * rather than the token: the request was already counted against the wrong principal, so\n * every forged header would be a free rate-limit bypass. No honest caller contradicts its own\n * credential.\n * - NOTHING vouched for it — nobody derived this key at all, so there is no evidence behind a\n * value a stranger typed. This is the common case, not the exotic one: the framework's\n * {@link DefaultJwtHook} stamps NO entries, and an app hook (jwt or api-key) only stamps the keys it can prove,\n * so any other trusted key a caller sends lands here.\n *\n * The pending value is discarded either way — the throw is what leaves the request.\n */\n private requireVouched(item: PendingTrustedValue): void {\n const vouched = RequestContext.getTrusted(item.key);\n if (vouched === item.value) {\n return;\n }\n log.error(\n `Rejecting inbound '${item.key.httpHeader}': it is a TRUSTED context key, this route does ` +\n `not authenticate its caller, and the credential ` +\n (vouched === undefined\n ? 'vouched for no such value'\n : 'derived a different value') +\n '.',\n );\n throw new ApiUnauthorizedError(\n `Header '${item.key.httpHeader}' cannot be supplied by the caller on this endpoint`,\n );\n }\n\n /**\n * `@WpAuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the\n * endpoint did not exist.\n *\n * WHY 404 AND NOT THE 403 APPS HAND-ROLLED. Off-local the route is not registered at all\n * (`ApiRoutingFactory` skips it), so the ordinary way to reach this path already answers 404. A\n * 403 from here would be a DIFFERENT answer from the same framework for the same endpoint, and\n * the difference is itself the leak: 403 confirms \"this path exists in production, you merely\n * lack permission\", which is a map of the dev-only surface for anyone probing. A local-only\n * endpoint should not admit it exists. Both gates therefore return the same 404, and this one is\n * the backstop for routes registered by hand through `RouteBuilder` rather than by\n * `ApiRoutingFactory`.\n *\n * The log line names WHICH reason applies, because \"you are deployed\" and \"nobody declared a\n * locality\" have completely different fixes and both look like a bare 404 from outside.\n */\n private enforceLocalOnly(meta: MethodMeta): void {\n if (RuntimeLocality.isLocalDevelopment()) {\n return;\n }\n log.warn(\n `Refusing @WpAuthLocalOnly endpoint ${meta.routeMeta.path}: ` +\n (RuntimeLocality.isDeclared()\n ? 'this process declared itself DEPLOYED.'\n : 'no startup declared a RuntimeLocality, so this process is treated as DEPLOYED. ' +\n 'Pass the locality into RuntimeSetupOptions if this really is a developer machine.'),\n );\n // Same shape as an unregistered route — see the method doc for why this is not a 403.\n throw new ApiEndpointNotFoundError(`No endpoint at ${meta.routeMeta.path}`);\n }\n\n private async enforceJwt(\n header: string | undefined,\n requirement: JwtRequirement,\n ): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new ApiUnauthorizedError('Authentication required');\n }\n if (!this.jwtHook) {\n throw new ApiUnauthorizedError('User-JWT auth is not enabled on this server');\n }\n const caller = await this.jwtHook.parseJwt(token); // AUTHENTICATE — throws ApiUnauthorizedError if invalid\n this.applyAuthenticatedCaller(caller);\n await this.jwtHook.authorizeJwt(caller, requirement); // AUTHORIZE — app policy; throws ApiForbiddenError to deny\n }\n\n private async enforceOidc(header: string | undefined, callers: string[]): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new ApiUnauthorizedError('Missing OIDC bearer token for @WpAuthOidc endpoint');\n }\n // App-bound OidcHook overrides the caller policy; otherwise the framework default runs directly.\n if (this.oidcHook) {\n await this.oidcHook.verifyOidc(token, callers);\n } else {\n await this.oidcVerifier.verify(token, callers);\n }\n }\n\n /** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */\n private enforceSharedSecret(provided: string | undefined, secretKey: string): void {\n const accepted = this.authConfig?.sharedSecrets[secretKey];\n if (!accepted || !provided || !this.matchesEither(provided, accepted)) {\n throw new ApiUnauthorizedError(\n 'Invalid shared secret for @WpAuthSharedSecret endpoint',\n );\n }\n }\n\n /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */\n private matchesEither(provided: string, accepted: SharedSecrets): boolean {\n return (\n (accepted.secret1 !== '' && this.constantTimeEquals(provided, accepted.secret1)) ||\n (accepted.secret2 !== '' && this.constantTimeEquals(provided, accepted.secret2))\n );\n }\n\n /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */\n private async bestEffortJwt(header: string | undefined): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!this.jwtHook || !token) {\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort on a public route: a bad/absent token just means \"not logged in\", must not fail the request\n try {\n this.applyAuthenticatedCaller(await this.jwtHook.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(\n 'Best-effort JWT parse on a public endpoint failed (treating as anonymous): ',\n error,\n );\n }\n }\n\n /**\n * Stamp the authenticated caller's context entries + the caller itself into the RequestContext.\n * ONE path for all three authenticating hooks — jwt, api-key and webhook — so a vendor hook that\n * proved which account a payload belongs to seeds context exactly as a JwtHook does.\n */\n private applyAuthenticatedCaller(caller: AuthenticatedCaller): void {\n for (const entry of caller.entries) {\n // ContextTuple.key is a TRUSTED key by type, so this is the one sanctioned write of a\n // proven identity: the app's hook derived it from a credential we just verified.\n RequestContext.putTrusted(entry.key, entry.value);\n }\n // A real TRUSTED ContextKey, not a raw string slot: the caller IS the framework's own proof,\n // so it is written with the same typed verb every other proven value goes through.\n RequestContext.putTrusted(AUTHENTICATED_CALLER_KEY, caller);\n }\n\n /**\n * The credential value IF the header carries the expected scheme, else undefined.\n *\n * Strict: a bare value with no scheme, or a value under the WRONG scheme (a shared secret sent\n * where a JWT is expected), yields undefined and the caller 401s.\n */\n private credential(header: string | undefined, scheme: string): string | undefined {\n if (!header) {\n return undefined;\n }\n const prefix = `${scheme} `;\n return header.startsWith(prefix) ? header.substring(prefix.length) : undefined;\n }\n\n private constantTimeEquals(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'utf8');\n const bufB = Buffer.from(b, 'utf8');\n if (bufA.length !== bufB.length) {\n return false;\n }\n return timingSafeEqual(bufA, bufB);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AuthFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-routing/src/filters/AuthFilter.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport {\n provideFrameworkSingleton,\n HttpRequest,\n RawHttpRequest,\n RequestContext,\n} from '@webpieces/core-context';\nimport {\n AuthMode,\n ApiEndpointNotFoundError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n JwtRequirement,\n LogManager,\n RuntimeLocality,\n Surface,\n toError,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { Filter, Service } from '@webpieces/core-util';\nimport { WpResponse } from '../WpResponse';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AUTH_CONFIG, SharedSecrets } from '../AuthConfig';\nimport {\n ApiKeyHook,\n API_KEY_HOOK,\n JwtHook,\n JWT_HOOK,\n OidcHook,\n OIDC_HOOK,\n WebhookAuthCallback,\n WEBHOOK_AUTH_CALLBACK,\n} from '../AuthHooks';\nimport { AuthenticatedCallerContext } from '../AuthenticatedCallerContext';\nimport { InvocationAuthentication } from '../InvocationAuthentication';\nimport { DefaultOidcVerifier } from '../DefaultOidcVerifier';\n\nconst log = LogManager.getLogger('AuthFilter');\n\n/**\n * The ONE credential header, read straight off the inbound HttpRequest.\n *\n * Deliberately NOT a ContextKey: a ContextKey with an httpHeader is a TRANSFERRED key, which would\n * put the caller's credential into RequestContext and hence onto every outbound call this service\n * makes, and onto every Cloud Task it enqueues. A credential belongs to ONE request hop.\n */\nconst AUTHORIZATION_HEADER = 'authorization';\n\n/**\n * The scheme (first word of the Authorization value) names WHICH credential follows, so a secret\n * can never be mistaken for a token, nor accepted where the other was expected:\n *\n * Authorization: Bearer <user JWT | service OIDC token>\n * Authorization: Webpieces <@WpAuthSharedSecret value>\n *\n * The scheme is REQUIRED. A bare value with no scheme is rejected.\n */\nconst BEARER_SCHEME = 'Bearer';\nconst SHARED_SECRET_SCHEME = 'Webpieces';\n\n/**\n * AuthFilter - the ONE framework auth filter, auto-installed just below the error filter on every\n * route. It is TRANSPORT-NEUTRAL: it reads the raw credential from the {@link HttpRequest} in\n * RequestContext (never express), so the SAME check runs over HTTP and via createApiClient.\n *\n * It enforces the endpoint's AuthMode from separately-bound pieces, each OPTIONAL except the OIDC\n * default:\n * - shared-secret → constant-time compare vs the {@link AuthConfig} secret VALUE (state). No\n * AuthConfig bound → no accepted secret → fail fast (401).\n * - jwt → the bound {@link JwtHook} (`parseJwt` + `authorizeJwt`, both awaited — an app's\n * strategy may reach a JWKS or a datastore). No JwtHook bound → \"not enabled\"\n * (401): JWT needs an app secret + payload shape.\n * - oidc → the bound {@link OidcHook} if any, else the framework {@link DefaultOidcVerifier}\n * run DIRECTLY — so a server that wires NOTHING still verifies Google OIDC.\n * - webhook → the bound {@link WebhookAuthCallback} verifies the VENDOR's signature over the retained\n * raw request. No WebhookAuthCallback bound → 401, like jwt: an unverified webhook is\n * never waved through because wiring was forgotten.\n * - apikey → the bound {@link ApiKeyHook} looks the CUSTOMER's key up (async, over the whole\n * header set) and returns the context to seed. No ApiKeyHook bound → 401, like jwt.\n * - public → BEST-EFFORT jwt parse (only if a JwtHook is bound): stamp the user's context so\n * a logged-out page still knows who is logged in; never fails.\n * - local-only → serve only when {@link RuntimeLocality} says this process is a developer's\n * machine; otherwise 404, indistinguishable from the route not existing (which,\n * off-local, it does not — `ApiRoutingFactory` never registered it).\n *\n * Zero wiring = OIDC just works; an app only binds the hooks it actually uses.\n */\n@provideFrameworkSingleton()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\nexport class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n private readonly callerContext = new AuthenticatedCallerContext();\n\n constructor(\n // Framework default, always available — verifies Google OIDC with zero app wiring.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- AuthFilter is DI-resolved via the esbuild/vitest path, which elides type-only imports (no design:paramtypes), so every param needs its explicit token\n @inject(DefaultOidcVerifier) private readonly oidcVerifier: DefaultOidcVerifier,\n // @optional: only bind an AuthConfig to enable @WpAuthSharedSecret endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(AUTH_CONFIG) private readonly authConfig?: AuthConfig,\n // @optional: only bind a JwtHook to enable @WpAuthJwt endpoints.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(JWT_HOOK) private readonly jwtHook?: JwtHook<never>,\n // @optional: only bind an OidcHook to OVERRIDE the DefaultOidcVerifier caller policy.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(OIDC_HOOK) private readonly oidcHook?: OidcHook,\n // @optional: only bind a WebhookAuthCallback to enable @WpAuthWebhook endpoints. Unbound = every such\n // endpoint 401s, which is the ONE default that must not be the other way round.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional()\n @inject(WEBHOOK_AUTH_CALLBACK)\n private readonly webhookAuthCallback?: WebhookAuthCallback,\n // @optional: only bind an ApiKeyHook to enable @WpAuthApiKey endpoints. Unbound = every such\n // endpoint 401s, for the same reason as the webhook hook above.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- see above: explicit token required for DI-resolved param\n @optional() @inject(API_KEY_HOOK) private readonly apiKeyHook?: ApiKeyHook,\n ) {\n super();\n }\n\n // webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\n override async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n const mode = meta.routeMeta.authMeta?.mode;\n const authHeader = meta.invocationAuthentication\n ? `Bearer ${meta.invocationAuthentication.token}`\n : RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);\n\n if (!mode || mode.kind === 'public') {\n // Public: best-effort parse so a logged-out page can still know the logged-in user.\n await this.bestEffortJwt(authHeader);\n this.callerContext.reconcileWireTrust(/*callerVerified*/ false);\n this.rethrowDeferredBodyError();\n return nextFilter.invoke(meta);\n }\n\n switch (mode.kind) {\n case 'jwt':\n await this.enforceJwt(authHeader, mode.requirement, meta.invocationAuthentication);\n break;\n case 'oidc':\n await this.enforceOidc(authHeader, mode.callers);\n break;\n case 'shared-secret':\n this.enforceSharedSecret(\n this.credential(authHeader, SHARED_SECRET_SCHEME),\n mode.secretKey,\n );\n break;\n case 'webhook':\n await this.enforceWebhook(mode.name, meta);\n break;\n case 'apikey':\n await this.enforceApiKey(mode.regime, meta);\n break;\n case 'local-only':\n this.enforceLocalOnly(meta);\n break;\n }\n this.applySurface(mode);\n this.callerContext.reconcileWireTrust(AuthFilter.verifiesCaller(mode));\n this.rethrowDeferredBodyError();\n return nextFilter.invoke(meta);\n }\n\n /**\n * Stamp WHICH KIND OF CALLER this is, from the auth mode that matched — `@WpAuthJwt` is a browser\n * GUI, `@WpAuthApiKey` is an external partner. See {@link Surface}.\n *\n * SET AT THE EDGE ONLY. The surface belongs to the ORIGINAL caller, so a hop that already has one\n * INHERITS it unchanged and this returns immediately. Two different callers put a value there\n * before this runs, and neither may be overwritten:\n *\n * - the MCP bridge, which stamps `llm` before invoking a `@WpMcpAuthJwt` tool through `ApiFactory`\n * — that in-process hop still runs this filter, and its HTTP auth mode says nothing about the\n * model on the other end;\n * - a `@WpAuthOidc` service-to-service hop, whose caller's surface arrived on the wire. OIDC is an\n * internal hop, NOT a surface of its own, so it deliberately maps to `undefined` below and the\n * propagated value survives.\n *\n * Running BEFORE {@link reconcileWireTrust} is what makes the trusted-header rule work for this\n * key: on a `@WpAuthJwt` edge a caller who also SENDS `x-wp-surface` must match the value the\n * framework just derived, and on a public route nothing vouches for it at all, so it is a 401.\n */\n private applySurface(mode: AuthMode): void {\n if (RequestContext.getTrusted(WebpiecesCoreHeaders.SURFACE) !== undefined) {\n return;\n }\n const surface = AuthFilter.surfaceFor(mode);\n if (surface !== undefined) {\n RequestContext.putTrusted(WebpiecesCoreHeaders.SURFACE, surface);\n }\n }\n\n /**\n * The auth mode -> {@link Surface} table, and the ONE place it is written down.\n *\n * `undefined` means \"this mode is not a surface\", which is a different statement from \"unknown\":\n * `oidc` and `shared-secret` are internal hops that RELAY somebody else's surface, `webhook`'s\n * sender is a vendor with no webpieces surface at all, and `local-only` is a developer's own box.\n *\n * An exhaustive switch with NO `default`, like {@link verifiesCaller} and\n * `DestinationTrust.forAuthMode`: a new AuthMode kind must fail to compile here rather than\n * silently landing on `gui` (which would publish 266 to a partner) or on `public-api` (which would\n * publish a real 4xx to a GUI).\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors verifiesCaller)\n private static surfaceFor(mode: AuthMode): Surface | undefined {\n switch (mode.kind) {\n case 'jwt':\n return 'gui';\n case 'apikey':\n return 'public-api';\n case 'oidc':\n case 'shared-secret':\n case 'webhook':\n case 'local-only':\n case 'public':\n return undefined;\n }\n }\n\n /**\n * `@WpAuthWebhook(name)`: hand the app's {@link WebhookAuthCallback} the verbatim request and let it call the\n * VENDOR's own validator. Three ways to fail, all 401, all before the controller is entered:\n *\n * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior;\n * an unverified webhook must never be waved through because wiring was forgotten.\n * 2. NO raw request — the transport kept no bytes. `assertEveryWebhookEndpointRetainsRawBody`\n * normally makes this a startup error, so reaching it means either a hand-registered route or\n * an in-process caller (a spec) that published an HttpRequest with no {@link RawRequest}. The\n * message says which fix applies rather than leaving a bare 401.\n * 3. The hook threw — the signature did not verify.\n *\n * Case 2 is checked HERE and nowhere else. {@link hasRawBytes} narrows the request to\n * {@link RawHttpRequest} at this one gate, so the hook's signature promises `raw` is present and\n * no vendor implementation ever writes `raw!` or a guard of its own.\n */\n private async enforceWebhook(name: string, meta: MethodMeta): Promise<void> {\n if (!this.webhookAuthCallback) {\n log.warn(\n `Refusing @WpAuthWebhook('${name}') endpoint ${meta.routeMeta.path}: no WebhookAuthCallback is bound. ` +\n `Bind one (options.bind(WEBHOOK_AUTH_CALLBACK).to(YourWebhookAuthCallback)) to enable webhook verification.`,\n );\n throw new ApiUnauthorizedError('Webhook auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n if (!this.hasRawBytes(request)) {\n log.warn(\n `Refusing @WpAuthWebhook('${name}') endpoint ${meta.routeMeta.path}: the inbound request carries ` +\n `no raw bytes. Declare @Endpoint(POST, path, WRITE, EXTERNAL, { calledBy: '${name}', rawBody: true }); a ` +\n `spec driving this route in-process must publish an HttpRequest built with a RawRequest.`,\n );\n throw new ApiUnauthorizedError(\n 'Webhook signature cannot be verified: no raw request was retained',\n );\n }\n // Throws ApiUnauthorizedError to deny. On success the vendor account the signature proved is\n // stamped through the SAME path a jwt or api-key caller takes.\n const caller = await this.webhookAuthCallback.verifyWebhook(name, request);\n this.callerContext.publish(caller);\n }\n\n /**\n * The ONE place the framework decides a request carries the verbatim bytes. A TYPE PREDICATE, so\n * the `true` branch hands {@link enforceWebhook} a {@link RawHttpRequest} with no cast and no\n * non-null assertion — the bad state stops being representable past this line rather than being\n * re-thrown about by every hook.\n */\n private hasRawBytes(request: HttpRequest | undefined): request is RawHttpRequest {\n return request?.raw !== undefined;\n }\n\n /**\n * `@WpAuthApiKey(regime, credentials)`: hand the app's {@link ApiKeyHook} the regime name and the inbound\n * headers and let it look the CUSTOMER's key up. The declared `credentials` are NOT read here — they\n * describe the contract for generators; the hook owns extraction. Three ways to fail, all 401, all\n * before the controller:\n *\n * 1. NO hook bound — the endpoint is not enabled. Matches {@link JwtHook}'s documented behavior; an\n * unverified partner request must never be waved through because wiring was forgotten.\n * 2. NO inbound request in scope — there are no headers to read, so there is nothing to verify. That\n * means a caller drove this route without publishing an HttpRequest; the message says so rather\n * than leaving a bare 401.\n * 3. The hook threw — the key, or the key/organization pair, did not check out.\n *\n * On success the hook's {@link AuthenticatedCaller} is stamped exactly as a jwt parse's is, which is\n * what puts the resolved organization into `RequestContext` for every downstream repository call.\n */\n private async enforceApiKey(regime: string, meta: MethodMeta): Promise<void> {\n if (!this.apiKeyHook) {\n log.warn(\n `Refusing @WpAuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no ApiKeyHook is bound. ` +\n `Bind one (options.bind(API_KEY_HOOK).to(YourApiKeyHook)) to enable api-key verification.`,\n );\n throw new ApiUnauthorizedError('API-key auth is not enabled on this server');\n }\n const request = RequestContext.getRequest();\n if (!request) {\n log.warn(\n `Refusing @WpAuthApiKey('${regime}') endpoint ${meta.routeMeta.path}: no inbound HttpRequest is ` +\n `in scope, so the hook has no headers to read. A spec driving this route in-process must ` +\n `publish an HttpRequest carrying the api-key headers.`,\n );\n throw new ApiUnauthorizedError(\n 'API key cannot be verified: no inbound request was published',\n );\n }\n // Throws ApiUnauthorizedError to deny. The hook gets the WHOLE request so it can cross-check\n // the key against a second header (the organization the customer is acting for).\n const caller = await this.apiKeyHook.verifyApiKey(regime, request);\n this.callerContext.publish(caller);\n }\n\n /**\n * A body that failed to parse is held on the {@link RawRequest} and surfaces HERE, after auth, as\n * the 400 it always was — never before it.\n *\n * The order is the whole point. A malformed body from an unauthenticated caller must answer 401,\n * because \"your JSON was bad\" also says \"I got past auth\", and on a webhook endpoint — whose url\n * is public by construction — that is a free oracle for anyone probing. Parsing first made the\n * framework hand that out for nothing.\n *\n * Only routes that retain raw bytes can defer at all; every other route still fails at parse time\n * in the transport, exactly as before.\n */\n private rethrowDeferredBodyError(): void {\n const parseError = RequestContext.getRequest()?.raw?.bodyParseError;\n if (parseError) {\n throw new ApiBadRequestError(\n 'Request body is not valid JSON',\n undefined,\n undefined,\n parseError,\n );\n }\n }\n\n /**\n * Does this mode authenticate the CALLER ITSELF (as opposed to a user, or nobody)? The INBOUND\n * twin of {@link DestinationTrust.forAuthMode}, and deliberately the same question: the client\n * omits trusted keys for a destination that cannot verify it, and the server rejects trusted keys\n * on a route that cannot verify the sender. One rule, two ends — if they disagreed, every call\n * would fail with a 401 that looks like a framework bug.\n *\n * - `oidc` / `shared-secret` → TRUE. An internal service is on the other end and the trusted\n * context it forwarded may be believed. This is what makes cross-service identity propagation\n * work.\n * - `jwt` / `public` → FALSE. A user JWT proves who the USER is; the SENDER is still whoever\n * holds the token, i.e. a browser.\n * - `local-only` → FALSE. It verifies WHERE WE ARE RUNNING, not who is calling — anything on\n * localhost reaches it, and it has no authenticator, so nothing can ever vouch for an inbound\n * trusted header. Any such header therefore rejects the request, which is exactly right.\n *\n * - `apikey` → FALSE. See the comment on that branch: the sender is a CUSTOMER.\n *\n * An exhaustive switch with NO `default`, returning on every branch: a new AuthMode kind is a\n * COMPILE error here (TS7030, no ending return) rather than silently landing on one posture. The\n * boolean expression this replaced defaulted every future mode to \"not verified\" — the safe\n * answer, but arrived at by accident rather than by decision.\n */\n // webpieces-disable no-function-outside-class -- static pure mapping from the AuthMode union, kept beside its only caller (mirrors DestinationTrust.forAuthMode)\n private static verifiesCaller(mode: AuthMode): boolean {\n switch (mode.kind) {\n case 'oidc':\n case 'shared-secret':\n return true;\n case 'jwt':\n case 'public':\n // `webhook` DOES authenticate its sender — but the sender is an outside VENDOR, not a\n // peer in this repo, and a vendor neither speaks nor forwards webpieces context headers.\n // So there is no forwarded identity to believe, and admitting one would mean trusting a\n // key a vendor's payload could carry. Same answer as the OUTBOUND half\n // (DestinationTrust.forAuthMode), which is the invariant that keeps the two ends agreeing.\n case 'webhook':\n // `apikey` authenticates the SENDER — but the sender is a CUSTOMER's codebase, not a peer\n // service in this repo, so its forwarded trusted context is exactly what must NOT be\n // believed: admitting it would let a partner assert another customer's org id on the wire.\n // The hook's OWN derived entries still land (applyAuthenticatedCaller), and reconcileWireTrust then\n // admits an inbound trusted header only when the hook independently derived the same value.\n case 'apikey':\n case 'local-only':\n return false;\n }\n }\n\n /**\n * `@WpAuthLocalOnly`: serve only on a developer's machine, and off-local behave EXACTLY as if the\n * endpoint did not exist.\n *\n * WHY 404 AND NOT THE 403 APPS HAND-ROLLED. Off-local the route is not registered at all\n * (`ApiRoutingFactory` skips it), so the ordinary way to reach this path already answers 404. A\n * 403 from here would be a DIFFERENT answer from the same framework for the same endpoint, and\n * the difference is itself the leak: 403 confirms \"this path exists in production, you merely\n * lack permission\", which is a map of the dev-only surface for anyone probing. A local-only\n * endpoint should not admit it exists. Both gates therefore return the same 404, and this one is\n * the backstop for routes registered by hand through `RouteBuilder` rather than by\n * `ApiRoutingFactory`.\n *\n * The log line names WHICH reason applies, because \"you are deployed\" and \"nobody declared a\n * locality\" have completely different fixes and both look like a bare 404 from outside.\n */\n private enforceLocalOnly(meta: MethodMeta): void {\n if (RuntimeLocality.isLocalDevelopment()) {\n return;\n }\n log.warn(\n `Refusing @WpAuthLocalOnly endpoint ${meta.routeMeta.path}: ` +\n (RuntimeLocality.isDeclared()\n ? 'this process declared itself DEPLOYED.'\n : 'no startup declared a RuntimeLocality, so this process is treated as DEPLOYED. ' +\n 'Pass the locality into RuntimeSetupOptions if this really is a developer machine.'),\n );\n // Same shape as an unregistered route — see the method doc for why this is not a 403.\n throw new ApiEndpointNotFoundError(`No endpoint at ${meta.routeMeta.path}`);\n }\n\n private async enforceJwt(\n header: string | undefined,\n requirement: JwtRequirement,\n invocation?: InvocationAuthentication,\n ): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new ApiUnauthorizedError('Authentication required');\n }\n if (!this.jwtHook) {\n throw new ApiUnauthorizedError('User-JWT auth is not enabled on this server');\n }\n const caller = await this.jwtHook.parseJwt(token); // AUTHENTICATE — throws ApiUnauthorizedError if invalid\n invocation?.assertCaller(caller);\n if (!invocation) this.callerContext.publish(caller);\n await this.jwtHook.authorizeJwt(caller, requirement); // AUTHORIZE — app policy; throws ApiForbiddenError to deny\n }\n\n private async enforceOidc(header: string | undefined, callers: string[]): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new ApiUnauthorizedError('Missing OIDC bearer token for @WpAuthOidc endpoint');\n }\n // App-bound OidcHook overrides the caller policy; otherwise the framework default runs directly.\n if (this.oidcHook) {\n await this.oidcHook.verifyOidc(token, callers);\n } else {\n await this.oidcVerifier.verify(token, callers);\n }\n }\n\n /** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */\n private enforceSharedSecret(provided: string | undefined, secretKey: string): void {\n const accepted = this.authConfig?.sharedSecrets[secretKey];\n if (!accepted || !provided || !this.matchesEither(provided, accepted)) {\n throw new ApiUnauthorizedError(\n 'Invalid shared secret for @WpAuthSharedSecret endpoint',\n );\n }\n }\n\n /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */\n private matchesEither(provided: string, accepted: SharedSecrets): boolean {\n return (\n (accepted.secret1 !== '' && this.constantTimeEquals(provided, accepted.secret1)) ||\n (accepted.secret2 !== '' && this.constantTimeEquals(provided, accepted.secret2))\n );\n }\n\n /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */\n private async bestEffortJwt(header: string | undefined): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!this.jwtHook || !token) {\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort on a public route: a bad/absent token just means \"not logged in\", must not fail the request\n try {\n this.callerContext.publish(await this.jwtHook.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n log.debug(\n 'Best-effort JWT parse on a public endpoint failed (treating as anonymous): ',\n error,\n );\n }\n }\n\n /**\n * The credential value IF the header carries the expected scheme, else undefined.\n *\n * Strict: a bare value with no scheme, or a value under the WRONG scheme (a shared secret sent\n * where a JWT is expected), yields undefined and the caller 401s.\n */\n private credential(header: string | undefined, scheme: string): string | undefined {\n if (!header) {\n return undefined;\n }\n const prefix = `${scheme} `;\n return header.startsWith(prefix) ? header.substring(prefix.length) : undefined;\n }\n\n private constantTimeEquals(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'utf8');\n const bufB = Buffer.from(b, 'utf8');\n if (bufA.length !== bufB.length) {\n return false;\n }\n return timingSafeEqual(bufA, bufB);\n }\n}\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';
|
|
|
7
7
|
export { Routes, RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';
|
|
8
8
|
export { HttpRequest, RawHttpRequest, RawRequest } from '@webpieces/core-context';
|
|
9
9
|
export { WpResponse } from './WpResponse';
|
|
10
|
+
export { AuthenticatedCallerContext } from './AuthenticatedCallerContext';
|
|
11
|
+
export { InvocationAuthentication } from './InvocationAuthentication';
|
|
10
12
|
export { MethodMeta } from './MethodMeta';
|
|
11
13
|
export { RouteHandler } from './RouteHandler';
|
|
12
14
|
export { LogApiFilter } from './filters/LogApiFilter';
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.provideFrameworkSingleton = exports.provideSingletonDefaultForApi = exports.ROUTING_METADATA_KEYS = exports.SourceFile = exports.isDocumentDesign = exports.DocumentDesign = exports.EXTERNAL = exports.CRON = exports.CLOUDTASKS = exports.RPC = exports.WRITE = exports.WRITE_IDEMPOTENT = exports.READ = exports.POST = exports.GET = exports.METADATA_KEYS = exports.HttpContractMapper = exports.RouteMetadataFactory = exports.RouteMetadata = exports.AuthMeta = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.isRawBody = exports.isFormPost = exports.getHttpParameterDeclarations = exports.getEndpointOperation = exports.getEndpointHttpMethod = exports.getEndpointOptions = exports.getEndpoints = exports.getApiPath = exports.Queue = exports.PubSub = exports.Rpc = exports.WpAuthLocalOnly = exports.WpAuthWebhook = exports.WpAuthSharedSecret = exports.WpAuthOidc = exports.rolesRequired = exports.WpAuthJwt = exports.WpAuthPublic = exports.QueryParam = exports.PathParam = exports.Endpoint = exports.ApiPath = void 0;
|
|
4
|
-
exports.WEBPIECES_CONFIG_TOKEN = exports.WebpiecesConfig = exports.RuntimeSetupOptions = exports.setupRuntime = exports.WebpiecesRouterFactory = exports.WebpiecesRouter = exports.DefaultJwtMintRequest = exports.DefaultJwtHook = exports.DefaultOidcVerifier = exports.API_KEY_HOOK = exports.ApiKeyHook = exports.WEBHOOK_AUTH_CALLBACK = exports.WebhookAuthCallback = exports.OIDC_HOOK = exports.OidcHook = exports.JWT_HOOK = exports.MintedJwt = exports.JwtHook = exports.SharedSecrets = exports.AUTHENTICATED_CALLER_KEY = exports.AuthenticatedCaller = exports.AUTH_CONFIG = exports.AuthConfig = exports.ApiClient = exports.FilterMatcher = exports.LogApiFilter = exports.RouteHandler = exports.MethodMeta = exports.WpResponse = exports.RawRequest = exports.HttpRequest = exports.FilterDefinition = exports.RouteDefinition = exports.ApiRoutingFactory = exports.buildFrameworkModule = exports.provideFrameworkSingletonDefaultForApi = void 0;
|
|
4
|
+
exports.WEBPIECES_CONFIG_TOKEN = exports.WebpiecesConfig = exports.RuntimeSetupOptions = exports.setupRuntime = exports.WebpiecesRouterFactory = exports.WebpiecesRouter = exports.DefaultJwtMintRequest = exports.DefaultJwtHook = exports.DefaultOidcVerifier = exports.API_KEY_HOOK = exports.ApiKeyHook = exports.WEBHOOK_AUTH_CALLBACK = exports.WebhookAuthCallback = exports.OIDC_HOOK = exports.OidcHook = exports.JWT_HOOK = exports.MintedJwt = exports.JwtHook = exports.SharedSecrets = exports.AUTHENTICATED_CALLER_KEY = exports.AuthenticatedCaller = exports.AUTH_CONFIG = exports.AuthConfig = exports.ApiClient = exports.FilterMatcher = exports.LogApiFilter = exports.RouteHandler = exports.MethodMeta = exports.InvocationAuthentication = exports.AuthenticatedCallerContext = exports.WpResponse = exports.RawRequest = exports.HttpRequest = exports.FilterDefinition = exports.RouteDefinition = exports.ApiRoutingFactory = exports.buildFrameworkModule = exports.provideFrameworkSingletonDefaultForApi = void 0;
|
|
5
5
|
// Re-export API decorators from core-util for convenience
|
|
6
6
|
var core_util_1 = require("@webpieces/core-util");
|
|
7
7
|
Object.defineProperty(exports, "ApiPath", { enumerable: true, get: function () { return core_util_1.ApiPath; } });
|
|
@@ -80,6 +80,10 @@ Object.defineProperty(exports, "RawRequest", { enumerable: true, get: function (
|
|
|
80
80
|
// rather than a second spelling of it. Import them from '@webpieces/core-util'.
|
|
81
81
|
var WpResponse_1 = require("./WpResponse");
|
|
82
82
|
Object.defineProperty(exports, "WpResponse", { enumerable: true, get: function () { return WpResponse_1.WpResponse; } });
|
|
83
|
+
var AuthenticatedCallerContext_1 = require("./AuthenticatedCallerContext");
|
|
84
|
+
Object.defineProperty(exports, "AuthenticatedCallerContext", { enumerable: true, get: function () { return AuthenticatedCallerContext_1.AuthenticatedCallerContext; } });
|
|
85
|
+
var InvocationAuthentication_1 = require("./InvocationAuthentication");
|
|
86
|
+
Object.defineProperty(exports, "InvocationAuthentication", { enumerable: true, get: function () { return InvocationAuthentication_1.InvocationAuthentication; } });
|
|
83
87
|
var MethodMeta_1 = require("./MethodMeta");
|
|
84
88
|
Object.defineProperty(exports, "MethodMeta", { enumerable: true, get: function () { return MethodMeta_1.MethodMeta; } });
|
|
85
89
|
var RouteHandler_1 = require("./RouteHandler");
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/index.ts"],"names":[],"mappings":"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","sourcesContent":["// Re-export API decorators from core-util for convenience\nexport {\n ApiPath,\n Endpoint,\n PathParam,\n QueryParam,\n WpAuthPublic,\n WpAuthJwt,\n rolesRequired,\n WpAuthOidc,\n WpAuthSharedSecret,\n WpAuthWebhook,\n WpAuthLocalOnly,\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n getEndpointHttpMethod,\n getEndpointOperation,\n getHttpParameterDeclarations,\n isFormPost,\n isRawBody,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n AuthMeta,\n RouteMetadata,\n RouteMetadataFactory,\n HttpContractMapper,\n METADATA_KEYS,\n ValidateImplementation,\n GET,\n POST,\n READ,\n WRITE_IDEMPOTENT,\n WRITE,\n RPC,\n CLOUDTASKS,\n CRON,\n EXTERNAL,\n // @DocumentDesign moved to core-util (design-root marker, browser + Node);\n // re-exported here for back-compat.\n DocumentDesign,\n isDocumentDesign,\n} from '@webpieces/core-util';\nexport type {\n AuthMode,\n ApiKind,\n HttpMethod,\n EndpointOperation,\n EndpointKind,\n EndpointOptions,\n EndpointResponseType,\n} from '@webpieces/core-util';\n\n// Server-side routing decorators and utilities\nexport { SourceFile, ROUTING_METADATA_KEYS } from './decorators';\n\n// DI provider decorators moved to core-context; re-exported here for back-compat\nexport { provideSingletonDefaultForApi } from '@webpieces/core-context';\n// Framework-only DI registry (packages/** framework classes use these; see frameworkProvide.ts)\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonDefaultForApi,\n buildFrameworkModule,\n} from '@webpieces/core-context';\n\nexport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\n\n// Core routing types\nexport { Routes, RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';\n\n// The transport-neutral request type (defined in core-context; this is http-routing's\n// public request — a transport adapter builds one and the chain reads it from RequestContext).\nexport { HttpRequest, RawHttpRequest, RawRequest } from '@webpieces/core-context';\n\n// The INBOUND chain's response type. `Filter`, `Service` and `FilterChain` are NOT re-exported\n// here: they moved to @webpieces/core-util so the outbound client chain is the SAME abstraction\n// rather than a second spelling of it. Import them from '@webpieces/core-util'.\nexport { WpResponse } from './WpResponse';\nexport { MethodMeta } from './MethodMeta';\nexport { RouteHandler } from './RouteHandler';\n\n// LogApiFilter: the fixed OUTERMOST framework filter (auto-installed at 1,000,000 above\n// AuthFilter). Exported for reference/testing only — apps must NOT install it themselves.\nexport { LogApiFilter } from './filters/LogApiFilter';\n\n// RouteBuilderImpl (the route table + chain composer) is now INTERNAL — it is never\n// handed to upper layers. The express layer consumes ApiFactory.apiClients() instead.\n\n// Filter matching\nexport { FilterMatcher, HttpFilter } from './FilterMatcher';\n\n// The app's server-surface declaration: DI binding modules + route groups + headers.\nexport { AppModules, RouteModule } from './AppModules';\n\n// The public API-surface abstraction: declare routes/filters, get them back as ApiClient[].\nexport { ApiFactory } from './ApiFactory';\nexport { ApiClient, ApiClientProxy } from './ApiClient';\n\n// Auth: the app-provided, container-bound pieces the framework AuthFilter injects.\n// - AuthConfig: shared-secret STATE (@WpAuthSharedSecret values).\n// - JwtHook / OidcHook / WebhookAuthCallback / ApiKeyHook: OPTIONAL verification mechanisms\n// (bind only what you use; unbound means the matching endpoints 401, never open).\n// - DefaultOidcVerifier: the built-in Google OIDC verifier used when no OidcHook is bound.\nexport {\n AuthConfig,\n AUTH_CONFIG,\n AuthenticatedCaller,\n AUTHENTICATED_CALLER_KEY,\n SharedSecrets,\n} from './AuthConfig';\nexport {\n JwtHook,\n MintedJwt,\n JWT_HOOK,\n OidcHook,\n OIDC_HOOK,\n WebhookAuthCallback,\n WEBHOOK_AUTH_CALLBACK,\n ApiKeyHook,\n API_KEY_HOOK,\n} from './AuthHooks';\nexport { DefaultOidcVerifier } from './DefaultOidcVerifier';\n// DefaultJwtHook: batteries-included HS256 JwtHook — `new DefaultJwtHook(secret)` and go.\nexport { DefaultJwtHook, DefaultJwtMintRequest } from './DefaultJwtHook';\n\n// Above-boundary context setup shared by every transport adapter.\n\n// Node-only router (the express-free heart: container + filter chain + in-process client)\nexport { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';\n\n// The ONE transport-free startup sequence (headers → logging → router → routes) → ApiFactory.\n// Reusable by any company/app and any framework adapter; a company wraps it with its own headers.\nexport { setupRuntime, RuntimeSetupOptions } from './setupRuntime';\n\n// Server configuration\nexport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/index.ts"],"names":[],"mappings":"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","sourcesContent":["// Re-export API decorators from core-util for convenience\nexport {\n ApiPath,\n Endpoint,\n PathParam,\n QueryParam,\n WpAuthPublic,\n WpAuthJwt,\n rolesRequired,\n WpAuthOidc,\n WpAuthSharedSecret,\n WpAuthWebhook,\n WpAuthLocalOnly,\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n getEndpointOptions,\n getEndpointHttpMethod,\n getEndpointOperation,\n getHttpParameterDeclarations,\n isFormPost,\n isRawBody,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n AuthMeta,\n RouteMetadata,\n RouteMetadataFactory,\n HttpContractMapper,\n METADATA_KEYS,\n ValidateImplementation,\n GET,\n POST,\n READ,\n WRITE_IDEMPOTENT,\n WRITE,\n RPC,\n CLOUDTASKS,\n CRON,\n EXTERNAL,\n // @DocumentDesign moved to core-util (design-root marker, browser + Node);\n // re-exported here for back-compat.\n DocumentDesign,\n isDocumentDesign,\n} from '@webpieces/core-util';\nexport type {\n AuthMode,\n ApiKind,\n HttpMethod,\n EndpointOperation,\n EndpointKind,\n EndpointOptions,\n EndpointResponseType,\n} from '@webpieces/core-util';\n\n// Server-side routing decorators and utilities\nexport { SourceFile, ROUTING_METADATA_KEYS } from './decorators';\n\n// DI provider decorators moved to core-context; re-exported here for back-compat\nexport { provideSingletonDefaultForApi } from '@webpieces/core-context';\n// Framework-only DI registry (packages/** framework classes use these; see frameworkProvide.ts)\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonDefaultForApi,\n buildFrameworkModule,\n} from '@webpieces/core-context';\n\nexport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\n\n// Core routing types\nexport { Routes, RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';\n\n// The transport-neutral request type (defined in core-context; this is http-routing's\n// public request — a transport adapter builds one and the chain reads it from RequestContext).\nexport { HttpRequest, RawHttpRequest, RawRequest } from '@webpieces/core-context';\n\n// The INBOUND chain's response type. `Filter`, `Service` and `FilterChain` are NOT re-exported\n// here: they moved to @webpieces/core-util so the outbound client chain is the SAME abstraction\n// rather than a second spelling of it. Import them from '@webpieces/core-util'.\nexport { WpResponse } from './WpResponse';\nexport { AuthenticatedCallerContext } from './AuthenticatedCallerContext';\nexport { InvocationAuthentication } from './InvocationAuthentication';\nexport { MethodMeta } from './MethodMeta';\nexport { RouteHandler } from './RouteHandler';\n\n// LogApiFilter: the fixed OUTERMOST framework filter (auto-installed at 1,000,000 above\n// AuthFilter). Exported for reference/testing only — apps must NOT install it themselves.\nexport { LogApiFilter } from './filters/LogApiFilter';\n\n// RouteBuilderImpl (the route table + chain composer) is now INTERNAL — it is never\n// handed to upper layers. The express layer consumes ApiFactory.apiClients() instead.\n\n// Filter matching\nexport { FilterMatcher, HttpFilter } from './FilterMatcher';\n\n// The app's server-surface declaration: DI binding modules + route groups + headers.\nexport { AppModules, RouteModule } from './AppModules';\n\n// The public API-surface abstraction: declare routes/filters, get them back as ApiClient[].\nexport { ApiFactory } from './ApiFactory';\nexport { ApiClient, ApiClientProxy } from './ApiClient';\n\n// Auth: the app-provided, container-bound pieces the framework AuthFilter injects.\n// - AuthConfig: shared-secret STATE (@WpAuthSharedSecret values).\n// - JwtHook / OidcHook / WebhookAuthCallback / ApiKeyHook: OPTIONAL verification mechanisms\n// (bind only what you use; unbound means the matching endpoints 401, never open).\n// - DefaultOidcVerifier: the built-in Google OIDC verifier used when no OidcHook is bound.\nexport {\n AuthConfig,\n AUTH_CONFIG,\n AuthenticatedCaller,\n AUTHENTICATED_CALLER_KEY,\n SharedSecrets,\n} from './AuthConfig';\nexport {\n JwtHook,\n MintedJwt,\n JWT_HOOK,\n OidcHook,\n OIDC_HOOK,\n WebhookAuthCallback,\n WEBHOOK_AUTH_CALLBACK,\n ApiKeyHook,\n API_KEY_HOOK,\n} from './AuthHooks';\nexport { DefaultOidcVerifier } from './DefaultOidcVerifier';\n// DefaultJwtHook: batteries-included HS256 JwtHook — `new DefaultJwtHook(secret)` and go.\nexport { DefaultJwtHook, DefaultJwtMintRequest } from './DefaultJwtHook';\n\n// Above-boundary context setup shared by every transport adapter.\n\n// Node-only router (the express-free heart: container + filter chain + in-process client)\nexport { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';\n\n// The ONE transport-free startup sequence (headers → logging → router → routes) → ApiFactory.\n// Reusable by any company/app and any framework adapter; a company wraps it with its own headers.\nexport { setupRuntime, RuntimeSetupOptions } from './setupRuntime';\n\n// Server configuration\nexport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\n"]}
|