@webpieces/http-routing 0.3.316 → 0.3.318
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 +3 -3
- package/src/ApiClientFactory.d.ts +8 -3
- package/src/ApiClientFactory.js +23 -21
- package/src/ApiClientFactory.js.map +1 -1
- package/src/filters/AuthFilter.d.ts +8 -1
- package/src/filters/AuthFilter.js +34 -8
- package/src/filters/AuthFilter.js.map +1 -1
- package/src/filters/ErrorLogFilter.d.ts +1 -1
- package/src/filters/ErrorLogFilter.js +1 -1
- package/src/filters/ErrorLogFilter.js.map +1 -1
- package/src/index.d.ts +0 -2
- package/src/index.js +2 -7
- package/src/index.js.map +1 -1
- package/src/fillContext.d.ts +0 -11
- package/src/fillContext.js +0 -35
- package/src/fillContext.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-routing",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.318",
|
|
4
4
|
"description": "Decorator-based routing with auto-wiring for WebPieces",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
25
|
-
"@webpieces/core-context": "0.3.
|
|
26
|
-
"@webpieces/core-util": "0.3.
|
|
25
|
+
"@webpieces/core-context": "0.3.318",
|
|
26
|
+
"@webpieces/core-util": "0.3.318",
|
|
27
27
|
"inversify": "7.10.4",
|
|
28
28
|
"minimatch": "10.0.1"
|
|
29
29
|
}
|
|
@@ -7,14 +7,19 @@ import { ApiClient } from './ApiClient';
|
|
|
7
7
|
* invoke the composed filter chain (via RouteBuilder.createRouteInvoker) — that proxy IS what
|
|
8
8
|
* createApiClient() returns. {@link apiClients} reuses the SAME proxy per registered api, so the
|
|
9
9
|
* express layer binds each method through it. There is no express dependency here, so the proxy
|
|
10
|
-
* is the single invocation path for BOTH in-process (tests) and HTTP
|
|
11
|
-
*
|
|
10
|
+
* is the single invocation path for BOTH in-process (tests) and HTTP.
|
|
11
|
+
*
|
|
12
|
+
* Establishing the request scope is a PRECONDITION of calling in here, never this class's job. The
|
|
13
|
+
* caller above the api boundary opens `RequestContext.run(...)`, publishes the inbound
|
|
14
|
+
* `HttpRequest`, and calls `RequestContextHeaders.fillFromRequest()` to move its headers into the
|
|
15
|
+
* context. `WebpiecesMiddleware` does all three for you; a non-webpieces transport (or a test
|
|
16
|
+
* driving `createApiClient` directly) must do the same. This proxy only CHECKS that it happened —
|
|
17
|
+
* manufacturing a context here would hide a missing filter and silently strip every request id.
|
|
12
18
|
*
|
|
13
19
|
* @provideFrameworkSingleton so WebpiecesRouter can inject it (it shares the one RouteBuilder).
|
|
14
20
|
*/
|
|
15
21
|
export declare class ApiClientFactory {
|
|
16
22
|
private readonly routeBuilder;
|
|
17
|
-
private readonly contextMgr;
|
|
18
23
|
constructor(routeBuilder: RouteBuilderImpl);
|
|
19
24
|
/**
|
|
20
25
|
* Create an API client proxy (cast to the API interface T). The proxy's methods run the full
|
package/src/ApiClientFactory.js
CHANGED
|
@@ -8,7 +8,20 @@ const core_context_1 = require("@webpieces/core-context");
|
|
|
8
8
|
const MethodMeta_1 = require("./MethodMeta");
|
|
9
9
|
const RouteBuilderImpl_1 = require("./RouteBuilderImpl");
|
|
10
10
|
const ApiClient_1 = require("./ApiClient");
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Every call through the proxy needs an ambient RequestContext, established ABOVE the api boundary.
|
|
13
|
+
* It is NOT auto-created here: manufacturing one silently would hide a missing top-level filter, and
|
|
14
|
+
* every log line, outbound call, and enqueued task under it would quietly lose its request id.
|
|
15
|
+
*/
|
|
16
|
+
// webpieces-disable no-function-outside-class -- a guard over ambient state; a class would own nothing
|
|
17
|
+
function requireActiveContext(routeMeta) {
|
|
18
|
+
if (core_context_1.RequestContext.isActive()) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`${routeMeta.controllerClassName}.${routeMeta.methodName} was called with no active RequestContext. ` +
|
|
22
|
+
`A server transport must wrap each request in RequestContext.run(...) (WebpiecesMiddleware does). ` +
|
|
23
|
+
`In a test, wrap the call yourself: await RequestContext.run(async () => api.foo(req));`);
|
|
24
|
+
}
|
|
12
25
|
/**
|
|
13
26
|
* ApiClientFactory - THE piece that wires api → Proxy → filters → controller.
|
|
14
27
|
*
|
|
@@ -16,16 +29,19 @@ const fillContext_1 = require("./fillContext");
|
|
|
16
29
|
* invoke the composed filter chain (via RouteBuilder.createRouteInvoker) — that proxy IS what
|
|
17
30
|
* createApiClient() returns. {@link apiClients} reuses the SAME proxy per registered api, so the
|
|
18
31
|
* express layer binds each method through it. There is no express dependency here, so the proxy
|
|
19
|
-
* is the single invocation path for BOTH in-process (tests) and HTTP
|
|
20
|
-
*
|
|
32
|
+
* is the single invocation path for BOTH in-process (tests) and HTTP.
|
|
33
|
+
*
|
|
34
|
+
* Establishing the request scope is a PRECONDITION of calling in here, never this class's job. The
|
|
35
|
+
* caller above the api boundary opens `RequestContext.run(...)`, publishes the inbound
|
|
36
|
+
* `HttpRequest`, and calls `RequestContextHeaders.fillFromRequest()` to move its headers into the
|
|
37
|
+
* context. `WebpiecesMiddleware` does all three for you; a non-webpieces transport (or a test
|
|
38
|
+
* driving `createApiClient` directly) must do the same. This proxy only CHECKS that it happened —
|
|
39
|
+
* manufacturing a context here would hide a missing filter and silently strip every request id.
|
|
21
40
|
*
|
|
22
41
|
* @provideFrameworkSingleton so WebpiecesRouter can inject it (it shares the one RouteBuilder).
|
|
23
42
|
*/
|
|
24
43
|
let ApiClientFactory = class ApiClientFactory {
|
|
25
44
|
routeBuilder;
|
|
26
|
-
// Builds request headers the SAME way the real HTTP client does — from the ambient
|
|
27
|
-
// RequestContext — so a credential a test put in context travels as a real request header.
|
|
28
|
-
contextMgr = new core_util_1.ContextMgr(new core_context_1.RequestContextReader());
|
|
29
45
|
constructor(routeBuilder) {
|
|
30
46
|
this.routeBuilder = routeBuilder;
|
|
31
47
|
}
|
|
@@ -73,10 +89,7 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
73
89
|
const service = this.routeBuilder.createRouteInvoker(httpMethod, path);
|
|
74
90
|
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
75
91
|
proxy[methodName] = async (requestDto) => {
|
|
76
|
-
|
|
77
|
-
if (!core_context_1.RequestContext.isActive()) {
|
|
78
|
-
return core_context_1.RequestContext.run(async () => this.runMethod(routeMeta, requestDto, service));
|
|
79
|
-
}
|
|
92
|
+
requireActiveContext(routeMeta);
|
|
80
93
|
return this.runMethod(routeMeta, requestDto, service);
|
|
81
94
|
};
|
|
82
95
|
}
|
|
@@ -84,17 +97,6 @@ let ApiClientFactory = class ApiClientFactory {
|
|
|
84
97
|
}
|
|
85
98
|
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
86
99
|
async runMethod(routeMeta, requestDto, service) {
|
|
87
|
-
// Only synthesize the request when NONE was published by a transport. The express adapter
|
|
88
|
-
// publishes the HttpRequest from `req` before calling the proxy, so its request wins; a
|
|
89
|
-
// pure in-process call synthesizes one from the ambient context (client-like).
|
|
90
|
-
if (!core_context_1.RequestContext.getRequest()) {
|
|
91
|
-
const headers = new Map();
|
|
92
|
-
this.contextMgr.buildOutboundHeaders().forEach((value, name) => {
|
|
93
|
-
headers.set(name.toLowerCase(), [value]);
|
|
94
|
-
});
|
|
95
|
-
core_context_1.RequestContext.setRequest(new core_context_1.HttpRequest(routeMeta.httpMethod, routeMeta.path, headers));
|
|
96
|
-
(0, fillContext_1.fillContext)();
|
|
97
|
-
}
|
|
98
100
|
const responseWrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestDto));
|
|
99
101
|
return responseWrapper.response;
|
|
100
102
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiClientFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiClientFactory.ts"],"names":[],"mappings":";;;;AAAA,yCAA+C;AAC/C,
|
|
1
|
+
{"version":3,"file":"ApiClientFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiClientFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, injectable } from 'inversify';\nimport {\n getApiPath,\n getEndpoints,\n RouteMetadata,\n} from '@webpieces/core-util';\nimport { provideFrameworkSingleton, RequestContext } from '@webpieces/core-context';\nimport { MethodMeta } from './MethodMeta';\nimport { Service, WpResponse } from './Filter';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiClient, ApiClientProxy } from './ApiClient';\nimport { ClassType } from './ApiRoutingFactory';\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()\n@injectable()\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 apis = new Set<ClassType>();\n for (const route of this.routeBuilder.getRoutes()) {\n apis.add(route.definition.apiClass as ClassType);\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 return [...apis].map((api: ClassType) => {\n // webpieces-disable no-any-unknown -- the registered api is an unconstrained ClassType\n const client = this.createApiClient<ApiClientProxy>(api as any);\n return new ApiClient(api, client);\n });\n }\n\n /** Build the proxy record (method name → invoker) from the API prototype's decorators. */\n // webpieces-disable no-any-unknown -- accepts any ClassType / abstract-constructor API prototype\n private buildProxy(apiPrototype: any): ApiClientProxy {\n const basePath = getApiPath(apiPrototype) || '';\n const endpoints = getEndpoints(apiPrototype) || {};\n const proxy: ApiClientProxy = {};\n\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n const httpMethod = 'POST';\n const path = basePath + endpointPath;\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 (requestDto: unknown): Promise<unknown> => {\n requireActiveContext(routeMeta);\n return this.runMethod(routeMeta, requestDto, service);\n };\n }\n\n return proxy;\n }\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n private async runMethod(routeMeta: RouteMetadata, requestDto: unknown, service: Service<MethodMeta, WpResponse<unknown>>): Promise<unknown> {\n const responseWrapper = await service.invoke(new MethodMeta(routeMeta, requestDto));\n return responseWrapper.response;\n }\n}\n"]}
|
|
@@ -23,6 +23,7 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
|
|
|
23
23
|
private requireAuthConfig;
|
|
24
24
|
private enforceJwt;
|
|
25
25
|
private enforceOidc;
|
|
26
|
+
/** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */
|
|
26
27
|
private enforceSharedSecret;
|
|
27
28
|
/** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */
|
|
28
29
|
private matchesEither;
|
|
@@ -30,6 +31,12 @@ export declare class AuthFilter extends Filter<MethodMeta, WpResponse<unknown>>
|
|
|
30
31
|
private bestEffortJwt;
|
|
31
32
|
/** Stamp the parsed user's context entries + the principal into the RequestContext. */
|
|
32
33
|
private applyAuthValues;
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The credential value IF the header carries the expected scheme, else undefined.
|
|
36
|
+
*
|
|
37
|
+
* Strict: a bare value with no scheme, or a value under the WRONG scheme (a shared secret sent
|
|
38
|
+
* where a JWT is expected), yields undefined and the caller 401s.
|
|
39
|
+
*/
|
|
40
|
+
private credential;
|
|
34
41
|
private constantTimeEquals;
|
|
35
42
|
}
|
|
@@ -9,6 +9,25 @@ const core_util_1 = require("@webpieces/core-util");
|
|
|
9
9
|
const Filter_1 = require("../Filter");
|
|
10
10
|
const AuthConfig_1 = require("../AuthConfig");
|
|
11
11
|
const log = core_util_1.LogManager.getLogger('AuthFilter');
|
|
12
|
+
/**
|
|
13
|
+
* The ONE credential header, read straight off the inbound HttpRequest.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately NOT a ContextKey: a ContextKey with an httpHeader is a TRANSFERRED key, which would
|
|
16
|
+
* put the caller's credential into RequestContext and hence onto every outbound call this service
|
|
17
|
+
* makes, and onto every Cloud Task it enqueues. A credential belongs to ONE request hop.
|
|
18
|
+
*/
|
|
19
|
+
const AUTHORIZATION_HEADER = 'authorization';
|
|
20
|
+
/**
|
|
21
|
+
* The scheme (first word of the Authorization value) names WHICH credential follows, so a secret
|
|
22
|
+
* can never be mistaken for a token, nor accepted where the other was expected:
|
|
23
|
+
*
|
|
24
|
+
* Authorization: Bearer <user JWT | service OIDC token>
|
|
25
|
+
* Authorization: Webpieces <@AuthSharedSecret value>
|
|
26
|
+
*
|
|
27
|
+
* The scheme is REQUIRED. A bare value with no scheme is rejected.
|
|
28
|
+
*/
|
|
29
|
+
const BEARER_SCHEME = 'Bearer';
|
|
30
|
+
const SHARED_SECRET_SCHEME = 'Webpieces';
|
|
12
31
|
/** Reserved context key holding the authenticated {@link AuthValues} (stamped after a jwt parse). */
|
|
13
32
|
const PRINCIPAL_KEY = '__webpieces_principal__';
|
|
14
33
|
/**
|
|
@@ -35,7 +54,7 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
|
|
|
35
54
|
// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility
|
|
36
55
|
async filter(meta, nextFilter) {
|
|
37
56
|
const mode = meta.authMeta?.mode;
|
|
38
|
-
const authHeader = core_context_1.RequestContext.getRequest()?.getHeader(
|
|
57
|
+
const authHeader = core_context_1.RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);
|
|
39
58
|
if (!mode || mode.kind === 'public') {
|
|
40
59
|
// Public: best-effort parse so a logged-out page can still know the logged-in user.
|
|
41
60
|
this.bestEffortJwt(authHeader);
|
|
@@ -49,7 +68,7 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
|
|
|
49
68
|
await this.enforceOidc(authHeader, mode.callers);
|
|
50
69
|
break;
|
|
51
70
|
case 'shared-secret':
|
|
52
|
-
this.enforceSharedSecret(
|
|
71
|
+
this.enforceSharedSecret(this.credential(authHeader, SHARED_SECRET_SCHEME), mode.secretKey);
|
|
53
72
|
break;
|
|
54
73
|
}
|
|
55
74
|
return nextFilter.invoke(meta);
|
|
@@ -61,7 +80,7 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
|
|
|
61
80
|
return this.authConfig;
|
|
62
81
|
}
|
|
63
82
|
enforceJwt(header, requirement) {
|
|
64
|
-
const token = this.
|
|
83
|
+
const token = this.credential(header, BEARER_SCHEME);
|
|
65
84
|
if (!token) {
|
|
66
85
|
throw new core_util_1.HttpUnauthorizedError('Authentication required');
|
|
67
86
|
}
|
|
@@ -71,12 +90,13 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
|
|
|
71
90
|
config.authorizeJwt(values, requirement); // AUTHORIZE — app policy; throws HttpForbiddenError to deny
|
|
72
91
|
}
|
|
73
92
|
async enforceOidc(header, callers) {
|
|
74
|
-
const token = this.
|
|
93
|
+
const token = this.credential(header, BEARER_SCHEME);
|
|
75
94
|
if (!token) {
|
|
76
95
|
throw new core_util_1.HttpUnauthorizedError('Missing OIDC bearer token for @AuthOidc endpoint');
|
|
77
96
|
}
|
|
78
97
|
await this.requireAuthConfig().verifyOidc(token, callers);
|
|
79
98
|
}
|
|
99
|
+
/** `provided` is the Authorization bearer value — the secret itself, same header as a JWT. */
|
|
80
100
|
enforceSharedSecret(provided, secretKey) {
|
|
81
101
|
const accepted = this.requireAuthConfig().sharedSecrets[secretKey];
|
|
82
102
|
if (!accepted || !provided || !this.matchesEither(provided, accepted)) {
|
|
@@ -90,7 +110,7 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
|
|
|
90
110
|
}
|
|
91
111
|
/** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */
|
|
92
112
|
bestEffortJwt(header) {
|
|
93
|
-
const token = this.
|
|
113
|
+
const token = this.credential(header, BEARER_SCHEME);
|
|
94
114
|
if (!this.authConfig || !token) {
|
|
95
115
|
return;
|
|
96
116
|
}
|
|
@@ -110,12 +130,18 @@ let AuthFilter = class AuthFilter extends Filter_1.Filter {
|
|
|
110
130
|
}
|
|
111
131
|
core_context_1.RequestContext.put(PRINCIPAL_KEY, values);
|
|
112
132
|
}
|
|
113
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The credential value IF the header carries the expected scheme, else undefined.
|
|
135
|
+
*
|
|
136
|
+
* Strict: a bare value with no scheme, or a value under the WRONG scheme (a shared secret sent
|
|
137
|
+
* where a JWT is expected), yields undefined and the caller 401s.
|
|
138
|
+
*/
|
|
139
|
+
credential(header, scheme) {
|
|
114
140
|
if (!header) {
|
|
115
141
|
return undefined;
|
|
116
142
|
}
|
|
117
|
-
const prefix =
|
|
118
|
-
return header.startsWith(prefix) ? header.substring(prefix.length) :
|
|
143
|
+
const prefix = `${scheme} `;
|
|
144
|
+
return header.startsWith(prefix) ? header.substring(prefix.length) : undefined;
|
|
119
145
|
}
|
|
120
146
|
constantTimeEquals(a, b) {
|
|
121
147
|
const bufA = Buffer.from(a, 'utf8');
|
|
@@ -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, injectable, optional } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport { provideFrameworkSingleton, RequestContext } from '@webpieces/core-context';\nimport { WebpiecesCoreHeaders, HttpUnauthorizedError, JwtRequirement, LogManager, toError } from '@webpieces/core-util';\nimport { Filter, WpResponse, Service } from '../Filter';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AuthValues, SharedSecrets } from '../AuthConfig';\n\nconst log = LogManager.getLogger('AuthFilter');\n\n/** Reserved context key holding the authenticated {@link AuthValues} (stamped after a jwt parse). */\nconst PRINCIPAL_KEY = '__webpieces_principal__';\n\n/**\n * AuthFilter - the ONE framework auth filter, auto-installed just below the error filter on every\n * route. It is TRANSPORT-NEUTRAL: it reads the raw credential from the {@link HttpRequest} in\n * RequestContext (never express), so the SAME check runs over HTTP and via createApiClient.\n *\n * It enforces the endpoint's AuthMode using the injected app-bound {@link AuthConfig}:\n * - shared-secret → constant-time compare vs the bound secret VALUE (state).\n * - jwt → `parseJwt` → stamp the user's context values + enforce @AuthJwt(...roles).\n * - oidc → `verifyOidc` (delegates to gcp-identity in the company layer).\n * - public → BEST-EFFORT jwt parse: if a token is present, stamp the user's context so a\n * logged-out page still knows who is logged in; never fails.\n *\n * The verifiers/secrets are app-provided (rebindable in tests), so http-routing needs no\n * jsonwebtoken / gcp-identity.\n */\n@provideFrameworkSingleton()\n@injectable()\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 // @optional: a public-only server need not bind an AuthConfig; a non-public route then\n // fails fast in requireAuthConfig().\n @optional() @inject(AuthConfig) private readonly authConfig?: AuthConfig,\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.authMeta?.mode;\n const authHeader = RequestContext.getRequest()?.getHeader(WebpiecesCoreHeaders.AUTHORIZATION);\n\n if (!mode || mode.kind === 'public') {\n // Public: best-effort parse so a logged-out page can still know the logged-in user.\n this.bestEffortJwt(authHeader);\n return nextFilter.invoke(meta);\n }\n\n switch (mode.kind) {\n case 'jwt':\n this.enforceJwt(authHeader, mode.requirement);\n break;\n case 'oidc':\n await this.enforceOidc(authHeader, mode.callers);\n break;\n case 'shared-secret':\n this.enforceSharedSecret(\n RequestContext.getRequest()?.getHeader(WebpiecesCoreHeaders.SHARED_SECRET),\n mode.secretKey,\n );\n break;\n }\n return nextFilter.invoke(meta);\n }\n\n private requireAuthConfig(): AuthConfig {\n if (!this.authConfig) {\n throw new HttpUnauthorizedError('No AuthConfig bound — cannot enforce a non-public endpoint');\n }\n return this.authConfig;\n }\n\n private enforceJwt(header: string | undefined, requirement: JwtRequirement): void {\n const token = this.stripBearer(header);\n if (!token) {\n throw new HttpUnauthorizedError('Authentication required');\n }\n const config = this.requireAuthConfig();\n const values = config.parseJwt(token); // AUTHENTICATE — throws HttpUnauthorizedError if invalid\n this.applyAuthValues(values);\n config.authorizeJwt(values, requirement); // AUTHORIZE — app policy; throws HttpForbiddenError to deny\n }\n\n private async enforceOidc(header: string | undefined, callers: string[]): Promise<void> {\n const token = this.stripBearer(header);\n if (!token) {\n throw new HttpUnauthorizedError('Missing OIDC bearer token for @AuthOidc endpoint');\n }\n await this.requireAuthConfig().verifyOidc(token, callers);\n }\n\n private enforceSharedSecret(provided: string | undefined, secretKey: string): void {\n const accepted = this.requireAuthConfig().sharedSecrets[secretKey];\n if (!accepted || !provided || !this.matchesEither(provided, accepted)) {\n throw new HttpUnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');\n }\n }\n\n /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */\n private matchesEither(provided: string, accepted: SharedSecrets): boolean {\n return (\n (accepted.secret1 !== '' && this.constantTimeEquals(provided, accepted.secret1)) ||\n (accepted.secret2 !== '' && this.constantTimeEquals(provided, accepted.secret2))\n );\n }\n\n /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */\n private bestEffortJwt(header: string | undefined): void {\n const token = this.stripBearer(header);\n if (!this.authConfig || !token) {\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort on a public route: a bad/absent token just means \"not logged in\", must not fail the request\n try {\n this.applyAuthValues(this.authConfig.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n log.debug('Best-effort JWT parse on a public endpoint failed (treating as anonymous): ', error);\n }\n }\n\n /** Stamp the parsed user's context entries + the principal into the RequestContext. */\n private applyAuthValues(values: AuthValues): void {\n for (const entry of values.entries) {\n RequestContext.putHeader(entry.key, entry.value);\n }\n RequestContext.put(PRINCIPAL_KEY, values);\n }\n\n private stripBearer(header: string | undefined): string | undefined {\n if (!header) {\n return undefined;\n }\n const prefix = 'Bearer ';\n return header.startsWith(prefix) ? header.substring(prefix.length) : header;\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, injectable, optional } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport { provideFrameworkSingleton, RequestContext } from '@webpieces/core-context';\nimport { HttpUnauthorizedError, JwtRequirement, LogManager, toError } from '@webpieces/core-util';\nimport { Filter, WpResponse, Service } from '../Filter';\nimport { MethodMeta } from '../MethodMeta';\nimport { AuthConfig, AuthValues, SharedSecrets } from '../AuthConfig';\n\nconst log = LogManager.getLogger('AuthFilter');\n\n/**\n * The ONE credential header, read straight off the inbound HttpRequest.\n *\n * Deliberately NOT a ContextKey: a ContextKey with an httpHeader is a TRANSFERRED key, which would\n * put the caller's credential into RequestContext and hence onto every outbound call this service\n * makes, and onto every Cloud Task it enqueues. A credential belongs to ONE request hop.\n */\nconst AUTHORIZATION_HEADER = 'authorization';\n\n/**\n * The scheme (first word of the Authorization value) names WHICH credential follows, so a secret\n * can never be mistaken for a token, nor accepted where the other was expected:\n *\n * Authorization: Bearer <user JWT | service OIDC token>\n * Authorization: Webpieces <@AuthSharedSecret value>\n *\n * The scheme is REQUIRED. A bare value with no scheme is rejected.\n */\nconst BEARER_SCHEME = 'Bearer';\nconst SHARED_SECRET_SCHEME = 'Webpieces';\n\n/** Reserved context key holding the authenticated {@link AuthValues} (stamped after a jwt parse). */\nconst PRINCIPAL_KEY = '__webpieces_principal__';\n\n/**\n * AuthFilter - the ONE framework auth filter, auto-installed just below the error filter on every\n * route. It is TRANSPORT-NEUTRAL: it reads the raw credential from the {@link HttpRequest} in\n * RequestContext (never express), so the SAME check runs over HTTP and via createApiClient.\n *\n * It enforces the endpoint's AuthMode using the injected app-bound {@link AuthConfig}:\n * - shared-secret → constant-time compare vs the bound secret VALUE (state).\n * - jwt → `parseJwt` → stamp the user's context values + enforce @AuthJwt(...roles).\n * - oidc → `verifyOidc` (delegates to gcp-identity in the company layer).\n * - public → BEST-EFFORT jwt parse: if a token is present, stamp the user's context so a\n * logged-out page still knows who is logged in; never fails.\n *\n * The verifiers/secrets are app-provided (rebindable in tests), so http-routing needs no\n * jsonwebtoken / gcp-identity.\n */\n@provideFrameworkSingleton()\n@injectable()\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 // @optional: a public-only server need not bind an AuthConfig; a non-public route then\n // fails fast in requireAuthConfig().\n @optional() @inject(AuthConfig) private readonly authConfig?: AuthConfig,\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.authMeta?.mode;\n const authHeader = RequestContext.getRequest()?.getHeader(AUTHORIZATION_HEADER);\n\n if (!mode || mode.kind === 'public') {\n // Public: best-effort parse so a logged-out page can still know the logged-in user.\n this.bestEffortJwt(authHeader);\n return nextFilter.invoke(meta);\n }\n\n switch (mode.kind) {\n case 'jwt':\n this.enforceJwt(authHeader, mode.requirement);\n break;\n case 'oidc':\n await this.enforceOidc(authHeader, mode.callers);\n break;\n case 'shared-secret':\n this.enforceSharedSecret(this.credential(authHeader, SHARED_SECRET_SCHEME), mode.secretKey);\n break;\n }\n return nextFilter.invoke(meta);\n }\n\n private requireAuthConfig(): AuthConfig {\n if (!this.authConfig) {\n throw new HttpUnauthorizedError('No AuthConfig bound — cannot enforce a non-public endpoint');\n }\n return this.authConfig;\n }\n\n private enforceJwt(header: string | undefined, requirement: JwtRequirement): void {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new HttpUnauthorizedError('Authentication required');\n }\n const config = this.requireAuthConfig();\n const values = config.parseJwt(token); // AUTHENTICATE — throws HttpUnauthorizedError if invalid\n this.applyAuthValues(values);\n config.authorizeJwt(values, requirement); // AUTHORIZE — app policy; throws HttpForbiddenError to deny\n }\n\n private async enforceOidc(header: string | undefined, callers: string[]): Promise<void> {\n const token = this.credential(header, BEARER_SCHEME);\n if (!token) {\n throw new HttpUnauthorizedError('Missing OIDC bearer token for @AuthOidc endpoint');\n }\n await this.requireAuthConfig().verifyOidc(token, callers);\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.requireAuthConfig().sharedSecrets[secretKey];\n if (!accepted || !provided || !this.matchesEither(provided, accepted)) {\n throw new HttpUnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');\n }\n }\n\n /** EITHER secret1 or secret2 passes — the rotation window. Constant-time on each non-empty slot. */\n private matchesEither(provided: string, accepted: SharedSecrets): boolean {\n return (\n (accepted.secret1 !== '' && this.constantTimeEquals(provided, accepted.secret1)) ||\n (accepted.secret2 !== '' && this.constantTimeEquals(provided, accepted.secret2))\n );\n }\n\n /** Parse a JWT if one is present, else do nothing — used on public routes; never throws. */\n private bestEffortJwt(header: string | undefined): void {\n const token = this.credential(header, BEARER_SCHEME);\n if (!this.authConfig || !token) {\n return;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort on a public route: a bad/absent token just means \"not logged in\", must not fail the request\n try {\n this.applyAuthValues(this.authConfig.parseJwt(token));\n } catch (err: unknown) {\n const error = toError(err);\n log.debug('Best-effort JWT parse on a public endpoint failed (treating as anonymous): ', error);\n }\n }\n\n /** Stamp the parsed user's context entries + the principal into the RequestContext. */\n private applyAuthValues(values: AuthValues): void {\n for (const entry of values.entries) {\n RequestContext.putHeader(entry.key, entry.value);\n }\n RequestContext.put(PRINCIPAL_KEY, values);\n }\n\n /**\n * The credential value IF the header carries the expected scheme, else undefined.\n *\n * Strict: a bare value with no scheme, or a value under the WRONG scheme (a shared secret sent\n * where a JWT is expected), yields undefined and the caller 401s.\n */\n private credential(header: string | undefined, scheme: string): string | undefined {\n if (!header) {\n return undefined;\n }\n const prefix = `${scheme} `;\n return header.startsWith(prefix) ? header.substring(prefix.length) : undefined;\n }\n\n private constantTimeEquals(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'utf8');\n const bufB = Buffer.from(b, 'utf8');\n if (bufA.length !== bufB.length) {\n return false;\n }\n return timingSafeEqual(bufA, bufB);\n }\n}\n"]}
|
|
@@ -4,7 +4,7 @@ import { MethodMeta } from '../MethodMeta';
|
|
|
4
4
|
* ErrorLogFilter - the OUTERMOST fixed framework filter (auto-installed above the auth filter on
|
|
5
5
|
* every route). It wraps the whole chain in a try/catch so EVERY failure — over HTTP or via
|
|
6
6
|
* createApiClient — is logged once WITH the request context (correlation/request id, etc.) that
|
|
7
|
-
*
|
|
7
|
+
* RequestContextHeaders.fillFromRequest() established above the boundary.
|
|
8
8
|
*
|
|
9
9
|
* It re-throws the error unchanged; the transport (express adapter, or another framework's
|
|
10
10
|
* adapter) maps HttpError subclasses → HTTP status. Being a below-boundary filter means the
|
|
@@ -11,7 +11,7 @@ const log = core_util_1.LogManager.getLogger('ErrorLogFilter');
|
|
|
11
11
|
* ErrorLogFilter - the OUTERMOST fixed framework filter (auto-installed above the auth filter on
|
|
12
12
|
* every route). It wraps the whole chain in a try/catch so EVERY failure — over HTTP or via
|
|
13
13
|
* createApiClient — is logged once WITH the request context (correlation/request id, etc.) that
|
|
14
|
-
*
|
|
14
|
+
* RequestContextHeaders.fillFromRequest() established above the boundary.
|
|
15
15
|
*
|
|
16
16
|
* It re-throws the error unchanged; the transport (express adapter, or another framework's
|
|
17
17
|
* adapter) maps HttpError subclasses → HTTP status. Being a below-boundary filter means the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ErrorLogFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-routing/src/filters/ErrorLogFilter.ts"],"names":[],"mappings":";;;;AAAA,yCAAuC;AACvC,0DAAoE;AACpE,oDAA2D;AAC3D,sCAAwD;AAGxD,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;AAEnD;;;;;;;;;GASG;AAII,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,eAAuC;IACvE,iGAAiG;IACxF,KAAK,CAAC,MAAM,CACjB,IAAgB,EAChB,UAAoD;QAEpD,iLAAiL;QACjL,IAAI,CAAC;YACD,OAAO,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC;YACtF,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;CACJ,CAAA;AAfY,wCAAc;yBAAd,cAAc;IAH1B,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;IACb,iGAAiG;GACpF,cAAc,CAe1B","sourcesContent":["import { injectable } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { toError, LogManager } from '@webpieces/core-util';\nimport { Filter, WpResponse, Service } from '../Filter';\nimport { MethodMeta } from '../MethodMeta';\n\nconst log = LogManager.getLogger('ErrorLogFilter');\n\n/**\n * ErrorLogFilter - the OUTERMOST fixed framework filter (auto-installed above the auth filter on\n * every route). It wraps the whole chain in a try/catch so EVERY failure — over HTTP or via\n * createApiClient — is logged once WITH the request context (correlation/request id, etc.) that\n *
|
|
1
|
+
{"version":3,"file":"ErrorLogFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-routing/src/filters/ErrorLogFilter.ts"],"names":[],"mappings":";;;;AAAA,yCAAuC;AACvC,0DAAoE;AACpE,oDAA2D;AAC3D,sCAAwD;AAGxD,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;AAEnD;;;;;;;;;GASG;AAII,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,eAAuC;IACvE,iGAAiG;IACxF,KAAK,CAAC,MAAM,CACjB,IAAgB,EAChB,UAAoD;QAEpD,iLAAiL;QACjL,IAAI,CAAC;YACD,OAAO,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC;YACtF,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;CACJ,CAAA;AAfY,wCAAc;yBAAd,cAAc;IAH1B,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;IACb,iGAAiG;GACpF,cAAc,CAe1B","sourcesContent":["import { injectable } from 'inversify';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { toError, LogManager } from '@webpieces/core-util';\nimport { Filter, WpResponse, Service } from '../Filter';\nimport { MethodMeta } from '../MethodMeta';\n\nconst log = LogManager.getLogger('ErrorLogFilter');\n\n/**\n * ErrorLogFilter - the OUTERMOST fixed framework filter (auto-installed above the auth filter on\n * every route). It wraps the whole chain in a try/catch so EVERY failure — over HTTP or via\n * createApiClient — is logged once WITH the request context (correlation/request id, etc.) that\n * RequestContextHeaders.fillFromRequest() established above the boundary.\n *\n * It re-throws the error unchanged; the transport (express adapter, or another framework's\n * adapter) maps HttpError subclasses → HTTP status. Being a below-boundary filter means the\n * in-process path gets the same consistent logging the HTTP path always had.\n */\n@provideFrameworkSingleton()\n@injectable()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response flexibility\nexport class ErrorLogFilter extends Filter<MethodMeta, WpResponse<unknown>> {\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- fixed boundary filter: log every failure with context, then re-throw for the transport to translate to a status\n try {\n return await nextFilter.invoke(meta);\n } catch (err: unknown) {\n const error = toError(err);\n log.error(`[${meta.httpMethod} ${meta.path}] ${error.name}: ${error.message}`, error);\n throw error;\n }\n }\n}\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -14,8 +14,6 @@ export { FilterMatcher, HttpFilter } from './FilterMatcher';
|
|
|
14
14
|
export { ApiFactory } from './ApiFactory';
|
|
15
15
|
export { ApiClient, ApiClientProxy } from './ApiClient';
|
|
16
16
|
export { AuthConfig, AuthValues, SharedSecrets } from './AuthConfig';
|
|
17
|
-
export { fillContext } from './fillContext';
|
|
18
17
|
export { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';
|
|
19
18
|
export { setupRuntime, RuntimeSetupOptions } from './setupRuntime';
|
|
20
|
-
export { RequestContextReader } from '@webpieces/core-context';
|
|
21
19
|
export { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.WEBPIECES_CONFIG_TOKEN = exports.WebpiecesConfig = exports.
|
|
3
|
+
exports.WebpiecesRouterFactory = exports.WebpiecesRouter = exports.SharedSecrets = exports.AuthValues = exports.AuthConfig = exports.ApiClient = exports.FilterMatcher = exports.RouteHandler = exports.MethodMeta = exports.FilterChain = exports.WpResponse = exports.Filter = exports.HttpRequest = exports.FilterDefinition = exports.RouteDefinition = exports.ApiRoutingFactory = exports.buildFrameworkModule = exports.provideFrameworkSingletonAs = exports.provideFrameworkSingleton = exports.provideTransient = exports.provideSingletonAs = exports.provideSingleton = exports.ROUTING_METADATA_KEYS = exports.SourceFile = exports.isDocumentDesign = exports.DocumentDesign = exports.METADATA_KEYS = exports.RouteMetadata = exports.AuthMeta = exports.getQueueName = exports.assertPubSubConventions = exports.assertApiKind = exports.getApiKind = exports.assertEveryEndpointHasAuthMode = exports.getAuthMode = exports.getAuthMeta = exports.isApiPath = exports.getEndpoints = exports.getApiPath = exports.Queue = exports.PubSub = exports.Rpc = exports.AuthSharedSecret = exports.AuthOidc = exports.AuthJwt = exports.Public = exports.AuthenticationConfig = exports.Authentication = exports.Endpoint = exports.ApiPath = void 0;
|
|
4
|
+
exports.WEBPIECES_CONFIG_TOKEN = exports.WebpiecesConfig = exports.RuntimeSetupOptions = exports.setupRuntime = 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; } });
|
|
@@ -79,8 +79,6 @@ Object.defineProperty(exports, "AuthConfig", { enumerable: true, get: function (
|
|
|
79
79
|
Object.defineProperty(exports, "AuthValues", { enumerable: true, get: function () { return AuthConfig_1.AuthValues; } });
|
|
80
80
|
Object.defineProperty(exports, "SharedSecrets", { enumerable: true, get: function () { return AuthConfig_1.SharedSecrets; } });
|
|
81
81
|
// Above-boundary context setup shared by every transport adapter.
|
|
82
|
-
var fillContext_1 = require("./fillContext");
|
|
83
|
-
Object.defineProperty(exports, "fillContext", { enumerable: true, get: function () { return fillContext_1.fillContext; } });
|
|
84
82
|
// Node-only router (the express-free heart: container + filter chain + in-process client)
|
|
85
83
|
var WebpiecesRouter_1 = require("./WebpiecesRouter");
|
|
86
84
|
Object.defineProperty(exports, "WebpiecesRouter", { enumerable: true, get: function () { return WebpiecesRouter_1.WebpiecesRouter; } });
|
|
@@ -90,9 +88,6 @@ Object.defineProperty(exports, "WebpiecesRouterFactory", { enumerable: true, get
|
|
|
90
88
|
var setupRuntime_1 = require("./setupRuntime");
|
|
91
89
|
Object.defineProperty(exports, "setupRuntime", { enumerable: true, get: function () { return setupRuntime_1.setupRuntime; } });
|
|
92
90
|
Object.defineProperty(exports, "RuntimeSetupOptions", { enumerable: true, get: function () { return setupRuntime_1.RuntimeSetupOptions; } });
|
|
93
|
-
// Context readers (Node.js only) moved to core-context; re-exported for back-compat
|
|
94
|
-
var core_context_4 = require("@webpieces/core-context");
|
|
95
|
-
Object.defineProperty(exports, "RequestContextReader", { enumerable: true, get: function () { return core_context_4.RequestContextReader; } });
|
|
96
91
|
// Server configuration
|
|
97
92
|
var WebpiecesConfig_1 = require("./WebpiecesConfig");
|
|
98
93
|
Object.defineProperty(exports, "WebpiecesConfig", { enumerable: true, get: function () { return WebpiecesConfig_1.WebpiecesConfig; } });
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/index.ts"],"names":[],"mappings":";;;;AAAA,0DAA0D;AAC1D,kDA8B8B;AA7B1B,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,2GAAA,cAAc,OAAA;AACd,iHAAA,oBAAoB,OAAA;AACpB,mGAAA,MAAM,OAAA;AACN,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,6GAAA,gBAAgB,OAAA;AAChB,gGAAA,GAAG,OAAA;AACH,mGAAA,MAAM,OAAA;AACN,kGAAA,KAAK,OAAA;AACL,uGAAA,UAAU,OAAA;AACV,yGAAA,YAAY,OAAA;AACZ,sGAAA,SAAS,OAAA;AACT,wGAAA,WAAW,OAAA;AACX,wGAAA,WAAW,OAAA;AACX,2HAAA,8BAA8B,OAAA;AAC9B,uGAAA,UAAU,OAAA;AACV,0GAAA,aAAa,OAAA;AACb,oHAAA,uBAAuB,OAAA;AACvB,yGAAA,YAAY,OAAA;AACZ,qGAAA,QAAQ,OAAA;AACR,0GAAA,aAAa,OAAA;AACb,0GAAA,aAAa,OAAA;AAEb,2EAA2E;AAC3E,oCAAoC;AACpC,2GAAA,cAAc,OAAA;AACd,6GAAA,gBAAgB,OAAA;AAIpB,+CAA+C;AAC/C,2CAGsB;AAFlB,wGAAA,UAAU,OAAA;AACV,mHAAA,qBAAqB,OAAA;AAGzB,iFAAiF;AACjF,wDAAiG;AAAxF,gHAAA,gBAAgB,OAAA;AAAE,kHAAA,kBAAkB,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAC/D,gGAAgG;AAChG,wDAIiC;AAH7B,yHAAA,yBAAyB,OAAA;AACzB,2HAAA,2BAA2B,OAAA;AAC3B,oHAAA,oBAAoB,OAAA;AAGxB,yDAAmE;AAA1D,sHAAA,iBAAiB,OAAA;AAE1B,qBAAqB;AACrB,2CAKsB;AAFlB,6GAAA,eAAe,OAAA;AACf,8GAAA,gBAAgB,OAAA;AAGpB,sFAAsF;AACtF,+FAA+F;AAC/F,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,qFAAqF;AACrF,mCAAuD;AAA9C,gGAAA,MAAM,OAAA;AAAE,oGAAA,UAAU,OAAA;AAC3B,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,oFAAoF;AACpF,sFAAsF;AAEtF,kBAAkB;AAClB,iDAA4D;AAAnD,8GAAA,aAAa,OAAA;AAItB,yCAAwD;AAA/C,sGAAA,SAAS,OAAA;AAElB,uFAAuF;AACvF,2CAAqE;AAA5D,wGAAA,UAAU,OAAA;AAAE,wGAAA,UAAU,OAAA;AAAE,2GAAA,aAAa,OAAA;AAE9C,kEAAkE;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/index.ts"],"names":[],"mappings":";;;;AAAA,0DAA0D;AAC1D,kDA8B8B;AA7B1B,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,2GAAA,cAAc,OAAA;AACd,iHAAA,oBAAoB,OAAA;AACpB,mGAAA,MAAM,OAAA;AACN,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,6GAAA,gBAAgB,OAAA;AAChB,gGAAA,GAAG,OAAA;AACH,mGAAA,MAAM,OAAA;AACN,kGAAA,KAAK,OAAA;AACL,uGAAA,UAAU,OAAA;AACV,yGAAA,YAAY,OAAA;AACZ,sGAAA,SAAS,OAAA;AACT,wGAAA,WAAW,OAAA;AACX,wGAAA,WAAW,OAAA;AACX,2HAAA,8BAA8B,OAAA;AAC9B,uGAAA,UAAU,OAAA;AACV,0GAAA,aAAa,OAAA;AACb,oHAAA,uBAAuB,OAAA;AACvB,yGAAA,YAAY,OAAA;AACZ,qGAAA,QAAQ,OAAA;AACR,0GAAA,aAAa,OAAA;AACb,0GAAA,aAAa,OAAA;AAEb,2EAA2E;AAC3E,oCAAoC;AACpC,2GAAA,cAAc,OAAA;AACd,6GAAA,gBAAgB,OAAA;AAIpB,+CAA+C;AAC/C,2CAGsB;AAFlB,wGAAA,UAAU,OAAA;AACV,mHAAA,qBAAqB,OAAA;AAGzB,iFAAiF;AACjF,wDAAiG;AAAxF,gHAAA,gBAAgB,OAAA;AAAE,kHAAA,kBAAkB,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAC/D,gGAAgG;AAChG,wDAIiC;AAH7B,yHAAA,yBAAyB,OAAA;AACzB,2HAAA,2BAA2B,OAAA;AAC3B,oHAAA,oBAAoB,OAAA;AAGxB,yDAAmE;AAA1D,sHAAA,iBAAiB,OAAA;AAE1B,qBAAqB;AACrB,2CAKsB;AAFlB,6GAAA,eAAe,OAAA;AACf,8GAAA,gBAAgB,OAAA;AAGpB,sFAAsF;AACtF,+FAA+F;AAC/F,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,qFAAqF;AACrF,mCAAuD;AAA9C,gGAAA,MAAM,OAAA;AAAE,oGAAA,UAAU,OAAA;AAC3B,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,oFAAoF;AACpF,sFAAsF;AAEtF,kBAAkB;AAClB,iDAA4D;AAAnD,8GAAA,aAAa,OAAA;AAItB,yCAAwD;AAA/C,sGAAA,SAAS,OAAA;AAElB,uFAAuF;AACvF,2CAAqE;AAA5D,wGAAA,UAAU,OAAA;AAAE,wGAAA,UAAU,OAAA;AAAE,2GAAA,aAAa,OAAA;AAE9C,kEAAkE;AAElE,0FAA0F;AAC1F,qDAAoG;AAA3F,kHAAA,eAAe,OAAA;AAAE,yHAAA,sBAAsB,OAAA;AAEhD,8FAA8F;AAC9F,kGAAkG;AAClG,+CAAmE;AAA1D,4GAAA,YAAY,OAAA;AAAE,mHAAA,mBAAmB,OAAA;AAE1C,uBAAuB;AACvB,qDAA4E;AAAnE,kHAAA,eAAe,OAAA;AAAE,yHAAA,sBAAsB,OAAA","sourcesContent":["// Re-export API decorators from core-util for convenience\nexport {\n ApiPath,\n Endpoint,\n Authentication,\n AuthenticationConfig,\n Public,\n AuthJwt,\n AuthOidc,\n AuthSharedSecret,\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n ValidateImplementation,\n // @DocumentDesign moved to core-util (design-root marker, browser + Node);\n // re-exported here for back-compat.\n DocumentDesign,\n isDocumentDesign,\n} from '@webpieces/core-util';\nexport type { AuthMode, ApiKind } from '@webpieces/core-util';\n\n// Server-side routing decorators and utilities\nexport {\n SourceFile,\n ROUTING_METADATA_KEYS,\n} from './decorators';\n\n// DI provider decorators moved to core-context; re-exported here for back-compat\nexport { provideSingleton, provideSingletonAs, provideTransient } from '@webpieces/core-context';\n// Framework-only DI registry (packages/** framework classes use these; see frameworkProvide.ts)\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonAs,\n buildFrameworkModule,\n} from '@webpieces/core-context';\n\nexport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\n\n// Core routing types\nexport {\n Routes,\n RouteBuilder,\n RouteDefinition,\n FilterDefinition,\n} from './WebAppMeta';\n\n// The transport-neutral request type (defined in core-context; this is http-routing's\n// public request — a transport adapter builds one and the chain reads it from RequestContext).\nexport { HttpRequest } from '@webpieces/core-context';\n\n// Filter-chain primitives (absorbed from the former @webpieces/http-filters package)\nexport { Filter, WpResponse, Service } from './Filter';\nexport { FilterChain } from './FilterChain';\nexport { MethodMeta } from './MethodMeta';\nexport { RouteHandler } from './RouteHandler';\n\n// 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 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 AuthConfig the framework AuthFilter injects.\nexport { AuthConfig, AuthValues, SharedSecrets } from './AuthConfig';\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"]}
|
package/src/fillContext.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* fillContext - ABOVE-the-boundary context setup, shared by every transport (the express
|
|
3
|
-
* adapter AND the in-process client). Call it once, inside RequestContext.run() and after
|
|
4
|
-
* RequestContext.setRequest(httpRequest): it transfers the platform/context headers from the
|
|
5
|
-
* HttpRequest into RequestContext (for logging + outbound propagation) and ensures a REQUEST_ID.
|
|
6
|
-
*
|
|
7
|
-
* This is the old below-boundary ContextFilter's job, moved above the api boundary so the raw
|
|
8
|
-
* request never has to survive as a chain filter — the fixed error/auth filters and the
|
|
9
|
-
* controller run below, reading the already-populated context (and the HttpRequest for auth).
|
|
10
|
-
*/
|
|
11
|
-
export declare function fillContext(): void;
|
package/src/fillContext.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.fillContext = fillContext;
|
|
4
|
-
const core_context_1 = require("@webpieces/core-context");
|
|
5
|
-
const core_util_1 = require("@webpieces/core-util");
|
|
6
|
-
/**
|
|
7
|
-
* fillContext - ABOVE-the-boundary context setup, shared by every transport (the express
|
|
8
|
-
* adapter AND the in-process client). Call it once, inside RequestContext.run() and after
|
|
9
|
-
* RequestContext.setRequest(httpRequest): it transfers the platform/context headers from the
|
|
10
|
-
* HttpRequest into RequestContext (for logging + outbound propagation) and ensures a REQUEST_ID.
|
|
11
|
-
*
|
|
12
|
-
* This is the old below-boundary ContextFilter's job, moved above the api boundary so the raw
|
|
13
|
-
* request never has to survive as a chain filter — the fixed error/auth filters and the
|
|
14
|
-
* controller run below, reading the already-populated context (and the HttpRequest for auth).
|
|
15
|
-
*/
|
|
16
|
-
function fillContext() {
|
|
17
|
-
const request = core_context_1.RequestContext.getRequest();
|
|
18
|
-
const registry = core_util_1.HeaderRegistry.get();
|
|
19
|
-
if (request) {
|
|
20
|
-
// Transfer each transferred key (read by wire name, store under key.name).
|
|
21
|
-
for (const key of registry.getTransferredKeys()) {
|
|
22
|
-
const values = request.getHeaderValues(key);
|
|
23
|
-
if (values && values.length > 0) {
|
|
24
|
-
core_context_1.RequestContext.putHeader(key, values[0]);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
if (!core_context_1.RequestContext.hasHeader(core_util_1.WebpiecesCoreHeaders.REQUEST_ID)) {
|
|
29
|
-
core_context_1.RequestContext.putHeader(core_util_1.WebpiecesCoreHeaders.REQUEST_ID, generateRequestId());
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function generateRequestId() {
|
|
33
|
-
return `svrGenReqId-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
34
|
-
}
|
|
35
|
-
//# sourceMappingURL=fillContext.js.map
|
package/src/fillContext.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fillContext.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/fillContext.ts"],"names":[],"mappings":";;AAaA,kCAiBC;AA9BD,0DAAyD;AACzD,oDAA4E;AAE5E;;;;;;;;;GASG;AACH,SAAgB,WAAW;IACvB,MAAM,OAAO,GAAG,6BAAc,CAAC,UAAU,EAAE,CAAC;IAC5C,MAAM,QAAQ,GAAG,0BAAc,CAAC,GAAG,EAAE,CAAC;IAEtC,IAAI,OAAO,EAAE,CAAC;QACV,2EAA2E;QAC3E,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,kBAAkB,EAAE,EAAE,CAAC;YAC9C,MAAM,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;YAC5C,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC9B,6BAAc,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7C,CAAC;QACL,CAAC;IACL,CAAC;IAED,IAAI,CAAC,6BAAc,CAAC,SAAS,CAAC,gCAAoB,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7D,6BAAc,CAAC,SAAS,CAAC,gCAAoB,CAAC,UAAU,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACnF,CAAC;AACL,CAAC;AAED,SAAS,iBAAiB;IACtB,OAAO,eAAe,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACtF,CAAC","sourcesContent":["import { RequestContext } from '@webpieces/core-context';\nimport { HeaderRegistry, WebpiecesCoreHeaders } from '@webpieces/core-util';\n\n/**\n * fillContext - ABOVE-the-boundary context setup, shared by every transport (the express\n * adapter AND the in-process client). Call it once, inside RequestContext.run() and after\n * RequestContext.setRequest(httpRequest): it transfers the platform/context headers from the\n * HttpRequest into RequestContext (for logging + outbound propagation) and ensures a REQUEST_ID.\n *\n * This is the old below-boundary ContextFilter's job, moved above the api boundary so the raw\n * request never has to survive as a chain filter — the fixed error/auth filters and the\n * controller run below, reading the already-populated context (and the HttpRequest for auth).\n */\nexport function fillContext(): void {\n const request = RequestContext.getRequest();\n const registry = HeaderRegistry.get();\n\n if (request) {\n // Transfer each transferred key (read by wire name, store under key.name).\n for (const key of registry.getTransferredKeys()) {\n const values = request.getHeaderValues(key);\n if (values && values.length > 0) {\n RequestContext.putHeader(key, values[0]);\n }\n }\n }\n\n if (!RequestContext.hasHeader(WebpiecesCoreHeaders.REQUEST_ID)) {\n RequestContext.putHeader(WebpiecesCoreHeaders.REQUEST_ID, generateRequestId());\n }\n}\n\nfunction generateRequestId(): string {\n return `svrGenReqId-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;\n}\n"]}
|