@webpieces/http-server 0.3.293 → 0.3.297
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 +5 -6
- package/src/WebpiecesExpressRouter.d.ts +51 -0
- package/src/WebpiecesExpressRouter.js +102 -0
- package/src/WebpiecesExpressRouter.js.map +1 -0
- package/src/WebpiecesMiddleware.d.ts +2 -2
- package/src/WebpiecesMiddleware.js +5 -2
- package/src/WebpiecesMiddleware.js.map +1 -1
- package/src/filters/LogApiFilter.d.ts +1 -1
- package/src/filters/LogApiFilter.js +3 -3
- package/src/filters/LogApiFilter.js.map +1 -1
- package/src/filters/RecordingFilter.d.ts +1 -1
- package/src/filters/RecordingFilter.js +2 -2
- package/src/filters/RecordingFilter.js.map +1 -1
- package/src/index.d.ts +2 -6
- package/src/index.js +14 -23
- package/src/index.js.map +1 -1
- package/src/WebpiecesExpress.d.ts +0 -44
- package/src/WebpiecesExpress.js +0 -71
- package/src/WebpiecesExpress.js.map +0 -1
- package/src/WebpiecesRouteCreator.d.ts +0 -52
- package/src/WebpiecesRouteCreator.js +0 -160
- package/src/WebpiecesRouteCreator.js.map +0 -1
- package/src/filters/ContextFilter.d.ts +0 -23
- package/src/filters/ContextFilter.js +0 -102
- package/src/filters/ContextFilter.js.map +0 -1
- package/src/filters/ServiceAuthFilter.d.ts +0 -19
- package/src/filters/ServiceAuthFilter.js +0 -83
- package/src/filters/ServiceAuthFilter.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.297",
|
|
4
4
|
"description": "WebPieces server with filter chain and dependency injection",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/core-context": "0.3.
|
|
26
|
-
"@webpieces/core-util": "0.3.
|
|
27
|
-
"@webpieces/gcp-identity": "0.3.
|
|
28
|
-
"@webpieces/http-
|
|
29
|
-
"@webpieces/http-routing": "0.3.293",
|
|
25
|
+
"@webpieces/core-context": "0.3.297",
|
|
26
|
+
"@webpieces/core-util": "0.3.297",
|
|
27
|
+
"@webpieces/gcp-identity": "0.3.297",
|
|
28
|
+
"@webpieces/http-routing": "0.3.297",
|
|
30
29
|
"cors": "2.8.5",
|
|
31
30
|
"express": "5.1.0",
|
|
32
31
|
"inversify": "7.10.4"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Express } from 'express';
|
|
2
|
+
import { ApiFactory } from '@webpieces/http-routing';
|
|
3
|
+
/** The value returned by express `app.listen(...)` (a node http.Server). */
|
|
4
|
+
type HttpServer = ReturnType<Express['listen']>;
|
|
5
|
+
/**
|
|
6
|
+
* WebpiecesExpressRouter - the express layer that sits ON TOP of a node-only
|
|
7
|
+
* {@link ApiFactory} (a WebpiecesRouter). It is the ONLY place express lifecycle lives.
|
|
8
|
+
*
|
|
9
|
+
* It never reaches into routing internals: it asks the ApiFactory for `apiClients()` — each
|
|
10
|
+
* an api + routeMeta + composed filter-chain→controller impl — and binds each to an express
|
|
11
|
+
* route (`app.<verb>(path, handler)`) invoked when the matching HTTP request arrives. The
|
|
12
|
+
* RouteBuilder stays hidden inside the ApiFactory.
|
|
13
|
+
*
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const apiFactory = await WebpiecesRouterFactory.create(config, { appBindings });
|
|
16
|
+
* apiFactory.addRoutes(SaveApi, SaveController);
|
|
17
|
+
* const express = new WebpiecesExpressRouter(apiFactory);
|
|
18
|
+
*
|
|
19
|
+
* // legacy / side-by-side: mount onto an existing app; you own listen + your middleware
|
|
20
|
+
* express.bindExpress(existingApp);
|
|
21
|
+
*
|
|
22
|
+
* // non-legacy: add webpieces global middleware + listen for you
|
|
23
|
+
* await express.bindAndStartExpress(express(), 8080);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare class WebpiecesExpressRouter {
|
|
27
|
+
private readonly apiFactory;
|
|
28
|
+
private readonly middleware;
|
|
29
|
+
constructor(apiFactory: ApiFactory);
|
|
30
|
+
/**
|
|
31
|
+
* Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,
|
|
32
|
+
* express-tier + api-tier filter chain, error→JSON) onto the caller's express app.
|
|
33
|
+
*
|
|
34
|
+
* Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose other
|
|
35
|
+
* routes must stay untouched. The caller owns app.listen() and any global middleware.
|
|
36
|
+
*/
|
|
37
|
+
bindExpress(app: Express): void;
|
|
38
|
+
/**
|
|
39
|
+
* Add the webpieces global middleware (HTML error page, localhost CORS, request logging),
|
|
40
|
+
* bind the routes, then app.listen(port). Convenience for a non-legacy webpieces server
|
|
41
|
+
* where webpieces owns the whole express app. Resolves with the http.Server once listening.
|
|
42
|
+
*/
|
|
43
|
+
bindAndStartExpress(app: Express, port?: number): Promise<HttpServer>;
|
|
44
|
+
/**
|
|
45
|
+
* Wrap one ApiClient's impl in an express handler (RequestContext.run, header read, manual
|
|
46
|
+
* JSON body parse, error→ProtocolError) and register it on the app for its method + path.
|
|
47
|
+
*/
|
|
48
|
+
private mountApiClient;
|
|
49
|
+
private registerHandler;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebpiecesExpressRouter = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
const WebpiecesMiddleware_1 = require("./WebpiecesMiddleware");
|
|
6
|
+
const log = core_util_1.LogManager.getLogger('WebpiecesExpressRouter');
|
|
7
|
+
/**
|
|
8
|
+
* WebpiecesExpressRouter - the express layer that sits ON TOP of a node-only
|
|
9
|
+
* {@link ApiFactory} (a WebpiecesRouter). It is the ONLY place express lifecycle lives.
|
|
10
|
+
*
|
|
11
|
+
* It never reaches into routing internals: it asks the ApiFactory for `apiClients()` — each
|
|
12
|
+
* an api + routeMeta + composed filter-chain→controller impl — and binds each to an express
|
|
13
|
+
* route (`app.<verb>(path, handler)`) invoked when the matching HTTP request arrives. The
|
|
14
|
+
* RouteBuilder stays hidden inside the ApiFactory.
|
|
15
|
+
*
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const apiFactory = await WebpiecesRouterFactory.create(config, { appBindings });
|
|
18
|
+
* apiFactory.addRoutes(SaveApi, SaveController);
|
|
19
|
+
* const express = new WebpiecesExpressRouter(apiFactory);
|
|
20
|
+
*
|
|
21
|
+
* // legacy / side-by-side: mount onto an existing app; you own listen + your middleware
|
|
22
|
+
* express.bindExpress(existingApp);
|
|
23
|
+
*
|
|
24
|
+
* // non-legacy: add webpieces global middleware + listen for you
|
|
25
|
+
* await express.bindAndStartExpress(express(), 8080);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
class WebpiecesExpressRouter {
|
|
29
|
+
apiFactory;
|
|
30
|
+
middleware = new WebpiecesMiddleware_1.WebpiecesMiddleware();
|
|
31
|
+
constructor(apiFactory) {
|
|
32
|
+
this.apiFactory = apiFactory;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,
|
|
36
|
+
* express-tier + api-tier filter chain, error→JSON) onto the caller's express app.
|
|
37
|
+
*
|
|
38
|
+
* Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose other
|
|
39
|
+
* routes must stay untouched. The caller owns app.listen() and any global middleware.
|
|
40
|
+
*/
|
|
41
|
+
bindExpress(app) {
|
|
42
|
+
const apiClients = this.apiFactory.apiClients();
|
|
43
|
+
for (const apiClient of apiClients) {
|
|
44
|
+
this.mountApiClient(app, apiClient);
|
|
45
|
+
}
|
|
46
|
+
log.info(`[WebpiecesExpressRouter] Mounted ${apiClients.length} webpieces route(s) onto express`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Add the webpieces global middleware (HTML error page, localhost CORS, request logging),
|
|
50
|
+
* bind the routes, then app.listen(port). Convenience for a non-legacy webpieces server
|
|
51
|
+
* where webpieces owns the whole express app. Resolves with the http.Server once listening.
|
|
52
|
+
*/
|
|
53
|
+
async bindAndStartExpress(app, port = 8080) {
|
|
54
|
+
// Global middleware layers (outermost first) — only for a webpieces-owned app.
|
|
55
|
+
app.use(this.middleware.globalErrorHandler.bind(this.middleware));
|
|
56
|
+
app.use(this.middleware.corsForLocalhost());
|
|
57
|
+
app.use(this.middleware.logNextLayer.bind(this.middleware));
|
|
58
|
+
this.bindExpress(app);
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const server = app.listen(port, (error) => {
|
|
61
|
+
if (error) {
|
|
62
|
+
log.error(`[WebpiecesExpressRouter] Failed to start on port ${port}:`, error);
|
|
63
|
+
reject(error);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
log.info(`[WebpiecesExpressRouter] Listening on http://localhost:${port}`);
|
|
67
|
+
resolve(server);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Wrap one ApiClient's impl in an express handler (RequestContext.run, header read, manual
|
|
73
|
+
* JSON body parse, error→ProtocolError) and register it on the app for its method + path.
|
|
74
|
+
*/
|
|
75
|
+
mountApiClient(app, apiClient) {
|
|
76
|
+
const wrapper = this.middleware.createExpressWrapper(apiClient.impl, apiClient.routeMeta);
|
|
77
|
+
this.registerHandler(app, apiClient.routeMeta.httpMethod, apiClient.routeMeta.path, wrapper.execute.bind(wrapper));
|
|
78
|
+
}
|
|
79
|
+
registerHandler(app, httpMethod, path, expressHandler) {
|
|
80
|
+
switch (httpMethod.toLowerCase()) {
|
|
81
|
+
case 'get':
|
|
82
|
+
app.get(path, expressHandler);
|
|
83
|
+
break;
|
|
84
|
+
case 'post':
|
|
85
|
+
app.post(path, expressHandler);
|
|
86
|
+
break;
|
|
87
|
+
case 'put':
|
|
88
|
+
app.put(path, expressHandler);
|
|
89
|
+
break;
|
|
90
|
+
case 'delete':
|
|
91
|
+
app.delete(path, expressHandler);
|
|
92
|
+
break;
|
|
93
|
+
case 'patch':
|
|
94
|
+
app.patch(path, expressHandler);
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
log.warn(`[WebpiecesExpressRouter] Unknown HTTP method: ${httpMethod}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.WebpiecesExpressRouter = WebpiecesExpressRouter;
|
|
102
|
+
//# sourceMappingURL=WebpiecesExpressRouter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebpiecesExpressRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesExpressRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Express } from 'express';\nimport { ApiFactory, ApiClient } from '@webpieces/http-routing';\nimport { LogManager } from '@webpieces/core-util';\nimport { WebpiecesMiddleware, ExpressRouteHandler } from './WebpiecesMiddleware';\n\nconst log = LogManager.getLogger('WebpiecesExpressRouter');\n\n/** The value returned by express `app.listen(...)` (a node http.Server). */\ntype HttpServer = ReturnType<Express['listen']>;\n\n/**\n * WebpiecesExpressRouter - the express layer that sits ON TOP of a node-only\n * {@link ApiFactory} (a WebpiecesRouter). It is the ONLY place express lifecycle lives.\n *\n * It never reaches into routing internals: it asks the ApiFactory for `apiClients()` — each\n * an api + routeMeta + composed filter-chain→controller impl — and binds each to an express\n * route (`app.<verb>(path, handler)`) invoked when the matching HTTP request arrives. The\n * RouteBuilder stays hidden inside the ApiFactory.\n *\n * ```typescript\n * const apiFactory = await WebpiecesRouterFactory.create(config, { appBindings });\n * apiFactory.addRoutes(SaveApi, SaveController);\n * const express = new WebpiecesExpressRouter(apiFactory);\n *\n * // legacy / side-by-side: mount onto an existing app; you own listen + your middleware\n * express.bindExpress(existingApp);\n *\n * // non-legacy: add webpieces global middleware + listen for you\n * await express.bindAndStartExpress(express(), 8080);\n * ```\n */\nexport class WebpiecesExpressRouter {\n private readonly middleware = new WebpiecesMiddleware();\n\n constructor(private readonly apiFactory: ApiFactory) {}\n\n /**\n * Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,\n * express-tier + api-tier filter chain, error→JSON) onto the caller's express app.\n *\n * Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose other\n * routes must stay untouched. The caller owns app.listen() and any global middleware.\n */\n bindExpress(app: Express): void {\n const apiClients = this.apiFactory.apiClients();\n for (const apiClient of apiClients) {\n this.mountApiClient(app, apiClient);\n }\n log.info(`[WebpiecesExpressRouter] Mounted ${apiClients.length} webpieces route(s) onto express`);\n }\n\n /**\n * Add the webpieces global middleware (HTML error page, localhost CORS, request logging),\n * bind the routes, then app.listen(port). Convenience for a non-legacy webpieces server\n * where webpieces owns the whole express app. Resolves with the http.Server once listening.\n */\n async bindAndStartExpress(app: Express, port: number = 8080): Promise<HttpServer> {\n // Global middleware layers (outermost first) — only for a webpieces-owned app.\n app.use(this.middleware.globalErrorHandler.bind(this.middleware));\n app.use(this.middleware.corsForLocalhost());\n app.use(this.middleware.logNextLayer.bind(this.middleware));\n\n this.bindExpress(app);\n\n return new Promise<HttpServer>(\n (resolve: (server: HttpServer) => void, reject: (err: Error) => void) => {\n const server: HttpServer = app.listen(port, (error?: Error) => {\n if (error) {\n log.error(`[WebpiecesExpressRouter] Failed to start on port ${port}:`, error);\n reject(error);\n return;\n }\n log.info(`[WebpiecesExpressRouter] Listening on http://localhost:${port}`);\n resolve(server);\n });\n },\n );\n }\n\n /**\n * Wrap one ApiClient's impl in an express handler (RequestContext.run, header read, manual\n * JSON body parse, error→ProtocolError) and register it on the app for its method + path.\n */\n private mountApiClient(app: Express, apiClient: ApiClient): void {\n const wrapper = this.middleware.createExpressWrapper(apiClient.impl, apiClient.routeMeta);\n this.registerHandler(\n app,\n apiClient.routeMeta.httpMethod,\n apiClient.routeMeta.path,\n wrapper.execute.bind(wrapper),\n );\n }\n\n private registerHandler(\n app: Express,\n httpMethod: string,\n path: string,\n expressHandler: ExpressRouteHandler,\n ): void {\n switch (httpMethod.toLowerCase()) {\n case 'get':\n app.get(path, expressHandler);\n break;\n case 'post':\n app.post(path, expressHandler);\n break;\n case 'put':\n app.put(path, expressHandler);\n break;\n case 'delete':\n app.delete(path, expressHandler);\n break;\n case 'patch':\n app.patch(path, expressHandler);\n break;\n default:\n log.warn(`[WebpiecesExpressRouter] Unknown HTTP method: ${httpMethod}`);\n }\n }\n}\n"]}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
2
|
import { MethodMeta } from '@webpieces/http-routing';
|
|
3
3
|
import { RouteMetadata } from '@webpieces/core-util';
|
|
4
|
-
import { Service, WpResponse } from '@webpieces/http-
|
|
4
|
+
import { Service, WpResponse } from '@webpieces/http-routing';
|
|
5
5
|
/**
|
|
6
6
|
* Express route handler function type. Lives in http-server (the express adapter),
|
|
7
7
|
* NOT in the node-only http-routing package, so http-routing stays express-free.
|
|
8
|
-
* Used by
|
|
8
|
+
* Used by WebpiecesExpressRouter to register handlers Express can call.
|
|
9
9
|
*/
|
|
10
10
|
export type ExpressRouteHandler = (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
11
11
|
export declare class ExpressWrapper {
|
|
@@ -46,8 +46,11 @@ class ExpressWrapper {
|
|
|
46
46
|
// Parse JSON
|
|
47
47
|
requestDto = bodyText ? JSON.parse(bodyText) : {};
|
|
48
48
|
}
|
|
49
|
-
// 3.
|
|
50
|
-
|
|
49
|
+
// 3. Publish the transport-neutral HttpRequest + fill the context (platform-header
|
|
50
|
+
// transfer + request id) ABOVE the boundary — the chain reads it, never express req.
|
|
51
|
+
core_context_1.RequestContext.setRequest(new core_context_1.HttpRequest(req.method, this.routeMeta.path, requestHeaders));
|
|
52
|
+
(0, http_routing_1.fillContext)();
|
|
53
|
+
const methodMeta = new http_routing_1.MethodMeta(this.routeMeta, requestDto);
|
|
51
54
|
// 4. Invoke the service (filter chain + controller)
|
|
52
55
|
const wpResponse = await this.service.invoke(methodMeta);
|
|
53
56
|
if (!wpResponse.response) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesMiddleware.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { Request, Response, NextFunction, RequestHandler } from 'express';\nimport cors from 'cors';\nimport { injectable } from 'inversify';\nimport { provideFrameworkSingleton, MethodMeta } from '@webpieces/http-routing';\nimport {\n ProtocolError,\n HttpError,\n HttpBadRequestError,\n HttpVendorError,\n HttpUserError,\n HttpNotFoundError,\n HttpTimeoutError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpInternalServerError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n RouteMetadata,\n} from '@webpieces/core-util';\nimport { Service, WpResponse } from '@webpieces/http-filters';\nimport { toError } from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('WebpiecesMiddleware');\n\n/**\n * Express route handler function type. Lives in http-server (the express adapter),\n * NOT in the node-only http-routing package, so http-routing stays express-free.\n * Used by WebpiecesRouteCreator to register handlers Express can call.\n */\nexport type ExpressRouteHandler = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => Promise<void>;\n\nexport class ExpressWrapper {\n constructor(\n private service: Service<MethodMeta, WpResponse<unknown>>,\n private routeMeta: RouteMetadata\n ) {\n }\n\n public async execute(req: Request, res: Response, next: NextFunction) {\n // MOVED: Wrap entire request in RequestContext.run()\n // This establishes AsyncLocalStorage context for the request\n await RequestContext.run(async () => {\n await this.executeTryCatch(req, res, next);\n });\n }\n\n public async executeTryCatch(req: Request, res: Response, next: NextFunction): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- ExpressWrapper catches errors to translate to HTTP responses\n try {\n await this.executeImpl(req, res, next);\n } catch (err: unknown) {\n const error = toError(err);\n // 5. Handle errors\n this.handleError(res, error);\n }\n }\n\n public async executeImpl(req: Request, res: Response, next: NextFunction) {\n // 1. Read HTTP headers from Express request\n const requestHeaders = this.readExpressHeaders(req);\n\n // 2. Parse JSON request body manually (SYMMETRIC with client's JSON.stringify)\n let requestDto: unknown = {};\n if (['POST', 'PUT', 'PATCH'].includes(req.method)) {\n // Read raw body as text\n const bodyText = await this.readRequestBody(req);\n // Parse JSON\n requestDto = bodyText ? JSON.parse(bodyText) : {};\n }\n\n // 3. Create MethodMeta with headers and request DTO\n const methodMeta = new MethodMeta(this.routeMeta, requestHeaders, requestDto);\n\n // 4. Invoke the service (filter chain + controller)\n const wpResponse = await this.service.invoke(methodMeta);\n if (!wpResponse.response) {\n throw new Error(\n `Route chain(filters & all) is not returning a response. ${this.routeMeta.controllerClassName}.${this.routeMeta.methodName}`\n );\n }\n\n // 5. Serialize response DTO to JSON (SYMMETRIC with client's response.json())\n const responseJson = JSON.stringify(wpResponse.response);\n res.status(200).setHeader('Content-Type', 'application/json').send(responseJson);\n }\n\n /**\n * Read HTTP headers from Express request.\n * Returns Map of header name (lowercase) -> array of values.\n *\n * HTTP spec allows multiple values for same header name.\n */\n private readExpressHeaders(req: Request): Map<string, string[]> {\n const headers = new Map<string, string[]>();\n\n // Express stores headers in req.headers as Record<string, string | string[]>\n for (const [name, value] of Object.entries(req.headers)) {\n const lowerName = name.toLowerCase();\n\n if (typeof value === 'string') {\n headers.set(lowerName, [value]);\n } else if (Array.isArray(value)) {\n headers.set(lowerName, value);\n }\n }\n\n return headers;\n }\n\n /**\n * Read raw request body as text.\n * Used to manually parse JSON (instead of express.json() middleware).\n */\n private async readRequestBody(req: Request): Promise<string> {\n return new Promise((resolve, reject) => {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk.toString();\n });\n req.on('end', () => {\n resolve(body);\n });\n req.on('error', (err) => {\n reject(err);\n });\n });\n }\n\n /**\n * Handle errors - translate to JSON ProtocolError (SYMMETRIC with ClientErrorTranslator).\n * PUBLIC so wrapExpress can call it for symmetric error handling.\n * Maps HttpError subclasses to appropriate HTTP status codes and ProtocolError response.\n *\n * Maps all HttpError types (must match ClientErrorTranslator.translateError()):\n * - HttpUserError → 266 (with errorCode)\n * - HttpBadRequestError → 400 (with field, guiAlertMessage)\n * - HttpUnauthorizedError → 401\n * - HttpForbiddenError → 403\n * - HttpNotFoundError → 404\n * - HttpTimeoutError → 408\n * - HttpInternalServerError → 500\n * - HttpBadGatewayError → 502\n * - HttpGatewayTimeoutError → 504\n * - HttpVendorError → 598 (with waitSeconds)\n */\n public handleError(res: Response, error: unknown): void {\n if (res.headersSent) {\n return;\n }\n\n const protocolError = new ProtocolError();\n\n if (error instanceof HttpError) {\n // Set common fields for all HttpError types\n protocolError.message = error.message;\n protocolError.subType = error.subType;\n protocolError.name = error.name;\n\n // Set type-specific fields (MUST match ClientErrorTranslator)\n if (error instanceof HttpUserError) {\n log.info(`[ExpressWrapper] User Error: ${error.message}`);\n protocolError.errorCode = error.errorCode;\n } else if (error instanceof HttpBadRequestError) {\n log.info(`[ExpressWrapper] Bad Request: ${error.message}`);\n protocolError.field = error.field;\n protocolError.guiAlertMessage = error.guiMessage;\n } else if (error instanceof HttpNotFoundError) {\n log.info(`[ExpressWrapper] Not Found: ${error.message}`);\n } else if (error instanceof HttpTimeoutError) {\n log.error(`[ExpressWrapper] Timeout Error: ${error.message}`);\n } else if (error instanceof HttpVendorError) {\n log.error(`[ExpressWrapper] Vendor Error: ${error.message}`);\n protocolError.waitSeconds = error.waitSeconds;\n } else if (error instanceof HttpUnauthorizedError) {\n log.info(`[ExpressWrapper] Unauthorized: ${error.message}`);\n } else if (error instanceof HttpForbiddenError) {\n log.info(`[ExpressWrapper] Forbidden: ${error.message}`);\n } else if (error instanceof HttpInternalServerError) {\n log.error(`[ExpressWrapper] Internal Server Error: ${error.message}`);\n } else if (error instanceof HttpBadGatewayError) {\n log.error(`[ExpressWrapper] Bad Gateway: ${error.message}`);\n } else if (error instanceof HttpGatewayTimeoutError) {\n log.error(`[ExpressWrapper] Gateway Timeout: ${error.message}`);\n } else {\n log.info(`[ExpressWrapper] Generic HttpError: ${error.message}`);\n }\n\n // Serialize ProtocolError to JSON (SYMMETRIC with client)\n const responseJson = JSON.stringify(protocolError);\n res.status(error.code).setHeader('Content-Type', 'application/json').send(responseJson);\n } else {\n // Unknown error - 500\n const err = toError(error);\n protocolError.message = 'Internal Server Error';\n log.error('[ExpressWrapper] Unexpected error:', err);\n const responseJson = JSON.stringify(protocolError);\n res.status(500).setHeader('Content-Type', 'application/json').send(responseJson);\n }\n }\n}\n\n/**\n * WebpiecesMiddleware - Express middleware for WebPieces server.\n *\n * This class contains all Express middleware used by WebpiecesServer:\n * 1. globalErrorHandler - Outermost error handler, returns HTML 500 page\n * 2. logNextLayer - Request/response logging\n * 3. jsonTranslator - JSON Content-Type validation and error translation\n *\n * The middleware is injected into WebpiecesServerImpl and registered with Express\n * in the start() method.\n *\n * IMPORTANT: jsonTranslator does NOT dispatch routes - route dispatch happens via\n * Express's registered route handlers (created by RouteBuilder.createHandler()).\n * jsonTranslator only validates Content-Type and translates errors to JSON.\n *\n * NEW: ExpressWrapper simplified - no longer handles JSON or headers\n * - JSON parsing/serialization moved to JsonFilter\n * - Header transfer moved to ContextFilter (injects PlatformHeadersExtension directly)\n * - ExpressWrapper just creates RouterReqResp and invokes filter chain\n *\n * Extension vs Plugin pattern:\n * - Extensions (DI-level): Contribute capabilities to framework (headers, converters, etc.)\n * - Plugins (App-level): Provide complete features with modules + routes (Hibernate, Jackson, etc.)\n */\n@provideFrameworkSingleton()\n@injectable()\nexport class WebpiecesMiddleware {\n\n /**\n * Global error handler middleware - catches ALL unhandled errors.\n * Returns HTML 500 error page for any errors that escape the filter chain.\n *\n * This is the outermost safety net - JsonTranslator catches JSON API errors,\n * this catches everything else.\n */\n async globalErrorHandler(\n req: Request,\n res: Response,\n next: NextFunction,\n ): Promise<void> {\n log.info(`🔴 [Layer 1: GlobalErrorHandler] Request START: ${req.method} ${req.path}`);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Global error handler IS the top-level catch-all\n try {\n // await next() catches BOTH:\n // 1. Synchronous throws from next() itself\n // 2. Rejected promises from downstream async middleware\n await next();\n log.info(\n `🔴 [Layer 1: GlobalErrorHandler] Request END (success): ${req.method} ${req.path}`,\n );\n } catch (err: unknown) {\n const error = toError(err);\n log.error('🔴 [Layer 1: GlobalErrorHandler] Caught unhandled error:', error);\n if (!res.headersSent) {\n // Return HTML error page (not JSON - JsonTranslator handles JSON errors)\n res.status(500).send(`\n <!DOCTYPE html>\n <html>\n <head><title>Server Error</title></head>\n <body>\n <h1>You hit a server error</h1>\n <p>An unexpected error occurred while processing your request.</p>\n <pre>${error.message}</pre>\n </body>\n </html>\n `);\n }\n log.info(\n `🔴 [Layer 1: GlobalErrorHandler] Request END (error): ${req.method} ${req.path}`,\n );\n }\n }\n\n /**\n * Logging middleware - logs request/response flow.\n * Demonstrates middleware execution order.\n * IMPORTANT: Must be async and await next() to properly chain with async middleware.\n */\n async logNextLayer(req: Request, res: Response, next: NextFunction): Promise<void> {\n log.info(`🟡 [Layer 2: LogNextLayer] Before next() - ${req.method} ${req.path}`);\n await next();\n log.info(`🟡 [Layer 2: LogNextLayer] After next() - ${req.method} ${req.path}`);\n }\n\n /**\n * CORS middleware for localhost development.\n * Only enables CORS when request origin is localhost:*.\n *\n * Wide open for all headers/methods in dev mode.\n * Non-localhost origins are blocked.\n *\n * @returns Express middleware handler for CORS\n */\n corsForLocalhost(): RequestHandler {\n log.info('[WebpiecesMiddleware] CORS enabled for localhost:* origins');\n\n return cors({\n origin: function (origin, callback) {\n // Allow requests with no origin (same-origin, Postman, curl)\n if (!origin) {\n callback(null, true);\n return;\n }\n\n // Only allow localhost origins\n if (origin.startsWith('http://localhost:') || origin.startsWith('https://localhost:')) {\n callback(null, true);\n } else {\n log.info(`[CORS] Blocked origin: ${origin} (only localhost:* allowed)`);\n callback(new Error(`CORS not allowed for origin: ${origin}`));\n }\n },\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],\n allowedHeaders: '*', // Wide open for dev\n exposedHeaders: '*', // Expose all response headers to browser JS\n maxAge: 3600,\n });\n }\n\n /**\n * Create an ExpressWrapper for a route.\n * The wrapper handles the full request/response cycle (symmetric design).\n *\n * NEW: Simplified - no longer passes headers (ContextFilter handles it now)\n *\n * @param service - The service wrapping the filter chain and controller\n * @param routeMeta - Route metadata for MethodMeta and DTO type\n * @returns ExpressWrapper instance\n */\n createExpressWrapper(\n service: Service<MethodMeta, WpResponse<unknown>>,\n routeMeta: RouteMetadata,\n ): ExpressWrapper {\n return new ExpressWrapper(service, routeMeta);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"WebpiecesMiddleware.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { Request, Response, NextFunction, RequestHandler } from 'express';\nimport cors from 'cors';\nimport { injectable } from 'inversify';\nimport { provideFrameworkSingleton, MethodMeta, fillContext } from '@webpieces/http-routing';\nimport {\n ProtocolError,\n HttpError,\n HttpBadRequestError,\n HttpVendorError,\n HttpUserError,\n HttpNotFoundError,\n HttpTimeoutError,\n HttpUnauthorizedError,\n HttpForbiddenError,\n HttpInternalServerError,\n HttpBadGatewayError,\n HttpGatewayTimeoutError,\n RouteMetadata,\n} from '@webpieces/core-util';\nimport { Service, WpResponse } from '@webpieces/http-routing';\nimport { toError } from '@webpieces/core-util';\nimport { RequestContext, HttpRequest } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('WebpiecesMiddleware');\n\n/**\n * Express route handler function type. Lives in http-server (the express adapter),\n * NOT in the node-only http-routing package, so http-routing stays express-free.\n * Used by WebpiecesExpressRouter to register handlers Express can call.\n */\nexport type ExpressRouteHandler = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => Promise<void>;\n\nexport class ExpressWrapper {\n constructor(\n private service: Service<MethodMeta, WpResponse<unknown>>,\n private routeMeta: RouteMetadata\n ) {\n }\n\n public async execute(req: Request, res: Response, next: NextFunction) {\n // MOVED: Wrap entire request in RequestContext.run()\n // This establishes AsyncLocalStorage context for the request\n await RequestContext.run(async () => {\n await this.executeTryCatch(req, res, next);\n });\n }\n\n public async executeTryCatch(req: Request, res: Response, next: NextFunction): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- ExpressWrapper catches errors to translate to HTTP responses\n try {\n await this.executeImpl(req, res, next);\n } catch (err: unknown) {\n const error = toError(err);\n // 5. Handle errors\n this.handleError(res, error);\n }\n }\n\n public async executeImpl(req: Request, res: Response, next: NextFunction): Promise<void> {\n // 1. Read HTTP headers from Express request\n const requestHeaders = this.readExpressHeaders(req);\n\n // 2. Parse JSON request body manually (SYMMETRIC with client's JSON.stringify)\n let requestDto: unknown = {};\n if (['POST', 'PUT', 'PATCH'].includes(req.method)) {\n // Read raw body as text\n const bodyText = await this.readRequestBody(req);\n // Parse JSON\n requestDto = bodyText ? JSON.parse(bodyText) : {};\n }\n\n // 3. Publish the transport-neutral HttpRequest + fill the context (platform-header\n // transfer + request id) ABOVE the boundary — the chain reads it, never express req.\n RequestContext.setRequest(new HttpRequest(req.method, this.routeMeta.path, requestHeaders));\n fillContext();\n const methodMeta = new MethodMeta(this.routeMeta, requestDto);\n\n // 4. Invoke the service (filter chain + controller)\n const wpResponse = await this.service.invoke(methodMeta);\n if (!wpResponse.response) {\n throw new Error(\n `Route chain(filters & all) is not returning a response. ${this.routeMeta.controllerClassName}.${this.routeMeta.methodName}`\n );\n }\n\n // 5. Serialize response DTO to JSON (SYMMETRIC with client's response.json())\n const responseJson = JSON.stringify(wpResponse.response);\n res.status(200).setHeader('Content-Type', 'application/json').send(responseJson);\n }\n\n /**\n * Read HTTP headers from Express request.\n * Returns Map of header name (lowercase) -> array of values.\n *\n * HTTP spec allows multiple values for same header name.\n */\n private readExpressHeaders(req: Request): Map<string, string[]> {\n const headers = new Map<string, string[]>();\n\n // Express stores headers in req.headers as Record<string, string | string[]>\n for (const [name, value] of Object.entries(req.headers)) {\n const lowerName = name.toLowerCase();\n\n if (typeof value === 'string') {\n headers.set(lowerName, [value]);\n } else if (Array.isArray(value)) {\n headers.set(lowerName, value);\n }\n }\n\n return headers;\n }\n\n /**\n * Read raw request body as text.\n * Used to manually parse JSON (instead of express.json() middleware).\n */\n private async readRequestBody(req: Request): Promise<string> {\n return new Promise((resolve, reject) => {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk.toString();\n });\n req.on('end', () => {\n resolve(body);\n });\n req.on('error', (err) => {\n reject(err);\n });\n });\n }\n\n /**\n * Handle errors - translate to JSON ProtocolError (SYMMETRIC with ClientErrorTranslator).\n * PUBLIC so wrapExpress can call it for symmetric error handling.\n * Maps HttpError subclasses to appropriate HTTP status codes and ProtocolError response.\n *\n * Maps all HttpError types (must match ClientErrorTranslator.translateError()):\n * - HttpUserError → 266 (with errorCode)\n * - HttpBadRequestError → 400 (with field, guiAlertMessage)\n * - HttpUnauthorizedError → 401\n * - HttpForbiddenError → 403\n * - HttpNotFoundError → 404\n * - HttpTimeoutError → 408\n * - HttpInternalServerError → 500\n * - HttpBadGatewayError → 502\n * - HttpGatewayTimeoutError → 504\n * - HttpVendorError → 598 (with waitSeconds)\n */\n public handleError(res: Response, error: unknown): void {\n if (res.headersSent) {\n return;\n }\n\n const protocolError = new ProtocolError();\n\n if (error instanceof HttpError) {\n // Set common fields for all HttpError types\n protocolError.message = error.message;\n protocolError.subType = error.subType;\n protocolError.name = error.name;\n\n // Set type-specific fields (MUST match ClientErrorTranslator)\n if (error instanceof HttpUserError) {\n log.info(`[ExpressWrapper] User Error: ${error.message}`);\n protocolError.errorCode = error.errorCode;\n } else if (error instanceof HttpBadRequestError) {\n log.info(`[ExpressWrapper] Bad Request: ${error.message}`);\n protocolError.field = error.field;\n protocolError.guiAlertMessage = error.guiMessage;\n } else if (error instanceof HttpNotFoundError) {\n log.info(`[ExpressWrapper] Not Found: ${error.message}`);\n } else if (error instanceof HttpTimeoutError) {\n log.error(`[ExpressWrapper] Timeout Error: ${error.message}`);\n } else if (error instanceof HttpVendorError) {\n log.error(`[ExpressWrapper] Vendor Error: ${error.message}`);\n protocolError.waitSeconds = error.waitSeconds;\n } else if (error instanceof HttpUnauthorizedError) {\n log.info(`[ExpressWrapper] Unauthorized: ${error.message}`);\n } else if (error instanceof HttpForbiddenError) {\n log.info(`[ExpressWrapper] Forbidden: ${error.message}`);\n } else if (error instanceof HttpInternalServerError) {\n log.error(`[ExpressWrapper] Internal Server Error: ${error.message}`);\n } else if (error instanceof HttpBadGatewayError) {\n log.error(`[ExpressWrapper] Bad Gateway: ${error.message}`);\n } else if (error instanceof HttpGatewayTimeoutError) {\n log.error(`[ExpressWrapper] Gateway Timeout: ${error.message}`);\n } else {\n log.info(`[ExpressWrapper] Generic HttpError: ${error.message}`);\n }\n\n // Serialize ProtocolError to JSON (SYMMETRIC with client)\n const responseJson = JSON.stringify(protocolError);\n res.status(error.code).setHeader('Content-Type', 'application/json').send(responseJson);\n } else {\n // Unknown error - 500\n const err = toError(error);\n protocolError.message = 'Internal Server Error';\n log.error('[ExpressWrapper] Unexpected error:', err);\n const responseJson = JSON.stringify(protocolError);\n res.status(500).setHeader('Content-Type', 'application/json').send(responseJson);\n }\n }\n}\n\n/**\n * WebpiecesMiddleware - Express middleware for WebPieces server.\n *\n * This class contains all Express middleware used by WebpiecesServer:\n * 1. globalErrorHandler - Outermost error handler, returns HTML 500 page\n * 2. logNextLayer - Request/response logging\n * 3. jsonTranslator - JSON Content-Type validation and error translation\n *\n * The middleware is injected into WebpiecesServerImpl and registered with Express\n * in the start() method.\n *\n * IMPORTANT: jsonTranslator does NOT dispatch routes - route dispatch happens via\n * Express's registered route handlers (created by RouteBuilder.createHandler()).\n * jsonTranslator only validates Content-Type and translates errors to JSON.\n *\n * NEW: ExpressWrapper simplified - no longer handles JSON or headers\n * - JSON parsing/serialization moved to JsonFilter\n * - Header transfer moved to ContextFilter (injects PlatformHeadersExtension directly)\n * - ExpressWrapper just creates RouterReqResp and invokes filter chain\n *\n * Extension vs Plugin pattern:\n * - Extensions (DI-level): Contribute capabilities to framework (headers, converters, etc.)\n * - Plugins (App-level): Provide complete features with modules + routes (Hibernate, Jackson, etc.)\n */\n@provideFrameworkSingleton()\n@injectable()\nexport class WebpiecesMiddleware {\n\n /**\n * Global error handler middleware - catches ALL unhandled errors.\n * Returns HTML 500 error page for any errors that escape the filter chain.\n *\n * This is the outermost safety net - JsonTranslator catches JSON API errors,\n * this catches everything else.\n */\n async globalErrorHandler(\n req: Request,\n res: Response,\n next: NextFunction,\n ): Promise<void> {\n log.info(`🔴 [Layer 1: GlobalErrorHandler] Request START: ${req.method} ${req.path}`);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Global error handler IS the top-level catch-all\n try {\n // await next() catches BOTH:\n // 1. Synchronous throws from next() itself\n // 2. Rejected promises from downstream async middleware\n await next();\n log.info(\n `🔴 [Layer 1: GlobalErrorHandler] Request END (success): ${req.method} ${req.path}`,\n );\n } catch (err: unknown) {\n const error = toError(err);\n log.error('🔴 [Layer 1: GlobalErrorHandler] Caught unhandled error:', error);\n if (!res.headersSent) {\n // Return HTML error page (not JSON - JsonTranslator handles JSON errors)\n res.status(500).send(`\n <!DOCTYPE html>\n <html>\n <head><title>Server Error</title></head>\n <body>\n <h1>You hit a server error</h1>\n <p>An unexpected error occurred while processing your request.</p>\n <pre>${error.message}</pre>\n </body>\n </html>\n `);\n }\n log.info(\n `🔴 [Layer 1: GlobalErrorHandler] Request END (error): ${req.method} ${req.path}`,\n );\n }\n }\n\n /**\n * Logging middleware - logs request/response flow.\n * Demonstrates middleware execution order.\n * IMPORTANT: Must be async and await next() to properly chain with async middleware.\n */\n async logNextLayer(req: Request, res: Response, next: NextFunction): Promise<void> {\n log.info(`🟡 [Layer 2: LogNextLayer] Before next() - ${req.method} ${req.path}`);\n await next();\n log.info(`🟡 [Layer 2: LogNextLayer] After next() - ${req.method} ${req.path}`);\n }\n\n /**\n * CORS middleware for localhost development.\n * Only enables CORS when request origin is localhost:*.\n *\n * Wide open for all headers/methods in dev mode.\n * Non-localhost origins are blocked.\n *\n * @returns Express middleware handler for CORS\n */\n corsForLocalhost(): RequestHandler {\n log.info('[WebpiecesMiddleware] CORS enabled for localhost:* origins');\n\n return cors({\n origin: function (origin, callback) {\n // Allow requests with no origin (same-origin, Postman, curl)\n if (!origin) {\n callback(null, true);\n return;\n }\n\n // Only allow localhost origins\n if (origin.startsWith('http://localhost:') || origin.startsWith('https://localhost:')) {\n callback(null, true);\n } else {\n log.info(`[CORS] Blocked origin: ${origin} (only localhost:* allowed)`);\n callback(new Error(`CORS not allowed for origin: ${origin}`));\n }\n },\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],\n allowedHeaders: '*', // Wide open for dev\n exposedHeaders: '*', // Expose all response headers to browser JS\n maxAge: 3600,\n });\n }\n\n /**\n * Create an ExpressWrapper for a route.\n * The wrapper handles the full request/response cycle (symmetric design).\n *\n * NEW: Simplified - no longer passes headers (ContextFilter handles it now)\n *\n * @param service - The service wrapping the filter chain and controller\n * @param routeMeta - Route metadata for MethodMeta and DTO type\n * @returns ExpressWrapper instance\n */\n createExpressWrapper(\n service: Service<MethodMeta, WpResponse<unknown>>,\n routeMeta: RouteMetadata,\n ): ExpressWrapper {\n return new ExpressWrapper(service, routeMeta);\n }\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MethodMeta } from '@webpieces/http-routing';
|
|
2
|
-
import { Filter, WpResponse, Service } from '@webpieces/http-
|
|
2
|
+
import { Filter, WpResponse, Service } from '@webpieces/http-routing';
|
|
3
3
|
export declare class LogApiFilter extends Filter<MethodMeta, WpResponse<unknown>> {
|
|
4
4
|
private logApiCall;
|
|
5
5
|
private headerMethods;
|
|
@@ -4,7 +4,7 @@ exports.LogApiFilter = void 0;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const inversify_1 = require("inversify");
|
|
6
6
|
const http_routing_1 = require("@webpieces/http-routing");
|
|
7
|
-
const
|
|
7
|
+
const http_routing_2 = require("@webpieces/http-routing");
|
|
8
8
|
const core_util_1 = require("@webpieces/core-util");
|
|
9
9
|
const core_util_2 = require("@webpieces/core-util");
|
|
10
10
|
/**
|
|
@@ -24,7 +24,7 @@ const core_util_2 = require("@webpieces/core-util");
|
|
|
24
24
|
* because they are expected behavior from the server's perspective.
|
|
25
25
|
*/
|
|
26
26
|
const log = core_util_1.LogManager.getLogger('LogApiFilter');
|
|
27
|
-
let LogApiFilter = class LogApiFilter extends
|
|
27
|
+
let LogApiFilter = class LogApiFilter extends http_routing_2.Filter {
|
|
28
28
|
logApiCall;
|
|
29
29
|
headerMethods = new core_util_2.HeaderMethods();
|
|
30
30
|
loggedKeys;
|
|
@@ -46,7 +46,7 @@ let LogApiFilter = class LogApiFilter extends http_filters_1.Filter {
|
|
|
46
46
|
return wpResponse.response;
|
|
47
47
|
};
|
|
48
48
|
const response = await this.logApiCall.execute("SVR", meta.routeMeta, meta.requestDto, headers, method);
|
|
49
|
-
return new
|
|
49
|
+
return new http_routing_2.WpResponse(response);
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
exports.LogApiFilter = LogApiFilter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LogApiFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/filters/LogApiFilter.ts"],"names":[],"mappings":";;;;AAAA,yCAAuC;AACvC,0DAAoG;AACpG,0DAAsE;AACtE,oDAAkD;AAClD,oDAK8B;AAE9B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;AAI1C,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,qBAAuC;IAC7D,UAAU,CAAa;IACvB,aAAa,GAAG,IAAI,yBAAa,EAAE,CAAC;IACpC,UAAU,CAAe;IAEjC;QACI,KAAK,EAAE,CAAC;QAER,4EAA4E;QAC5E,4DAA4D;QAC5D,IAAI,CAAC,UAAU,GAAG,0BAAc,CAAC,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC;QAEvD,GAAG,CAAC,IAAI,CAAC,wBAAwB,IAAI,CAAC,UAAU,CAAC,MAAM,0CAA0C,CAAC,CAAC;QAEnG,IAAI,CAAC,UAAU,GAAG,IAAI,sBAAU,EAAE,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,MAAM,CACR,IAAgB,EAChB,UAAoD;QAEpD,gFAAgF;QAChF,MAAM,aAAa,GAAG,IAAI,mCAAoB,EAAE,CAAC;QACjD,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,qBAAqB,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;QAEzF,+DAA+D;QAC/D,MAAM,MAAM,GAAG,KAAK,IAAsB,EAAE;YACxC,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACjD,OAAO,UAAU,CAAC,QAAQ,CAAC;QAC/B,CAAC,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACxG,OAAO,IAAI,yBAAU,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;CACJ,CAAA;AAlCY,oCAAY;uBAAZ,YAAY;IAFxB,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;;GACA,YAAY,CAkCxB","sourcesContent":["import { injectable } from 'inversify';\nimport {provideFrameworkSingleton, MethodMeta, RequestContextReader} from '@webpieces/http-routing';\nimport { Filter, WpResponse, Service } from '@webpieces/http-
|
|
1
|
+
{"version":3,"file":"LogApiFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/filters/LogApiFilter.ts"],"names":[],"mappings":";;;;AAAA,yCAAuC;AACvC,0DAAoG;AACpG,0DAAsE;AACtE,oDAAkD;AAClD,oDAK8B;AAE9B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;AAI1C,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,qBAAuC;IAC7D,UAAU,CAAa;IACvB,aAAa,GAAG,IAAI,yBAAa,EAAE,CAAC;IACpC,UAAU,CAAe;IAEjC;QACI,KAAK,EAAE,CAAC;QAER,4EAA4E;QAC5E,4DAA4D;QAC5D,IAAI,CAAC,UAAU,GAAG,0BAAc,CAAC,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC;QAEvD,GAAG,CAAC,IAAI,CAAC,wBAAwB,IAAI,CAAC,UAAU,CAAC,MAAM,0CAA0C,CAAC,CAAC;QAEnG,IAAI,CAAC,UAAU,GAAG,IAAI,sBAAU,EAAE,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,MAAM,CACR,IAAgB,EAChB,UAAoD;QAEpD,gFAAgF;QAChF,MAAM,aAAa,GAAG,IAAI,mCAAoB,EAAE,CAAC;QACjD,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,qBAAqB,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;QAEzF,+DAA+D;QAC/D,MAAM,MAAM,GAAG,KAAK,IAAsB,EAAE;YACxC,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACjD,OAAO,UAAU,CAAC,QAAQ,CAAC;QAC/B,CAAC,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACxG,OAAO,IAAI,yBAAU,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;CACJ,CAAA;AAlCY,oCAAY;uBAAZ,YAAY;IAFxB,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;;GACA,YAAY,CAkCxB","sourcesContent":["import { injectable } from 'inversify';\nimport {provideFrameworkSingleton, MethodMeta, RequestContextReader} from '@webpieces/http-routing';\nimport { Filter, WpResponse, Service } from '@webpieces/http-routing';\nimport { LogManager } from '@webpieces/core-util';\nimport {\n ContextKey,\n HeaderMethods,\n HeaderRegistry,\n LogApiCall,\n} from '@webpieces/core-util';\n\n/**\n * LogApiFilter - Structured API logging for all requests/responses.\n * Priority: 1800 (after ContextFilter at 2000, before custom filters)\n *\n * Logging patterns (via LogApiCall):\n * - [API-SVR-req] Class.method /url request={...} headers={...}\n * - [API-SVR-resp-SUCCESS] Class.method response={...}\n * - [API-SVR-resp-FAIL] Class.method error=... (server errors: 500, 502, 504)\n * - [API-SVR-resp-OTHER] Class.method errorType=... (user errors: 400, 401, 403, 404, 266)\n *\n * Headers are read from RequestContext (NOT from meta.requestHeaders which is undefined\n * after ContextFilter runs at priority 2000).\n *\n * User errors (HttpBadRequestError, etc.) are logged as OTHER, not FAIL,\n * because they are expected behavior from the server's perspective.\n */\nconst log = LogManager.getLogger('LogApiFilter');\n\n@provideFrameworkSingleton()\n@injectable()\nexport class LogApiFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n private logApiCall: LogApiCall;\n private headerMethods = new HeaderMethods();\n private loggedKeys: ContextKey[];\n\n constructor() {\n super();\n\n // The global registry is the single source of truth (configured at startup,\n // duplicate-validated). Log map keys off each key's `name`.\n this.loggedKeys = HeaderRegistry.get().getLoggedKeys();\n\n log.info(`[LogApiFilter] Using ${this.loggedKeys.length} logged context keys from HeaderRegistry`);\n\n this.logApiCall = new LogApiCall();\n }\n\n async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n // Build log map from RequestContext (keys already transferred by ContextFilter)\n const contextReader = new RequestContextReader();\n const headers = this.headerMethods.buildSecureMapForLogs(this.loggedKeys, contextReader);\n\n // Wrap nextFilter.invoke in a method that returns the response\n const method = async (): Promise<unknown> => {\n const wpResponse = await nextFilter.invoke(meta);\n return wpResponse.response;\n };\n\n const response = await this.logApiCall.execute(\"SVR\", meta.routeMeta, meta.requestDto, headers, method);\n return new WpResponse(response);\n }\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MethodMeta, WebpiecesConfig } from '@webpieces/http-routing';
|
|
2
|
-
import { Filter, WpResponse, Service } from '@webpieces/http-
|
|
2
|
+
import { Filter, WpResponse, Service } from '@webpieces/http-routing';
|
|
3
3
|
/**
|
|
4
4
|
* RecordingFilter - Records a request as a replayable test case (port of Java
|
|
5
5
|
* RecordingFilter).
|
|
@@ -5,7 +5,7 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const inversify_1 = require("inversify");
|
|
6
6
|
const http_routing_1 = require("@webpieces/http-routing");
|
|
7
7
|
const core_context_1 = require("@webpieces/core-context");
|
|
8
|
-
const
|
|
8
|
+
const http_routing_2 = require("@webpieces/http-routing");
|
|
9
9
|
const core_util_1 = require("@webpieces/core-util");
|
|
10
10
|
const TestCaseRecorderImpl_1 = require("../recorder/TestCaseRecorderImpl");
|
|
11
11
|
/**
|
|
@@ -26,7 +26,7 @@ const TestCaseRecorderImpl_1 = require("../recorder/TestCaseRecorderImpl");
|
|
|
26
26
|
* Recording NEVER alters the response - failures inside the recorder are
|
|
27
27
|
* caught and logged.
|
|
28
28
|
*/
|
|
29
|
-
let RecordingFilter = class RecordingFilter extends
|
|
29
|
+
let RecordingFilter = class RecordingFilter extends http_routing_2.Filter {
|
|
30
30
|
config;
|
|
31
31
|
headerMethods = new core_util_1.HeaderMethods();
|
|
32
32
|
constructor(config) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecordingFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/filters/RecordingFilter.ts"],"names":[],"mappings":";;;;AAAA,yCAA+C;AAC/C,0DAMiC;AACjC,0DAAyD;AACzD,0DAAsE;AACtE,oDAQ8B;AAE9B,2EAAwE;AAExE;;;;;;;;;;;;;;;;;GAiBG;AAII,IAAM,eAAe,GAArB,MAAM,eAAgB,SAAQ,qBAAuC;IAI5B;IAHpC,aAAa,GAAG,IAAI,yBAAa,EAAE,CAAC;IAE5C,YAC4C,MAAuB;QAE/D,KAAK,EAAE,CAAC;QAFgC,WAAM,GAAN,MAAM,CAAiB;IAGnE,CAAC;IAED,sGAAsG;IACtG,KAAK,CAAC,MAAM,CACR,IAAgB,EAChB,UAAoD;QAEpD,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,EAAE,CAAC;YAC/B,OAAO,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,2CAAoB,EAAE,CAAC;QAC5C,6BAAc,CAAC,SAAS,CAAC,wBAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAE1D,MAAM,cAAc,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;QAEtD,4HAA4H;QAC5H,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC/C,cAAc,CAAC,eAAe,GAAG,QAAQ,CAAC,QAAQ,CAAC;YACnD,OAAO,QAAQ,CAAC;QACpB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,cAAc,CAAC,eAAe,GAAG,IAAI,yBAAa,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YAC9E,MAAM,GAAG,CAAC;QACd,CAAC;gBAAS,CAAC;YACP,6BAAc,CAAC,MAAM,CAAC,wBAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAClD,QAAQ,CAAC,eAAe,CAAC,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QACvE,CAAC;IACL,CAAC;IAEO,oBAAoB;QACxB,IAAI,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,4EAA4E;QAC5E,OAAO,6BAAc,CAAC,SAAS,CAAC,gCAAoB,CAAC,SAAS,CAAC,CAAC;IACpE,CAAC;IAEO,mBAAmB,CAAC,IAAgB;QACxC,8EAA8E;QAC9E,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,qBAAqB,CAAC,0BAAc,CAAC,GAAG,EAAE,CAAC,aAAa,EAAE,EAAE,IAAI,mCAAoB,EAAE,CAAC,CAAC;QAC1H,MAAM,WAAW,GAA2B,EAAE,CAAC;QAC/C,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;YACnC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrC,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,mBAAmB,IAAI,mBAAmB,CAAC;QAC1E,OAAO,IAAI,4BAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,WAAW,CAAC,CAAC;IAC1F,CAAC;CACJ,CAAA;AAzDY,0CAAe;0BAAf,eAAe;IAH3B,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;IACb,sGAAsG;;IAK7F,mBAAA,IAAA,kBAAM,EAAC,qCAAsB,CAAC,CAAA;6CAAiB,8BAAe;GAJ1D,eAAe,CAyD3B","sourcesContent":["import { inject, injectable } from 'inversify';\nimport {\n provideFrameworkSingleton,\n MethodMeta,\n RequestContextReader,\n WebpiecesConfig,\n WEBPIECES_CONFIG_TOKEN,\n} from '@webpieces/http-routing';\nimport { RequestContext } from '@webpieces/core-context';\nimport { Filter, WpResponse, Service } from '@webpieces/http-
|
|
1
|
+
{"version":3,"file":"RecordingFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/filters/RecordingFilter.ts"],"names":[],"mappings":";;;;AAAA,yCAA+C;AAC/C,0DAMiC;AACjC,0DAAyD;AACzD,0DAAsE;AACtE,oDAQ8B;AAE9B,2EAAwE;AAExE;;;;;;;;;;;;;;;;;GAiBG;AAII,IAAM,eAAe,GAArB,MAAM,eAAgB,SAAQ,qBAAuC;IAI5B;IAHpC,aAAa,GAAG,IAAI,yBAAa,EAAE,CAAC;IAE5C,YAC4C,MAAuB;QAE/D,KAAK,EAAE,CAAC;QAFgC,WAAM,GAAN,MAAM,CAAiB;IAGnE,CAAC;IAED,sGAAsG;IACtG,KAAK,CAAC,MAAM,CACR,IAAgB,EAChB,UAAoD;QAEpD,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,EAAE,CAAC;YAC/B,OAAO,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,2CAAoB,EAAE,CAAC;QAC5C,6BAAc,CAAC,SAAS,CAAC,wBAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAE1D,MAAM,cAAc,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;QAEtD,4HAA4H;QAC5H,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC/C,cAAc,CAAC,eAAe,GAAG,QAAQ,CAAC,QAAQ,CAAC;YACnD,OAAO,QAAQ,CAAC;QACpB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,cAAc,CAAC,eAAe,GAAG,IAAI,yBAAa,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YAC9E,MAAM,GAAG,CAAC;QACd,CAAC;gBAAS,CAAC;YACP,6BAAc,CAAC,MAAM,CAAC,wBAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAClD,QAAQ,CAAC,eAAe,CAAC,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QACvE,CAAC;IACL,CAAC;IAEO,oBAAoB;QACxB,IAAI,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,4EAA4E;QAC5E,OAAO,6BAAc,CAAC,SAAS,CAAC,gCAAoB,CAAC,SAAS,CAAC,CAAC;IACpE,CAAC;IAEO,mBAAmB,CAAC,IAAgB;QACxC,8EAA8E;QAC9E,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,qBAAqB,CAAC,0BAAc,CAAC,GAAG,EAAE,CAAC,aAAa,EAAE,EAAE,IAAI,mCAAoB,EAAE,CAAC,CAAC;QAC1H,MAAM,WAAW,GAA2B,EAAE,CAAC;QAC/C,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;YACnC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrC,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,mBAAmB,IAAI,mBAAmB,CAAC;QAC1E,OAAO,IAAI,4BAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,WAAW,CAAC,CAAC;IAC1F,CAAC;CACJ,CAAA;AAzDY,0CAAe;0BAAf,eAAe;IAH3B,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;IACb,sGAAsG;;IAK7F,mBAAA,IAAA,kBAAM,EAAC,qCAAsB,CAAC,CAAA;6CAAiB,8BAAe;GAJ1D,eAAe,CAyD3B","sourcesContent":["import { inject, injectable } from 'inversify';\nimport {\n provideFrameworkSingleton,\n MethodMeta,\n RequestContextReader,\n WebpiecesConfig,\n WEBPIECES_CONFIG_TOKEN,\n} from '@webpieces/http-routing';\nimport { RequestContext } from '@webpieces/core-context';\nimport { Filter, WpResponse, Service } from '@webpieces/http-routing';\nimport {\n HeaderMethods,\n HeaderRegistry,\n RecordedEndpoint,\n RecordedError,\n RecorderKeys,\n WebpiecesCoreHeaders,\n toError,\n} from '@webpieces/core-util';\n\nimport { TestCaseRecorderImpl } from '../recorder/TestCaseRecorderImpl';\n\n/**\n * RecordingFilter - Records a request as a replayable test case (port of Java\n * RecordingFilter).\n *\n * Suggested priority: 1850 (after ContextFilter 2000 so the RECORDING header\n * has been transferred to the context, after AuthFilter 1900 so only real\n * authorized flows are recorded, before LogApiFilter 1800).\n *\n * Activates when WebpiecesConfig.recordingAlwaysOn is set OR the request\n * carries WebpiecesCoreHeaders.RECORDING (x-webpieces-recording). While\n * active, a TestCaseRecorderImpl travels in the RequestContext under\n * RecorderKeys.RECORDER; the http-client proxy and recordable() wrappers add\n * every downstream call. On completion the fixture + generated spec are\n * logged (and written to config.recordingDir when set).\n *\n * Recording NEVER alters the response - failures inside the recorder are\n * caught and logged.\n */\n@provideFrameworkSingleton()\n@injectable()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response type flexibility\nexport class RecordingFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n private headerMethods = new HeaderMethods();\n\n constructor(\n @inject(WEBPIECES_CONFIG_TOKEN) private config: WebpiecesConfig,\n ) {\n super();\n }\n\n // webpieces-disable no-any-unknown -- Filter generic params use unknown for response type flexibility\n async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n if (!this.isRecordingRequested()) {\n return await nextFilter.invoke(meta);\n }\n\n const recorder = new TestCaseRecorderImpl();\n RequestContext.putHeader(RecorderKeys.RECORDER, recorder);\n\n const serverEndpoint = this.buildServerEndpoint(meta);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- capture failure into the recording, then rethrow unchanged\n try {\n const response = await nextFilter.invoke(meta);\n serverEndpoint.successResponse = response.response;\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n serverEndpoint.failureResponse = new RecordedError(error.name, error.message);\n throw err;\n } finally {\n RequestContext.remove(RecorderKeys.RECORDER.name);\n recorder.spitOutTestCase(serverEndpoint, this.config.recordingDir);\n }\n }\n\n private isRecordingRequested(): boolean {\n if (this.config.recordingAlwaysOn) {\n return true;\n }\n // ContextFilter (priority 2000) already transferred the header into context\n return RequestContext.hasHeader(WebpiecesCoreHeaders.RECORDING);\n }\n\n private buildServerEndpoint(meta: MethodMeta): RecordedEndpoint {\n // Masked snapshot of the magic context (secured values masked, keyed by name)\n const logMap = this.headerMethods.buildSecureMapForLogs(HeaderRegistry.get().getLoggedKeys(), new RequestContextReader());\n const ctxSnapshot: Record<string, string> = {};\n for (const entry of logMap.entries()) {\n ctxSnapshot[entry[0]] = entry[1];\n }\n\n const apiName = meta.routeMeta.controllerClassName ?? 'UnknownController';\n return new RecordedEndpoint(apiName, meta.methodName, [meta.requestDto], ctxSnapshot);\n }\n}\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { WebpiecesExpressRouter } from './WebpiecesExpressRouter';
|
|
2
2
|
export { WebpiecesMiddleware } from './WebpiecesMiddleware';
|
|
3
|
-
export { WebpiecesRouteCreator } from './WebpiecesRouteCreator';
|
|
4
|
-
export { InProcessApiClientFactory } from '@webpieces/http-routing';
|
|
5
|
-
export { ContextFilter } from './filters/ContextFilter';
|
|
6
3
|
export { LogApiFilter } from './filters/LogApiFilter';
|
|
7
4
|
export { RecordingFilter } from './filters/RecordingFilter';
|
|
8
|
-
export { ServiceAuthFilter } from './filters/ServiceAuthFilter';
|
|
9
5
|
export { TestCaseRecorderImpl } from './recorder/TestCaseRecorderImpl';
|
|
10
6
|
export { SpecGenerator } from './recorder/SpecGenerator';
|
|
11
7
|
export { recordable } from './recorder/recordable';
|
|
12
8
|
export { WebpiecesCoreHeaders } from './headers/WebpiecesCoreHeaders';
|
|
13
9
|
export { HeaderRegistry } from '@webpieces/core-util';
|
|
14
|
-
export { RouteHandler, MethodMeta,
|
|
10
|
+
export { RouteHandler, MethodMeta, HttpFilter, FilterMatcher, FilterDefinition, ApiFactory, ApiClient, HttpRequest, AuthConfig, Principal, } from '@webpieces/http-routing';
|
|
15
11
|
export { ExpressRouteHandler } from './WebpiecesMiddleware';
|
package/src/index.js
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
// Express adapter (the only place express lifecycle lives) over the node-only
|
|
5
|
-
var
|
|
6
|
-
Object.defineProperty(exports, "
|
|
3
|
+
exports.Principal = exports.AuthConfig = exports.HttpRequest = exports.ApiClient = exports.FilterDefinition = exports.FilterMatcher = exports.MethodMeta = exports.RouteHandler = exports.HeaderRegistry = exports.WebpiecesCoreHeaders = exports.recordable = exports.SpecGenerator = exports.TestCaseRecorderImpl = exports.RecordingFilter = exports.LogApiFilter = exports.WebpiecesMiddleware = exports.WebpiecesExpressRouter = void 0;
|
|
4
|
+
// Express adapter (the only place express lifecycle lives) over the node-only ApiFactory
|
|
5
|
+
var WebpiecesExpressRouter_1 = require("./WebpiecesExpressRouter");
|
|
6
|
+
Object.defineProperty(exports, "WebpiecesExpressRouter", { enumerable: true, get: function () { return WebpiecesExpressRouter_1.WebpiecesExpressRouter; } });
|
|
7
7
|
var WebpiecesMiddleware_1 = require("./WebpiecesMiddleware");
|
|
8
8
|
Object.defineProperty(exports, "WebpiecesMiddleware", { enumerable: true, get: function () { return WebpiecesMiddleware_1.WebpiecesMiddleware; } });
|
|
9
|
-
var WebpiecesRouteCreator_1 = require("./WebpiecesRouteCreator");
|
|
10
|
-
Object.defineProperty(exports, "WebpiecesRouteCreator", { enumerable: true, get: function () { return WebpiecesRouteCreator_1.WebpiecesRouteCreator; } });
|
|
11
|
-
// InProcessApiClientFactory moved to node-only http-routing; re-export for back-compat
|
|
12
|
-
var http_routing_1 = require("@webpieces/http-routing");
|
|
13
|
-
Object.defineProperty(exports, "InProcessApiClientFactory", { enumerable: true, get: function () { return http_routing_1.InProcessApiClientFactory; } });
|
|
14
|
-
var ContextFilter_1 = require("./filters/ContextFilter");
|
|
15
|
-
Object.defineProperty(exports, "ContextFilter", { enumerable: true, get: function () { return ContextFilter_1.ContextFilter; } });
|
|
16
9
|
var LogApiFilter_1 = require("./filters/LogApiFilter");
|
|
17
10
|
Object.defineProperty(exports, "LogApiFilter", { enumerable: true, get: function () { return LogApiFilter_1.LogApiFilter; } });
|
|
18
11
|
var RecordingFilter_1 = require("./filters/RecordingFilter");
|
|
19
12
|
Object.defineProperty(exports, "RecordingFilter", { enumerable: true, get: function () { return RecordingFilter_1.RecordingFilter; } });
|
|
20
|
-
var ServiceAuthFilter_1 = require("./filters/ServiceAuthFilter");
|
|
21
|
-
Object.defineProperty(exports, "ServiceAuthFilter", { enumerable: true, get: function () { return ServiceAuthFilter_1.ServiceAuthFilter; } });
|
|
22
13
|
// Test-case recording (contract lives in @webpieces/core-util)
|
|
23
14
|
var TestCaseRecorderImpl_1 = require("./recorder/TestCaseRecorderImpl");
|
|
24
15
|
Object.defineProperty(exports, "TestCaseRecorderImpl", { enumerable: true, get: function () { return TestCaseRecorderImpl_1.TestCaseRecorderImpl; } });
|
|
@@ -31,14 +22,14 @@ var WebpiecesCoreHeaders_1 = require("./headers/WebpiecesCoreHeaders");
|
|
|
31
22
|
Object.defineProperty(exports, "WebpiecesCoreHeaders", { enumerable: true, get: function () { return WebpiecesCoreHeaders_1.WebpiecesCoreHeaders; } });
|
|
32
23
|
var core_util_1 = require("@webpieces/core-util");
|
|
33
24
|
Object.defineProperty(exports, "HeaderRegistry", { enumerable: true, get: function () { return core_util_1.HeaderRegistry; } });
|
|
34
|
-
// Re-export from http-routing for
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Object.defineProperty(exports, "
|
|
38
|
-
Object.defineProperty(exports, "
|
|
39
|
-
Object.defineProperty(exports, "
|
|
40
|
-
Object.defineProperty(exports, "
|
|
41
|
-
Object.defineProperty(exports, "
|
|
42
|
-
Object.defineProperty(exports, "
|
|
43
|
-
Object.defineProperty(exports, "
|
|
25
|
+
// Re-export from http-routing for one-import adapter ergonomics
|
|
26
|
+
var http_routing_1 = require("@webpieces/http-routing");
|
|
27
|
+
Object.defineProperty(exports, "RouteHandler", { enumerable: true, get: function () { return http_routing_1.RouteHandler; } });
|
|
28
|
+
Object.defineProperty(exports, "MethodMeta", { enumerable: true, get: function () { return http_routing_1.MethodMeta; } });
|
|
29
|
+
Object.defineProperty(exports, "FilterMatcher", { enumerable: true, get: function () { return http_routing_1.FilterMatcher; } });
|
|
30
|
+
Object.defineProperty(exports, "FilterDefinition", { enumerable: true, get: function () { return http_routing_1.FilterDefinition; } });
|
|
31
|
+
Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return http_routing_1.ApiClient; } });
|
|
32
|
+
Object.defineProperty(exports, "HttpRequest", { enumerable: true, get: function () { return http_routing_1.HttpRequest; } });
|
|
33
|
+
Object.defineProperty(exports, "AuthConfig", { enumerable: true, get: function () { return http_routing_1.AuthConfig; } });
|
|
34
|
+
Object.defineProperty(exports, "Principal", { enumerable: true, get: function () { return http_routing_1.Principal; } });
|
|
44
35
|
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/index.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/index.ts"],"names":[],"mappings":";;;AAAA,yFAAyF;AACzF,mEAAkE;AAAzD,gIAAA,sBAAsB,OAAA;AAC/B,6DAA4D;AAAnD,0HAAA,mBAAmB,OAAA;AAC5B,uDAAsD;AAA7C,4GAAA,YAAY,OAAA;AACrB,6DAA4D;AAAnD,kHAAA,eAAe,OAAA;AAExB,+DAA+D;AAC/D,wEAAuE;AAA9D,4HAAA,oBAAoB,OAAA;AAC7B,0DAAyD;AAAhD,8GAAA,aAAa,OAAA;AACtB,oDAAmD;AAA1C,wGAAA,UAAU,OAAA;AAEnB,mEAAmE;AACnE,uEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAC7B,kDAAsD;AAA7C,2GAAA,cAAc,OAAA;AAEvB,gEAAgE;AAChE,wDAWiC;AAV7B,4GAAA,YAAY,OAAA;AACZ,0GAAA,UAAU,OAAA;AAEV,6GAAA,aAAa,OAAA;AACb,gHAAA,gBAAgB,OAAA;AAEhB,yGAAA,SAAS,OAAA;AACT,2GAAA,WAAW,OAAA;AACX,0GAAA,UAAU,OAAA;AACV,yGAAA,SAAS,OAAA","sourcesContent":["// Express adapter (the only place express lifecycle lives) over the node-only ApiFactory\nexport { WebpiecesExpressRouter } from './WebpiecesExpressRouter';\nexport { WebpiecesMiddleware } from './WebpiecesMiddleware';\nexport { LogApiFilter } from './filters/LogApiFilter';\nexport { RecordingFilter } from './filters/RecordingFilter';\n\n// Test-case recording (contract lives in @webpieces/core-util)\nexport { TestCaseRecorderImpl } from './recorder/TestCaseRecorderImpl';\nexport { SpecGenerator } from './recorder/SpecGenerator';\nexport { recordable } from './recorder/recordable';\n\n// Context keys + registry (the global magic-context header system)\nexport { WebpiecesCoreHeaders } from './headers/WebpiecesCoreHeaders';\nexport { HeaderRegistry } from '@webpieces/core-util';\n\n// Re-export from http-routing for one-import adapter ergonomics\nexport {\n RouteHandler,\n MethodMeta,\n HttpFilter,\n FilterMatcher,\n FilterDefinition,\n ApiFactory,\n ApiClient,\n HttpRequest,\n AuthConfig,\n Principal,\n} from '@webpieces/http-routing';\n// ExpressRouteHandler now lives in http-server (express adapter), not node-only http-routing\nexport { ExpressRouteHandler } from './WebpiecesMiddleware';\n"]}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Express } from 'express';
|
|
2
|
-
import { WebpiecesRouter } from '@webpieces/http-routing';
|
|
3
|
-
/** The value returned by express `app.listen(...)` (a node http.Server). */
|
|
4
|
-
type HttpServer = ReturnType<Express['listen']>;
|
|
5
|
-
/**
|
|
6
|
-
* WebpiecesExpress - the express adapter (the ONLY place express lifecycle lives).
|
|
7
|
-
*
|
|
8
|
-
* Wraps a node-only {@link WebpiecesRouter} and binds its routes + filter chain onto an
|
|
9
|
-
* express app that the CALLER owns. Webpieces never constructs express and (except for the
|
|
10
|
-
* opt-in bindAndStartExpress) never calls listen — so a webpieces app can run side-by-side
|
|
11
|
-
* inside a legacy express server.
|
|
12
|
-
*
|
|
13
|
-
* ```typescript
|
|
14
|
-
* const router = await WebpiecesRouterFactory.create(config, { modules });
|
|
15
|
-
* router.addRoutes(SaveApi, SaveController);
|
|
16
|
-
* const server = new WebpiecesExpress(router);
|
|
17
|
-
*
|
|
18
|
-
* // legacy / side-by-side: mount onto an existing app; you own listen + your middleware
|
|
19
|
-
* server.bindExpress(existingApp);
|
|
20
|
-
*
|
|
21
|
-
* // non-legacy: add webpieces global middleware + listen for you
|
|
22
|
-
* await server.bindAndStartExpress(express(), 8080);
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
export declare class WebpiecesExpress {
|
|
26
|
-
private readonly router;
|
|
27
|
-
private readonly middleware;
|
|
28
|
-
constructor(router: WebpiecesRouter);
|
|
29
|
-
/**
|
|
30
|
-
* Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,
|
|
31
|
-
* express-tier + api-tier filter chain, error->JSON) onto the caller's express app.
|
|
32
|
-
*
|
|
33
|
-
* Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose
|
|
34
|
-
* other routes must stay untouched. The caller owns app.listen() and any global middleware.
|
|
35
|
-
*/
|
|
36
|
-
bindExpress(app: Express): void;
|
|
37
|
-
/**
|
|
38
|
-
* Add the webpieces global middleware (HTML error page, localhost CORS, request logging),
|
|
39
|
-
* bind the routes, then app.listen(port). Convenience for a non-legacy webpieces server
|
|
40
|
-
* where webpieces owns the whole express app. Resolves with the http.Server once listening.
|
|
41
|
-
*/
|
|
42
|
-
bindAndStartExpress(app: Express, port?: number): Promise<HttpServer>;
|
|
43
|
-
}
|
|
44
|
-
export {};
|
package/src/WebpiecesExpress.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WebpiecesExpress = void 0;
|
|
4
|
-
const WebpiecesMiddleware_1 = require("./WebpiecesMiddleware");
|
|
5
|
-
const WebpiecesRouteCreator_1 = require("./WebpiecesRouteCreator");
|
|
6
|
-
const core_util_1 = require("@webpieces/core-util");
|
|
7
|
-
const log = core_util_1.LogManager.getLogger('WebpiecesExpress');
|
|
8
|
-
/**
|
|
9
|
-
* WebpiecesExpress - the express adapter (the ONLY place express lifecycle lives).
|
|
10
|
-
*
|
|
11
|
-
* Wraps a node-only {@link WebpiecesRouter} and binds its routes + filter chain onto an
|
|
12
|
-
* express app that the CALLER owns. Webpieces never constructs express and (except for the
|
|
13
|
-
* opt-in bindAndStartExpress) never calls listen — so a webpieces app can run side-by-side
|
|
14
|
-
* inside a legacy express server.
|
|
15
|
-
*
|
|
16
|
-
* ```typescript
|
|
17
|
-
* const router = await WebpiecesRouterFactory.create(config, { modules });
|
|
18
|
-
* router.addRoutes(SaveApi, SaveController);
|
|
19
|
-
* const server = new WebpiecesExpress(router);
|
|
20
|
-
*
|
|
21
|
-
* // legacy / side-by-side: mount onto an existing app; you own listen + your middleware
|
|
22
|
-
* server.bindExpress(existingApp);
|
|
23
|
-
*
|
|
24
|
-
* // non-legacy: add webpieces global middleware + listen for you
|
|
25
|
-
* await server.bindAndStartExpress(express(), 8080);
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
class WebpiecesExpress {
|
|
29
|
-
router;
|
|
30
|
-
middleware = new WebpiecesMiddleware_1.WebpiecesMiddleware();
|
|
31
|
-
constructor(router) {
|
|
32
|
-
this.router = router;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,
|
|
36
|
-
* express-tier + api-tier filter chain, error->JSON) onto the caller's express app.
|
|
37
|
-
*
|
|
38
|
-
* Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose
|
|
39
|
-
* other routes must stay untouched. The caller owns app.listen() and any global middleware.
|
|
40
|
-
*/
|
|
41
|
-
bindExpress(app) {
|
|
42
|
-
const creator = new WebpiecesRouteCreator_1.WebpiecesRouteCreator(app, this.router.getContainer(), this.router.getRouteBuilder(), this.middleware);
|
|
43
|
-
const count = creator.mountRegisteredRoutes();
|
|
44
|
-
log.info(`[WebpiecesExpress] Mounted ${count} webpieces route(s) onto express`);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Add the webpieces global middleware (HTML error page, localhost CORS, request logging),
|
|
48
|
-
* bind the routes, then app.listen(port). Convenience for a non-legacy webpieces server
|
|
49
|
-
* where webpieces owns the whole express app. Resolves with the http.Server once listening.
|
|
50
|
-
*/
|
|
51
|
-
async bindAndStartExpress(app, port = 8080) {
|
|
52
|
-
// Global middleware layers (outermost first) — only for a webpieces-owned app.
|
|
53
|
-
app.use(this.middleware.globalErrorHandler.bind(this.middleware));
|
|
54
|
-
app.use(this.middleware.corsForLocalhost());
|
|
55
|
-
app.use(this.middleware.logNextLayer.bind(this.middleware));
|
|
56
|
-
this.bindExpress(app);
|
|
57
|
-
return new Promise((resolve, reject) => {
|
|
58
|
-
const server = app.listen(port, (error) => {
|
|
59
|
-
if (error) {
|
|
60
|
-
log.error(`[WebpiecesExpress] Failed to start on port ${port}:`, error);
|
|
61
|
-
reject(error);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
log.info(`[WebpiecesExpress] Listening on http://localhost:${port}`);
|
|
65
|
-
resolve(server);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
exports.WebpiecesExpress = WebpiecesExpress;
|
|
71
|
-
//# sourceMappingURL=WebpiecesExpress.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesExpress.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesExpress.ts"],"names":[],"mappings":";;;AAEA,+DAA4D;AAC5D,mEAAgE;AAChE,oDAAkD;AAElD,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;AAKrD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAa,gBAAgB;IAGI;IAFZ,UAAU,GAAG,IAAI,yCAAmB,EAAE,CAAC;IAExD,YAA6B,MAAuB;QAAvB,WAAM,GAAN,MAAM,CAAiB;IAAG,CAAC;IAExD;;;;;;OAMG;IACH,WAAW,CAAC,GAAY;QACpB,MAAM,OAAO,GAAG,IAAI,6CAAqB,CACrC,GAAG,EACH,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,EAC1B,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,EAC7B,IAAI,CAAC,UAAU,CAClB,CAAC;QACF,MAAM,KAAK,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;QAC9C,GAAG,CAAC,IAAI,CAAC,8BAA8B,KAAK,kCAAkC,CAAC,CAAC;IACpF,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,mBAAmB,CAAC,GAAY,EAAE,OAAe,IAAI;QACvD,+EAA+E;QAC/E,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAClE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAC5C,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAE5D,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAEtB,OAAO,IAAI,OAAO,CACd,CAAC,OAAqC,EAAE,MAA4B,EAAE,EAAE;YACpE,MAAM,MAAM,GAAe,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAa,EAAE,EAAE;gBAC1D,IAAI,KAAK,EAAE,CAAC;oBACR,GAAG,CAAC,KAAK,CAAC,8CAA8C,IAAI,GAAG,EAAE,KAAK,CAAC,CAAC;oBACxE,MAAM,CAAC,KAAK,CAAC,CAAC;oBACd,OAAO;gBACX,CAAC;gBACD,GAAG,CAAC,IAAI,CAAC,oDAAoD,IAAI,EAAE,CAAC,CAAC;gBACrE,OAAO,CAAC,MAAM,CAAC,CAAC;YACpB,CAAC,CAAC,CAAC;QACP,CAAC,CACJ,CAAC;IACN,CAAC;CACJ;AAlDD,4CAkDC","sourcesContent":["import { Express } from 'express';\nimport { WebpiecesRouter } from '@webpieces/http-routing';\nimport { WebpiecesMiddleware } from './WebpiecesMiddleware';\nimport { WebpiecesRouteCreator } from './WebpiecesRouteCreator';\nimport { LogManager } from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('WebpiecesExpress');\n\n/** The value returned by express `app.listen(...)` (a node http.Server). */\ntype HttpServer = ReturnType<Express['listen']>;\n\n/**\n * WebpiecesExpress - the express adapter (the ONLY place express lifecycle lives).\n *\n * Wraps a node-only {@link WebpiecesRouter} and binds its routes + filter chain onto an\n * express app that the CALLER owns. Webpieces never constructs express and (except for the\n * opt-in bindAndStartExpress) never calls listen — so a webpieces app can run side-by-side\n * inside a legacy express server.\n *\n * ```typescript\n * const router = await WebpiecesRouterFactory.create(config, { modules });\n * router.addRoutes(SaveApi, SaveController);\n * const server = new WebpiecesExpress(router);\n *\n * // legacy / side-by-side: mount onto an existing app; you own listen + your middleware\n * server.bindExpress(existingApp);\n *\n * // non-legacy: add webpieces global middleware + listen for you\n * await server.bindAndStartExpress(express(), 8080);\n * ```\n */\nexport class WebpiecesExpress {\n private readonly middleware = new WebpiecesMiddleware();\n\n constructor(private readonly router: WebpiecesRouter) {}\n\n /**\n * Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,\n * express-tier + api-tier filter chain, error->JSON) onto the caller's express app.\n *\n * Adds NO global app.use() middleware, so it is safe to attach to a legacy app whose\n * other routes must stay untouched. The caller owns app.listen() and any global middleware.\n */\n bindExpress(app: Express): void {\n const creator = new WebpiecesRouteCreator(\n app,\n this.router.getContainer(),\n this.router.getRouteBuilder(),\n this.middleware,\n );\n const count = creator.mountRegisteredRoutes();\n log.info(`[WebpiecesExpress] Mounted ${count} webpieces route(s) onto express`);\n }\n\n /**\n * Add the webpieces global middleware (HTML error page, localhost CORS, request logging),\n * bind the routes, then app.listen(port). Convenience for a non-legacy webpieces server\n * where webpieces owns the whole express app. Resolves with the http.Server once listening.\n */\n async bindAndStartExpress(app: Express, port: number = 8080): Promise<HttpServer> {\n // Global middleware layers (outermost first) — only for a webpieces-owned app.\n app.use(this.middleware.globalErrorHandler.bind(this.middleware));\n app.use(this.middleware.corsForLocalhost());\n app.use(this.middleware.logNextLayer.bind(this.middleware));\n\n this.bindExpress(app);\n\n return new Promise<HttpServer>(\n (resolve: (server: HttpServer) => void, reject: (err: Error) => void) => {\n const server: HttpServer = app.listen(port, (error?: Error) => {\n if (error) {\n log.error(`[WebpiecesExpress] Failed to start on port ${port}:`, error);\n reject(error);\n return;\n }\n log.info(`[WebpiecesExpress] Listening on http://localhost:${port}`);\n resolve(server);\n });\n },\n );\n }\n}\n"]}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { Express } from 'express';
|
|
2
|
-
import { Container } from 'inversify';
|
|
3
|
-
import { ClassType, FilterDefinition, RouteBuilderImpl } from '@webpieces/http-routing';
|
|
4
|
-
import { WebpiecesMiddleware } from './WebpiecesMiddleware';
|
|
5
|
-
export declare class WebpiecesRouteCreator {
|
|
6
|
-
private app;
|
|
7
|
-
private routeBuilder;
|
|
8
|
-
private middleware;
|
|
9
|
-
private clientFactory;
|
|
10
|
-
/** Locks wireFilters() once the first wireApi() has composed a filter chain. */
|
|
11
|
-
private apisWired;
|
|
12
|
-
/**
|
|
13
|
-
* @param app - The Express app to mount routes on (yours - never taken over)
|
|
14
|
-
* @param container - Inversify container used to resolve controllers and filters
|
|
15
|
-
* @param routeBuilder - Internal: WebpiecesExpress passes its DI singleton; standalone users omit
|
|
16
|
-
* @param middleware - Internal: WebpiecesExpress passes its DI singleton; standalone users omit
|
|
17
|
-
*/
|
|
18
|
-
constructor(app: Express, container: Container, routeBuilder?: RouteBuilderImpl, middleware?: WebpiecesMiddleware);
|
|
19
|
-
/**
|
|
20
|
-
* Register filters that wrap every matching route (glob pattern vs controller filepath).
|
|
21
|
-
* Must be called before the first wireApi() - filter chains are composed per-route.
|
|
22
|
-
*/
|
|
23
|
-
wireFilters(...defs: FilterDefinition[]): void;
|
|
24
|
-
/**
|
|
25
|
-
* Wire an API prototype class (with @ApiPath/@Endpoint decorators) to its
|
|
26
|
-
* controller, mounting one Express route per endpoint with the full filter
|
|
27
|
-
* chain. The controller is resolved from the Inversify container.
|
|
28
|
-
*/
|
|
29
|
-
wireApi<TApi, TController extends TApi>(apiPrototype: ClassType<TApi>, controllerClass: ClassType<TController>): void;
|
|
30
|
-
/**
|
|
31
|
-
* Mount every route currently registered on the RouteBuilder.
|
|
32
|
-
* Used by WebpiecesExpress (the full-server path) where routes were registered
|
|
33
|
-
* up front on the shared RouteBuilder.
|
|
34
|
-
*
|
|
35
|
-
* @returns Number of routes mounted
|
|
36
|
-
*/
|
|
37
|
-
mountRegisteredRoutes(): number;
|
|
38
|
-
/**
|
|
39
|
-
* Create an in-process API client (full filter chain + controller, no HTTP).
|
|
40
|
-
* Same testing story as WebpiecesServer.createApiClient().
|
|
41
|
-
*/
|
|
42
|
-
createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;
|
|
43
|
-
/**
|
|
44
|
-
* Escape hatch for advanced wiring (e.g. addRoute with a hand-built RouteDefinition).
|
|
45
|
-
*/
|
|
46
|
-
getRouteBuilder(): RouteBuilderImpl;
|
|
47
|
-
/**
|
|
48
|
-
* Compose the filter chain for one route and register it on the Express app.
|
|
49
|
-
*/
|
|
50
|
-
private mountRoute;
|
|
51
|
-
private registerHandler;
|
|
52
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WebpiecesRouteCreator = void 0;
|
|
4
|
-
const http_routing_1 = require("@webpieces/http-routing");
|
|
5
|
-
const WebpiecesMiddleware_1 = require("./WebpiecesMiddleware");
|
|
6
|
-
const core_util_1 = require("@webpieces/core-util");
|
|
7
|
-
/**
|
|
8
|
-
* WebpiecesRouteCreator - Embeddable adapter that mounts the webpieces
|
|
9
|
-
* api -> filters -> controller pipeline onto ANY existing Express app.
|
|
10
|
-
*
|
|
11
|
-
* Legacy Express apps can adopt webpieces incrementally: existing routes and
|
|
12
|
-
* middleware keep working untouched; each wired webpieces route is fully
|
|
13
|
-
* self-contained (own body parsing, own RequestContext, own error->JSON mapping).
|
|
14
|
-
* This class never calls app.use() - it only registers per-route handlers.
|
|
15
|
-
*
|
|
16
|
-
* Usage (the legacy-server example app builds the container via the shared
|
|
17
|
-
* setupCompanyRuntime + router.getContainer(); a bare standalone user can also
|
|
18
|
-
* assemble one directly):
|
|
19
|
-
* ```typescript
|
|
20
|
-
* const app = express(); // your existing legacy app
|
|
21
|
-
* const container = new Container();
|
|
22
|
-
* await container.load(buildFrameworkModule()); // webpieces framework classes (ContextFilter, ...)
|
|
23
|
-
* await container.load(buildProviderModule()); // your @provideSingleton controllers/filters
|
|
24
|
-
*
|
|
25
|
-
* const creator = new WebpiecesRouteCreator(app, container);
|
|
26
|
-
* creator.wireFilters(
|
|
27
|
-
* new FilterDefinition(2000, ContextFilter, '*'),
|
|
28
|
-
* new FilterDefinition(1900, AuthFilter, 'src/controllers/admin/**'),
|
|
29
|
-
* );
|
|
30
|
-
* creator.wireApi(SaveApi, SaveController); // controller resolved from container
|
|
31
|
-
* creator.wireApi(PublicApi, PublicController);
|
|
32
|
-
* app.listen(8080);
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* Notes:
|
|
36
|
-
* - ALL wireFilters() calls must come BEFORE the first wireApi() call. Filter
|
|
37
|
-
* chains are composed per-route at wireApi time, so late filters would be
|
|
38
|
-
* silently ignored - we throw instead.
|
|
39
|
-
* - Scoped filter glob patterns match the controller filepath from the
|
|
40
|
-
* @SourceFile decorator, falling back to the pattern `**\/{ClassName}.ts`.
|
|
41
|
-
* - Want webpieces' localhost CORS? Opt in yourself:
|
|
42
|
-
* `app.use(new WebpiecesMiddleware().corsForLocalhost())`.
|
|
43
|
-
*
|
|
44
|
-
* This same class is used internally by WebpiecesExpress (the full-server express
|
|
45
|
-
* adapter), so the full server and the embeddable adapter share one code path.
|
|
46
|
-
*/
|
|
47
|
-
const log = core_util_1.LogManager.getLogger('WebpiecesRouteCreator');
|
|
48
|
-
class WebpiecesRouteCreator {
|
|
49
|
-
app;
|
|
50
|
-
routeBuilder;
|
|
51
|
-
middleware;
|
|
52
|
-
clientFactory;
|
|
53
|
-
/** Locks wireFilters() once the first wireApi() has composed a filter chain. */
|
|
54
|
-
apisWired = false;
|
|
55
|
-
/**
|
|
56
|
-
* @param app - The Express app to mount routes on (yours - never taken over)
|
|
57
|
-
* @param container - Inversify container used to resolve controllers and filters
|
|
58
|
-
* @param routeBuilder - Internal: WebpiecesExpress passes its DI singleton; standalone users omit
|
|
59
|
-
* @param middleware - Internal: WebpiecesExpress passes its DI singleton; standalone users omit
|
|
60
|
-
*/
|
|
61
|
-
constructor(app, container, routeBuilder, middleware) {
|
|
62
|
-
this.app = app;
|
|
63
|
-
this.routeBuilder = routeBuilder ?? new http_routing_1.RouteBuilderImpl();
|
|
64
|
-
this.routeBuilder.setContainer(container);
|
|
65
|
-
this.middleware = middleware ?? new WebpiecesMiddleware_1.WebpiecesMiddleware();
|
|
66
|
-
this.clientFactory = new http_routing_1.InProcessApiClientFactory(this.routeBuilder);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Register filters that wrap every matching route (glob pattern vs controller filepath).
|
|
70
|
-
* Must be called before the first wireApi() - filter chains are composed per-route.
|
|
71
|
-
*/
|
|
72
|
-
wireFilters(...defs) {
|
|
73
|
-
if (this.apisWired) {
|
|
74
|
-
throw new Error('wireFilters() must be called before wireApi() - filter chains are composed per-route at wireApi time, so filters added later would never run.');
|
|
75
|
-
}
|
|
76
|
-
for (const def of defs) {
|
|
77
|
-
this.routeBuilder.addFilter(def);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Wire an API prototype class (with @ApiPath/@Endpoint decorators) to its
|
|
82
|
-
* controller, mounting one Express route per endpoint with the full filter
|
|
83
|
-
* chain. The controller is resolved from the Inversify container.
|
|
84
|
-
*/
|
|
85
|
-
wireApi(apiPrototype, controllerClass) {
|
|
86
|
-
this.apisWired = true;
|
|
87
|
-
// Reuses all existing validation: @ApiPath present, controller extends
|
|
88
|
-
// api prototype, every endpoint implemented + has @Authentication.
|
|
89
|
-
const factory = new http_routing_1.ApiRoutingFactory(apiPrototype, controllerClass);
|
|
90
|
-
// Mount only the routes added by THIS call
|
|
91
|
-
const routesBefore = this.routeBuilder.getRoutes().length;
|
|
92
|
-
factory.configure(this.routeBuilder);
|
|
93
|
-
const routes = this.routeBuilder.getRoutes();
|
|
94
|
-
for (let i = routesBefore; i < routes.length; i++) {
|
|
95
|
-
this.mountRoute(routes[i]);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Mount every route currently registered on the RouteBuilder.
|
|
100
|
-
* Used by WebpiecesExpress (the full-server path) where routes were registered
|
|
101
|
-
* up front on the shared RouteBuilder.
|
|
102
|
-
*
|
|
103
|
-
* @returns Number of routes mounted
|
|
104
|
-
*/
|
|
105
|
-
mountRegisteredRoutes() {
|
|
106
|
-
const routes = this.routeBuilder.getRoutes();
|
|
107
|
-
for (const routeWithMeta of routes) {
|
|
108
|
-
this.mountRoute(routeWithMeta);
|
|
109
|
-
}
|
|
110
|
-
return routes.length;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Create an in-process API client (full filter chain + controller, no HTTP).
|
|
114
|
-
* Same testing story as WebpiecesServer.createApiClient().
|
|
115
|
-
*/
|
|
116
|
-
// webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args
|
|
117
|
-
createApiClient(apiPrototype) {
|
|
118
|
-
return this.clientFactory.createApiClient(apiPrototype);
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Escape hatch for advanced wiring (e.g. addRoute with a hand-built RouteDefinition).
|
|
122
|
-
*/
|
|
123
|
-
getRouteBuilder() {
|
|
124
|
-
return this.routeBuilder;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Compose the filter chain for one route and register it on the Express app.
|
|
128
|
-
*/
|
|
129
|
-
mountRoute(routeWithMeta) {
|
|
130
|
-
const service = this.routeBuilder.createRouteHandler(routeWithMeta);
|
|
131
|
-
const routeMeta = routeWithMeta.definition.routeMeta;
|
|
132
|
-
// ExpressWrapper handles the full request/response cycle per route:
|
|
133
|
-
// RequestContext.run, header read, manual JSON body parse, error->ProtocolError
|
|
134
|
-
const wrapper = this.middleware.createExpressWrapper(service, routeMeta);
|
|
135
|
-
this.registerHandler(routeMeta.httpMethod, routeMeta.path, wrapper.execute.bind(wrapper));
|
|
136
|
-
}
|
|
137
|
-
registerHandler(httpMethod, path, expressHandler) {
|
|
138
|
-
switch (httpMethod.toLowerCase()) {
|
|
139
|
-
case 'get':
|
|
140
|
-
this.app.get(path, expressHandler);
|
|
141
|
-
break;
|
|
142
|
-
case 'post':
|
|
143
|
-
this.app.post(path, expressHandler);
|
|
144
|
-
break;
|
|
145
|
-
case 'put':
|
|
146
|
-
this.app.put(path, expressHandler);
|
|
147
|
-
break;
|
|
148
|
-
case 'delete':
|
|
149
|
-
this.app.delete(path, expressHandler);
|
|
150
|
-
break;
|
|
151
|
-
case 'patch':
|
|
152
|
-
this.app.patch(path, expressHandler);
|
|
153
|
-
break;
|
|
154
|
-
default:
|
|
155
|
-
log.warn(`[WebpiecesRouteCreator] Unknown HTTP method: ${httpMethod}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
exports.WebpiecesRouteCreator = WebpiecesRouteCreator;
|
|
160
|
-
//# sourceMappingURL=WebpiecesRouteCreator.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesRouteCreator.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesRouteCreator.ts"],"names":[],"mappings":"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","sourcesContent":["import { Express } from 'express';\nimport { Container } from 'inversify';\nimport {\n ApiRoutingFactory,\n ClassType,\n FilterDefinition,\n RouteBuilderImpl,\n RouteHandlerWithMeta,\n InProcessApiClientFactory,\n} from '@webpieces/http-routing';\nimport { WebpiecesMiddleware, ExpressRouteHandler } from './WebpiecesMiddleware';\nimport { LogManager } from '@webpieces/core-util';\n\n/**\n * WebpiecesRouteCreator - Embeddable adapter that mounts the webpieces\n * api -> filters -> controller pipeline onto ANY existing Express app.\n *\n * Legacy Express apps can adopt webpieces incrementally: existing routes and\n * middleware keep working untouched; each wired webpieces route is fully\n * self-contained (own body parsing, own RequestContext, own error->JSON mapping).\n * This class never calls app.use() - it only registers per-route handlers.\n *\n * Usage (the legacy-server example app builds the container via the shared\n * setupCompanyRuntime + router.getContainer(); a bare standalone user can also\n * assemble one directly):\n * ```typescript\n * const app = express(); // your existing legacy app\n * const container = new Container();\n * await container.load(buildFrameworkModule()); // webpieces framework classes (ContextFilter, ...)\n * await container.load(buildProviderModule()); // your @provideSingleton controllers/filters\n *\n * const creator = new WebpiecesRouteCreator(app, container);\n * creator.wireFilters(\n * new FilterDefinition(2000, ContextFilter, '*'),\n * new FilterDefinition(1900, AuthFilter, 'src/controllers/admin/**'),\n * );\n * creator.wireApi(SaveApi, SaveController); // controller resolved from container\n * creator.wireApi(PublicApi, PublicController);\n * app.listen(8080);\n * ```\n *\n * Notes:\n * - ALL wireFilters() calls must come BEFORE the first wireApi() call. Filter\n * chains are composed per-route at wireApi time, so late filters would be\n * silently ignored - we throw instead.\n * - Scoped filter glob patterns match the controller filepath from the\n * @SourceFile decorator, falling back to the pattern `**\\/{ClassName}.ts`.\n * - Want webpieces' localhost CORS? Opt in yourself:\n * `app.use(new WebpiecesMiddleware().corsForLocalhost())`.\n *\n * This same class is used internally by WebpiecesExpress (the full-server express\n * adapter), so the full server and the embeddable adapter share one code path.\n */\nconst log = LogManager.getLogger('WebpiecesRouteCreator');\n\nexport class WebpiecesRouteCreator {\n private routeBuilder: RouteBuilderImpl;\n private middleware: WebpiecesMiddleware;\n private clientFactory: InProcessApiClientFactory;\n\n /** Locks wireFilters() once the first wireApi() has composed a filter chain. */\n private apisWired = false;\n\n /**\n * @param app - The Express app to mount routes on (yours - never taken over)\n * @param container - Inversify container used to resolve controllers and filters\n * @param routeBuilder - Internal: WebpiecesExpress passes its DI singleton; standalone users omit\n * @param middleware - Internal: WebpiecesExpress passes its DI singleton; standalone users omit\n */\n constructor(\n private app: Express,\n container: Container,\n routeBuilder?: RouteBuilderImpl,\n middleware?: WebpiecesMiddleware,\n ) {\n this.routeBuilder = routeBuilder ?? new RouteBuilderImpl();\n this.routeBuilder.setContainer(container);\n this.middleware = middleware ?? new WebpiecesMiddleware();\n this.clientFactory = new InProcessApiClientFactory(this.routeBuilder);\n }\n\n /**\n * Register filters that wrap every matching route (glob pattern vs controller filepath).\n * Must be called before the first wireApi() - filter chains are composed per-route.\n */\n wireFilters(...defs: FilterDefinition[]): void {\n if (this.apisWired) {\n throw new Error(\n 'wireFilters() must be called before wireApi() - filter chains are composed per-route at wireApi time, so filters added later would never run.',\n );\n }\n for (const def of defs) {\n this.routeBuilder.addFilter(def);\n }\n }\n\n /**\n * Wire an API prototype class (with @ApiPath/@Endpoint decorators) to its\n * controller, mounting one Express route per endpoint with the full filter\n * chain. The controller is resolved from the Inversify container.\n */\n wireApi<TApi, TController extends TApi>(\n apiPrototype: ClassType<TApi>,\n controllerClass: ClassType<TController>,\n ): void {\n this.apisWired = true;\n\n // Reuses all existing validation: @ApiPath present, controller extends\n // api prototype, every endpoint implemented + has @Authentication.\n const factory = new ApiRoutingFactory(apiPrototype, controllerClass);\n\n // Mount only the routes added by THIS call\n const routesBefore = this.routeBuilder.getRoutes().length;\n factory.configure(this.routeBuilder);\n const routes = this.routeBuilder.getRoutes();\n\n for (let i = routesBefore; i < routes.length; i++) {\n this.mountRoute(routes[i]);\n }\n }\n\n /**\n * Mount every route currently registered on the RouteBuilder.\n * Used by WebpiecesExpress (the full-server path) where routes were registered\n * up front on the shared RouteBuilder.\n *\n * @returns Number of routes mounted\n */\n mountRegisteredRoutes(): number {\n const routes = this.routeBuilder.getRoutes();\n for (const routeWithMeta of routes) {\n this.mountRoute(routeWithMeta);\n }\n return routes.length;\n }\n\n /**\n * Create an in-process API client (full filter chain + controller, no HTTP).\n * Same testing story as WebpiecesServer.createApiClient().\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.clientFactory.createApiClient(apiPrototype);\n }\n\n /**\n * Escape hatch for advanced wiring (e.g. addRoute with a hand-built RouteDefinition).\n */\n getRouteBuilder(): RouteBuilderImpl {\n return this.routeBuilder;\n }\n\n /**\n * Compose the filter chain for one route and register it on the Express app.\n */\n private mountRoute(routeWithMeta: RouteHandlerWithMeta): void {\n const service = this.routeBuilder.createRouteHandler(routeWithMeta);\n const routeMeta = routeWithMeta.definition.routeMeta;\n\n // ExpressWrapper handles the full request/response cycle per route:\n // RequestContext.run, header read, manual JSON body parse, error->ProtocolError\n const wrapper = this.middleware.createExpressWrapper(service, routeMeta);\n\n this.registerHandler(\n routeMeta.httpMethod,\n routeMeta.path,\n wrapper.execute.bind(wrapper),\n );\n }\n\n private registerHandler(httpMethod: string, path: string, expressHandler: ExpressRouteHandler): void {\n switch (httpMethod.toLowerCase()) {\n case 'get':\n this.app.get(path, expressHandler);\n break;\n case 'post':\n this.app.post(path, expressHandler);\n break;\n case 'put':\n this.app.put(path, expressHandler);\n break;\n case 'delete':\n this.app.delete(path, expressHandler);\n break;\n case 'patch':\n this.app.patch(path, expressHandler);\n break;\n default:\n log.warn(`[WebpiecesRouteCreator] Unknown HTTP method: ${httpMethod}`);\n }\n }\n}\n"]}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { MethodMeta } from '@webpieces/http-routing';
|
|
2
|
-
import { Filter, WpResponse, Service } from '@webpieces/http-filters';
|
|
3
|
-
export declare class ContextFilter extends Filter<MethodMeta, WpResponse<unknown>> {
|
|
4
|
-
private transferredKeys;
|
|
5
|
-
constructor();
|
|
6
|
-
filter(meta: MethodMeta, nextFilter: Service<MethodMeta, WpResponse<unknown>>): Promise<WpResponse<unknown>>;
|
|
7
|
-
/**
|
|
8
|
-
* Transfer transferred keys from MethodMeta.requestHeaders to RequestContext.
|
|
9
|
-
* A key is transferred when it has an httpHeader (wire name); the value is read
|
|
10
|
-
* from the incoming request under that httpHeader and stored under the key's name.
|
|
11
|
-
*/
|
|
12
|
-
private transferHeaders;
|
|
13
|
-
/**
|
|
14
|
-
* Ensure REQUEST_ID is set in RequestContext.
|
|
15
|
-
* Generates one if not present.
|
|
16
|
-
*/
|
|
17
|
-
private ensureRequestId;
|
|
18
|
-
/**
|
|
19
|
-
* Generate a unique request ID.
|
|
20
|
-
* Format: req-{timestamp}-{random}
|
|
21
|
-
*/
|
|
22
|
-
private generateRequestId;
|
|
23
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ContextFilter = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
|
-
const inversify_1 = require("inversify");
|
|
6
|
-
const http_routing_1 = require("@webpieces/http-routing");
|
|
7
|
-
const core_context_1 = require("@webpieces/core-context");
|
|
8
|
-
const http_filters_1 = require("@webpieces/http-filters");
|
|
9
|
-
const core_util_1 = require("@webpieces/core-util");
|
|
10
|
-
const WebpiecesCoreHeaders_1 = require("../headers/WebpiecesCoreHeaders");
|
|
11
|
-
const ContextKeys_1 = require("../headers/ContextKeys");
|
|
12
|
-
const core_util_2 = require("@webpieces/core-util");
|
|
13
|
-
/**
|
|
14
|
-
* ContextFilter - Transfers platform headers and stores request metadata in RequestContext.
|
|
15
|
-
* Priority: 2000 (executes first in filter chain)
|
|
16
|
-
*
|
|
17
|
-
* NEW: Now handles header transfer from RouterRequest to RequestContext
|
|
18
|
-
* - Injects PlatformHeadersExtension instances via @multiInject (safe because filter created after modules load)
|
|
19
|
-
* - Reads headers from RouterRequest (Express-independent)
|
|
20
|
-
* - Transfers only headers marked with isWantTransferred=true
|
|
21
|
-
* - Generates REQUEST_ID if not present
|
|
22
|
-
*
|
|
23
|
-
* RequestContext lifecycle:
|
|
24
|
-
* 1. ExpressWrapper.execute() calls RequestContext.run() (establishes context)
|
|
25
|
-
* 2. ExpressWrapper creates RouterReqResp and MethodMeta
|
|
26
|
-
* 3. Filter chain executes, starting with ContextFilter
|
|
27
|
-
* 4. ContextFilter transfers headers from RouterRequest to RequestContext
|
|
28
|
-
* 5. ContextFilter stores metadata (METHOD_META, REQUEST_PATH, HTTP_METHOD)
|
|
29
|
-
* 6. Downstream filters and controller can access headers + metadata
|
|
30
|
-
* 7. Context auto-clears when RequestContext.run() completes
|
|
31
|
-
*/
|
|
32
|
-
const log = core_util_2.LogManager.getLogger('ContextFilter');
|
|
33
|
-
let ContextFilter = class ContextFilter extends http_filters_1.Filter {
|
|
34
|
-
transferredKeys;
|
|
35
|
-
constructor() {
|
|
36
|
-
super();
|
|
37
|
-
// The global registry is the single source of truth (configured at startup,
|
|
38
|
-
// duplicate-validated). No DI — HeaderRegistry.configure(...) ran first.
|
|
39
|
-
const registry = core_util_1.HeaderRegistry.get();
|
|
40
|
-
this.transferredKeys = registry.getTransferredKeys();
|
|
41
|
-
log.info(`[ContextFilter] Using ${registry.getKeys().length} context keys from HeaderRegistry (${this.transferredKeys.length} transferred)`);
|
|
42
|
-
}
|
|
43
|
-
async filter(meta, nextFilter) {
|
|
44
|
-
// Transfer platform headers from MethodMeta.requestHeaders to RequestContext
|
|
45
|
-
this.transferHeaders(meta);
|
|
46
|
-
// Store request metadata in context for other filters/controllers to access
|
|
47
|
-
core_context_1.RequestContext.putHeader(ContextKeys_1.ContextKeys.METHOD_META, meta);
|
|
48
|
-
core_context_1.RequestContext.putHeader(ContextKeys_1.ContextKeys.REQUEST_PATH, meta.path);
|
|
49
|
-
core_context_1.RequestContext.putHeader(ContextKeys_1.ContextKeys.HTTP_METHOD, meta.httpMethod);
|
|
50
|
-
// Execute next filter/controller
|
|
51
|
-
return await nextFilter.invoke(meta);
|
|
52
|
-
// RequestContext is auto-cleared by ExpressWrapper when request completes
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Transfer transferred keys from MethodMeta.requestHeaders to RequestContext.
|
|
56
|
-
* A key is transferred when it has an httpHeader (wire name); the value is read
|
|
57
|
-
* from the incoming request under that httpHeader and stored under the key's name.
|
|
58
|
-
*/
|
|
59
|
-
transferHeaders(meta) {
|
|
60
|
-
if (!meta.requestHeaders) {
|
|
61
|
-
// No headers in test mode (createApiClient creates context but not headers)
|
|
62
|
-
this.ensureRequestId();
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
// Transfer each key to RequestContext (read by wire name, store by key name).
|
|
66
|
-
for (const key of this.transferredKeys) {
|
|
67
|
-
// Get values from requestHeaders (case-insensitive lookup by wire name)
|
|
68
|
-
const values = meta.requestHeaders.get(key.httpHeader.toLowerCase());
|
|
69
|
-
if (values && values.length > 0) {
|
|
70
|
-
core_context_1.RequestContext.putHeader(key, values[0]);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// Clear request headers from MethodMeta - MUST FORCE USAGE of RequestContext!!!
|
|
74
|
-
meta.requestHeaders = undefined;
|
|
75
|
-
// Generate REQUEST_ID if not present (first service in chain)
|
|
76
|
-
this.ensureRequestId();
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Ensure REQUEST_ID is set in RequestContext.
|
|
80
|
-
* Generates one if not present.
|
|
81
|
-
*/
|
|
82
|
-
ensureRequestId() {
|
|
83
|
-
if (!core_context_1.RequestContext.hasHeader(WebpiecesCoreHeaders_1.WebpiecesCoreHeaders.REQUEST_ID)) {
|
|
84
|
-
const requestId = this.generateRequestId();
|
|
85
|
-
core_context_1.RequestContext.putHeader(WebpiecesCoreHeaders_1.WebpiecesCoreHeaders.REQUEST_ID, requestId);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Generate a unique request ID.
|
|
90
|
-
* Format: req-{timestamp}-{random}
|
|
91
|
-
*/
|
|
92
|
-
generateRequestId() {
|
|
93
|
-
return `svrGenReqId-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
exports.ContextFilter = ContextFilter;
|
|
97
|
-
exports.ContextFilter = ContextFilter = tslib_1.__decorate([
|
|
98
|
-
(0, http_routing_1.provideFrameworkSingleton)(),
|
|
99
|
-
(0, inversify_1.injectable)(),
|
|
100
|
-
tslib_1.__metadata("design:paramtypes", [])
|
|
101
|
-
], ContextFilter);
|
|
102
|
-
//# sourceMappingURL=ContextFilter.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ContextFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/filters/ContextFilter.ts"],"names":[],"mappings":";;;;AAAA,yCAAuC;AACvC,0DAAgF;AAChF,0DAAyD;AACzD,0DAAsE;AACtE,oDAAkE;AAClE,0EAAqE;AACrE,wDAAmD;AACnD,oDAAkD;AAElD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;AAI3C,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,qBAAuC;IAC9D,eAAe,CAAe;IAEtC;QACI,KAAK,EAAE,CAAC;QAER,4EAA4E;QAC5E,yEAAyE;QACzE,MAAM,QAAQ,GAAG,0BAAc,CAAC,GAAG,EAAE,CAAC;QACtC,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC,kBAAkB,EAAE,CAAC;QAErD,GAAG,CAAC,IAAI,CAAC,yBAAyB,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,sCAAsC,IAAI,CAAC,eAAe,CAAC,MAAM,eAAe,CAAC,CAAC;IACjJ,CAAC;IAED,KAAK,CAAC,MAAM,CACR,IAAgB,EAChB,UAAoD;QAEpD,6EAA6E;QAC7E,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QAE3B,4EAA4E;QAC5E,6BAAc,CAAC,SAAS,CAAC,yBAAW,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QACxD,6BAAc,CAAC,SAAS,CAAC,yBAAW,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9D,6BAAc,CAAC,SAAS,CAAC,yBAAW,CAAC,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAEnE,iCAAiC;QACjC,OAAO,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACrC,0EAA0E;IAC9E,CAAC;IAED;;;;OAIG;IACK,eAAe,CAAC,IAAgB;QACpC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YACvB,4EAA4E;YAC5E,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,OAAO;QACX,CAAC;QAED,8EAA8E;QAC9E,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACrC,wEAAwE;YACxE,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,UAAW,CAAC,WAAW,EAAE,CAAC,CAAC;YACtE,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;QAED,gFAAgF;QAChF,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;QAEhC,8DAA8D;QAC9D,IAAI,CAAC,eAAe,EAAE,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACK,eAAe;QACnB,IAAI,CAAC,6BAAc,CAAC,SAAS,CAAC,2CAAoB,CAAC,UAAU,CAAC,EAAE,CAAC;YAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC3C,6BAAc,CAAC,SAAS,CAAC,2CAAoB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QACzE,CAAC;IACL,CAAC;IAED;;;OAGG;IACK,iBAAiB;QACrB,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;IACtF,CAAC;CACJ,CAAA;AA7EY,sCAAa;wBAAb,aAAa;IAFzB,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;;GACA,aAAa,CA6EzB","sourcesContent":["import { injectable } from 'inversify';\nimport { provideFrameworkSingleton, MethodMeta } from '@webpieces/http-routing';\nimport { RequestContext } from '@webpieces/core-context';\nimport { Filter, WpResponse, Service } from '@webpieces/http-filters';\nimport { ContextKey, HeaderRegistry } from '@webpieces/core-util';\nimport {WebpiecesCoreHeaders} from \"../headers/WebpiecesCoreHeaders\";\nimport {ContextKeys} from \"../headers/ContextKeys\";\nimport { LogManager } from '@webpieces/core-util';\n\n/**\n * ContextFilter - Transfers platform headers and stores request metadata in RequestContext.\n * Priority: 2000 (executes first in filter chain)\n *\n * NEW: Now handles header transfer from RouterRequest to RequestContext\n * - Injects PlatformHeadersExtension instances via @multiInject (safe because filter created after modules load)\n * - Reads headers from RouterRequest (Express-independent)\n * - Transfers only headers marked with isWantTransferred=true\n * - Generates REQUEST_ID if not present\n *\n * RequestContext lifecycle:\n * 1. ExpressWrapper.execute() calls RequestContext.run() (establishes context)\n * 2. ExpressWrapper creates RouterReqResp and MethodMeta\n * 3. Filter chain executes, starting with ContextFilter\n * 4. ContextFilter transfers headers from RouterRequest to RequestContext\n * 5. ContextFilter stores metadata (METHOD_META, REQUEST_PATH, HTTP_METHOD)\n * 6. Downstream filters and controller can access headers + metadata\n * 7. Context auto-clears when RequestContext.run() completes\n */\nconst log = LogManager.getLogger('ContextFilter');\n\n@provideFrameworkSingleton()\n@injectable()\nexport class ContextFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n private transferredKeys: ContextKey[];\n\n constructor() {\n super();\n\n // The global registry is the single source of truth (configured at startup,\n // duplicate-validated). No DI — HeaderRegistry.configure(...) ran first.\n const registry = HeaderRegistry.get();\n this.transferredKeys = registry.getTransferredKeys();\n\n log.info(`[ContextFilter] Using ${registry.getKeys().length} context keys from HeaderRegistry (${this.transferredKeys.length} transferred)`);\n }\n\n async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n // Transfer platform headers from MethodMeta.requestHeaders to RequestContext\n this.transferHeaders(meta);\n\n // Store request metadata in context for other filters/controllers to access\n RequestContext.putHeader(ContextKeys.METHOD_META, meta);\n RequestContext.putHeader(ContextKeys.REQUEST_PATH, meta.path);\n RequestContext.putHeader(ContextKeys.HTTP_METHOD, meta.httpMethod);\n\n // Execute next filter/controller\n return await nextFilter.invoke(meta);\n // RequestContext is auto-cleared by ExpressWrapper when request completes\n }\n\n /**\n * Transfer transferred keys from MethodMeta.requestHeaders to RequestContext.\n * A key is transferred when it has an httpHeader (wire name); the value is read\n * from the incoming request under that httpHeader and stored under the key's name.\n */\n private transferHeaders(meta: MethodMeta): void {\n if (!meta.requestHeaders) {\n // No headers in test mode (createApiClient creates context but not headers)\n this.ensureRequestId();\n return;\n }\n\n // Transfer each key to RequestContext (read by wire name, store by key name).\n for (const key of this.transferredKeys) {\n // Get values from requestHeaders (case-insensitive lookup by wire name)\n const values = meta.requestHeaders.get(key.httpHeader!.toLowerCase());\n if (values && values.length > 0) {\n RequestContext.putHeader(key, values[0]);\n }\n }\n\n // Clear request headers from MethodMeta - MUST FORCE USAGE of RequestContext!!!\n meta.requestHeaders = undefined;\n\n // Generate REQUEST_ID if not present (first service in chain)\n this.ensureRequestId();\n }\n\n /**\n * Ensure REQUEST_ID is set in RequestContext.\n * Generates one if not present.\n */\n private ensureRequestId(): void {\n if (!RequestContext.hasHeader(WebpiecesCoreHeaders.REQUEST_ID)) {\n const requestId = this.generateRequestId();\n RequestContext.putHeader(WebpiecesCoreHeaders.REQUEST_ID, requestId);\n }\n }\n\n /**\n * Generate a unique request ID.\n * Format: req-{timestamp}-{random}\n */\n private generateRequestId(): string {\n return `svrGenReqId-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;\n }\n}\n\n"]}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Filter, WpResponse, Service, MethodMeta } from '@webpieces/http-filters';
|
|
2
|
-
/**
|
|
3
|
-
* ServiceAuthFilter - framework filter that enforces the SERVICE-to-service auth modes
|
|
4
|
-
* (@AuthOidc, @AuthSharedSecret). Priority 1950: runs right after ContextFilter (2000)
|
|
5
|
-
* so the credential headers are already in RequestContext, and before app filters.
|
|
6
|
-
*
|
|
7
|
-
* This is what secures Cloud Tasks delivery: a @PubSub endpoint marked @AuthOidc only
|
|
8
|
-
* accepts a request carrying a valid Google OIDC token from an allowed caller SA. The
|
|
9
|
-
* `public` and `jwt` modes are NOT this filter's job (jwt stays in the app AuthFilter).
|
|
10
|
-
*/
|
|
11
|
-
export declare class ServiceAuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {
|
|
12
|
-
filter(meta: MethodMeta, nextFilter: Service<MethodMeta, WpResponse<unknown>>): Promise<WpResponse<unknown>>;
|
|
13
|
-
/** Verify a Google OIDC bearer token from an allowed caller service account. */
|
|
14
|
-
private enforceOidc;
|
|
15
|
-
/** Constant-time compare of the shared-secret header against process.env[secretEnv]. */
|
|
16
|
-
private enforceSharedSecret;
|
|
17
|
-
private stripBearer;
|
|
18
|
-
private constantTimeEquals;
|
|
19
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ServiceAuthFilter = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
|
-
const inversify_1 = require("inversify");
|
|
6
|
-
const crypto_1 = require("crypto");
|
|
7
|
-
const core_context_1 = require("@webpieces/core-context");
|
|
8
|
-
const http_filters_1 = require("@webpieces/http-filters");
|
|
9
|
-
const core_util_1 = require("@webpieces/core-util");
|
|
10
|
-
const gcp_identity_1 = require("@webpieces/gcp-identity");
|
|
11
|
-
const core_util_2 = require("@webpieces/core-util");
|
|
12
|
-
const log = core_util_2.LogManager.getLogger('ServiceAuthFilter');
|
|
13
|
-
/**
|
|
14
|
-
* ServiceAuthFilter - framework filter that enforces the SERVICE-to-service auth modes
|
|
15
|
-
* (@AuthOidc, @AuthSharedSecret). Priority 1950: runs right after ContextFilter (2000)
|
|
16
|
-
* so the credential headers are already in RequestContext, and before app filters.
|
|
17
|
-
*
|
|
18
|
-
* This is what secures Cloud Tasks delivery: a @PubSub endpoint marked @AuthOidc only
|
|
19
|
-
* accepts a request carrying a valid Google OIDC token from an allowed caller SA. The
|
|
20
|
-
* `public` and `jwt` modes are NOT this filter's job (jwt stays in the app AuthFilter).
|
|
21
|
-
*/
|
|
22
|
-
let ServiceAuthFilter = class ServiceAuthFilter extends http_filters_1.Filter {
|
|
23
|
-
// webpieces-disable no-any-unknown -- Filter generic params use unknown for response type flexibility
|
|
24
|
-
async filter(meta, nextFilter) {
|
|
25
|
-
const authMeta = meta.authMeta;
|
|
26
|
-
if (authMeta) {
|
|
27
|
-
const mode = authMeta.mode;
|
|
28
|
-
if (mode.kind === 'oidc') {
|
|
29
|
-
await this.enforceOidc(mode.callers);
|
|
30
|
-
}
|
|
31
|
-
else if (mode.kind === 'shared-secret') {
|
|
32
|
-
this.enforceSharedSecret(mode.secretEnv);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return nextFilter.invoke(meta);
|
|
36
|
-
}
|
|
37
|
-
/** Verify a Google OIDC bearer token from an allowed caller service account. */
|
|
38
|
-
async enforceOidc(callers) {
|
|
39
|
-
const header = core_context_1.RequestContext.getHeader(core_util_1.WebpiecesCoreHeaders.AUTHORIZATION);
|
|
40
|
-
const token = this.stripBearer(header);
|
|
41
|
-
if (!token) {
|
|
42
|
-
throw new core_util_1.HttpUnauthorizedError('Missing OIDC bearer token for @AuthOidc endpoint');
|
|
43
|
-
}
|
|
44
|
-
const result = await (0, gcp_identity_1.verifyOidcFromCallers)(token, callers);
|
|
45
|
-
if (!result.ok) {
|
|
46
|
-
throw new core_util_1.HttpUnauthorizedError(`OIDC auth failed: ${result.reason ?? 'unknown'}`);
|
|
47
|
-
}
|
|
48
|
-
log.debug(`OIDC caller verified: ${result.email}`);
|
|
49
|
-
}
|
|
50
|
-
/** Constant-time compare of the shared-secret header against process.env[secretEnv]. */
|
|
51
|
-
enforceSharedSecret(secretEnv) {
|
|
52
|
-
const expected = process.env[secretEnv];
|
|
53
|
-
if (!expected) {
|
|
54
|
-
throw new core_util_1.HttpUnauthorizedError(`Shared secret env '${secretEnv}' is not configured`);
|
|
55
|
-
}
|
|
56
|
-
const provided = core_context_1.RequestContext.getHeader(core_util_1.WebpiecesCoreHeaders.SHARED_SECRET);
|
|
57
|
-
if (!provided || !this.constantTimeEquals(provided, expected)) {
|
|
58
|
-
throw new core_util_1.HttpUnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
stripBearer(header) {
|
|
62
|
-
if (!header) {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
const prefix = 'Bearer ';
|
|
66
|
-
return header.startsWith(prefix) ? header.substring(prefix.length) : header;
|
|
67
|
-
}
|
|
68
|
-
constantTimeEquals(a, b) {
|
|
69
|
-
const bufA = Buffer.from(a, 'utf8');
|
|
70
|
-
const bufB = Buffer.from(b, 'utf8');
|
|
71
|
-
if (bufA.length !== bufB.length) {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
return (0, crypto_1.timingSafeEqual)(bufA, bufB);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
exports.ServiceAuthFilter = ServiceAuthFilter;
|
|
78
|
-
exports.ServiceAuthFilter = ServiceAuthFilter = tslib_1.__decorate([
|
|
79
|
-
(0, core_context_1.provideFrameworkSingleton)(),
|
|
80
|
-
(0, inversify_1.injectable)()
|
|
81
|
-
// webpieces-disable no-any-unknown -- Filter generic params use unknown for response type flexibility
|
|
82
|
-
], ServiceAuthFilter);
|
|
83
|
-
//# sourceMappingURL=ServiceAuthFilter.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ServiceAuthFilter.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/filters/ServiceAuthFilter.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable } from 'inversify';\nimport { timingSafeEqual } from 'crypto';\nimport { provideFrameworkSingleton, RequestContext } from '@webpieces/core-context';\nimport { Filter, WpResponse, Service, MethodMeta } from '@webpieces/http-filters';\nimport { HttpUnauthorizedError, WebpiecesCoreHeaders } from '@webpieces/core-util';\nimport { verifyOidcFromCallers } from '@webpieces/gcp-identity';\nimport { LogManager } from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('ServiceAuthFilter');\n\n/**\n * ServiceAuthFilter - framework filter that enforces the SERVICE-to-service auth modes\n * (@AuthOidc, @AuthSharedSecret). Priority 1950: runs right after ContextFilter (2000)\n * so the credential headers are already in RequestContext, and before app filters.\n *\n * This is what secures Cloud Tasks delivery: a @PubSub endpoint marked @AuthOidc only\n * accepts a request carrying a valid Google OIDC token from an allowed caller SA. The\n * `public` and `jwt` modes are NOT this filter's job (jwt stays in the app AuthFilter).\n */\n@provideFrameworkSingleton()\n@injectable()\n// webpieces-disable no-any-unknown -- Filter generic params use unknown for response type flexibility\nexport class ServiceAuthFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n\n // webpieces-disable no-any-unknown -- Filter generic params use unknown for response type flexibility\n override async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>,\n ): Promise<WpResponse<unknown>> {\n const authMeta = meta.authMeta;\n if (authMeta) {\n const mode = authMeta.mode;\n if (mode.kind === 'oidc') {\n await this.enforceOidc(mode.callers);\n } else if (mode.kind === 'shared-secret') {\n this.enforceSharedSecret(mode.secretEnv);\n }\n }\n return nextFilter.invoke(meta);\n }\n\n /** Verify a Google OIDC bearer token from an allowed caller service account. */\n private async enforceOidc(callers: string[]): Promise<void> {\n const header = RequestContext.getHeader(WebpiecesCoreHeaders.AUTHORIZATION) as string | undefined;\n const token = this.stripBearer(header);\n if (!token) {\n throw new HttpUnauthorizedError('Missing OIDC bearer token for @AuthOidc endpoint');\n }\n const result = await verifyOidcFromCallers(token, callers);\n if (!result.ok) {\n throw new HttpUnauthorizedError(`OIDC auth failed: ${result.reason ?? 'unknown'}`);\n }\n log.debug(`OIDC caller verified: ${result.email}`);\n }\n\n /** Constant-time compare of the shared-secret header against process.env[secretEnv]. */\n private enforceSharedSecret(secretEnv: string): void {\n const expected = process.env[secretEnv];\n if (!expected) {\n throw new HttpUnauthorizedError(`Shared secret env '${secretEnv}' is not configured`);\n }\n const provided = RequestContext.getHeader(WebpiecesCoreHeaders.SHARED_SECRET) as string | undefined;\n if (!provided || !this.constantTimeEquals(provided, expected)) {\n throw new HttpUnauthorizedError('Invalid shared secret for @AuthSharedSecret endpoint');\n }\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"]}
|