@webpieces/http-server 0.4.863 → 0.4.865
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +5 -5
- package/src/ExpressWrapper.d.ts +2 -2
- package/src/ExpressWrapper.js +2 -2
- package/src/ExpressWrapper.js.map +1 -1
- package/src/WebpiecesExpressRouter.js +1 -1
- package/src/WebpiecesExpressRouter.js.map +1 -1
- package/src/WebpiecesMiddleware.d.ts +1 -1
- package/src/WebpiecesMiddleware.js +1 -1
- package/src/WebpiecesMiddleware.js.map +1 -1
package/README.md
CHANGED
|
@@ -51,9 +51,9 @@ derives the status from it (`SurfaceEndUserStatus`):
|
|
|
51
51
|
|
|
52
52
|
| how the request authenticated | surface | an `ApiEndUserError` answers |
|
|
53
53
|
|---|---|---|
|
|
54
|
-
|
|
|
55
|
-
| `@
|
|
56
|
-
|
|
|
54
|
+
| `jwt()` | `gui` | 266 |
|
|
55
|
+
| `@WpAuthorization` (through the MCP bridge) | `llm` | 266 |
|
|
56
|
+
| `apiKey(...)` | `public-api` | `edgeHttpStatus`, else 400 |
|
|
57
57
|
| nothing established one (public, webhook, internal hop) | absent | 266 |
|
|
58
58
|
|
|
59
59
|
`gui` and `llm` are both webpieces clients that DECODE the body and render the message themselves, so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.865",
|
|
4
4
|
"description": "WebPieces server with filter chain and dependency injection",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/core-context": "0.4.
|
|
26
|
-
"@webpieces/core-util": "0.4.
|
|
27
|
-
"@webpieces/gcp-identity": "0.4.
|
|
28
|
-
"@webpieces/http-routing": "0.4.
|
|
25
|
+
"@webpieces/core-context": "0.4.865",
|
|
26
|
+
"@webpieces/core-util": "0.4.865",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.865",
|
|
28
|
+
"@webpieces/http-routing": "0.4.865",
|
|
29
29
|
"cors": "2.8.5",
|
|
30
30
|
"express": "5.1.0",
|
|
31
31
|
"inversify": "7.10.4"
|
package/src/ExpressWrapper.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare class ExpressWrapper {
|
|
|
34
34
|
private formPost;
|
|
35
35
|
/**
|
|
36
36
|
* True for an @Endpoint(..., { rawBody: true }) route: RETAIN the verbatim bytes + the
|
|
37
|
-
* absolute url on the published {@link HttpRequest}, so an
|
|
37
|
+
* absolute url on the published {@link HttpRequest}, so an webhook(...) hook can verify a
|
|
38
38
|
* vendor signature over what the sender actually transmitted. Also switches the JSON parse
|
|
39
39
|
* failure from "throw now" to "hold it for AuthFilter" — see {@link RawRequest.bodyParseError}.
|
|
40
40
|
*/
|
|
@@ -70,7 +70,7 @@ export declare class ExpressWrapper {
|
|
|
70
70
|
formPost?: boolean,
|
|
71
71
|
/**
|
|
72
72
|
* True for an @Endpoint(..., { rawBody: true }) route: RETAIN the verbatim bytes + the
|
|
73
|
-
* absolute url on the published {@link HttpRequest}, so an
|
|
73
|
+
* absolute url on the published {@link HttpRequest}, so an webhook(...) hook can verify a
|
|
74
74
|
* vendor signature over what the sender actually transmitted. Also switches the JSON parse
|
|
75
75
|
* failure from "throw now" to "hold it for AuthFilter" — see {@link RawRequest.bodyParseError}.
|
|
76
76
|
*/
|
package/src/ExpressWrapper.js
CHANGED
|
@@ -67,7 +67,7 @@ class ExpressWrapper {
|
|
|
67
67
|
formPost = false,
|
|
68
68
|
/**
|
|
69
69
|
* True for an @Endpoint(..., { rawBody: true }) route: RETAIN the verbatim bytes + the
|
|
70
|
-
* absolute url on the published {@link HttpRequest}, so an
|
|
70
|
+
* absolute url on the published {@link HttpRequest}, so an webhook(...) hook can verify a
|
|
71
71
|
* vendor signature over what the sender actually transmitted. Also switches the JSON parse
|
|
72
72
|
* failure from "throw now" to "hold it for AuthFilter" — see {@link RawRequest.bodyParseError}.
|
|
73
73
|
*/
|
|
@@ -129,7 +129,7 @@ class ExpressWrapper {
|
|
|
129
129
|
// know" and step aside. The translator was never too late; the context it needs was.
|
|
130
130
|
//
|
|
131
131
|
// No `raw` yet — the bytes have not been read. Step 3 republishes WITH them, below the
|
|
132
|
-
// same request scope and still above the filter chain, so
|
|
132
|
+
// same request scope and still above the filter chain, so webhook(...) signature
|
|
133
133
|
// verification sees exactly what it saw before.
|
|
134
134
|
//
|
|
135
135
|
// KNOWN ISSUE, ACCEPTED AND NOT FIXED (issue #862): this publishes the request but does
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpressWrapper.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/ExpressWrapper.ts"],"names":[],"mappings":"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","sourcesContent":["import { Request, Response, NextFunction } from 'express';\nimport {\n ClientRegistry,\n ApiBadRequestError,\n HttpContractMapper,\n HttpResponseDto,\n HttpResponseStatus,\n RouteMetadata,\n ApiImplementationError,\n toError,\n WebpiecesCoreHeaders,\n ApiErrorCodec,\n ApiErrorPayload,\n SurfaceEndUserStatus,\n WEBPIECES_DEFAULT_ERROR_TRANSLATOR,\n} from '@webpieces/core-util';\nimport {\n RequestContext,\n HttpRequest,\n RawRequest,\n RequestContextHeaders,\n} from '@webpieces/core-context';\nimport { ExpressResponseWriter } from './ExpressResponseWriter';\nimport { RequestBodyReader } from './body/RequestBodyReader';\nimport { StreamBodyReader } from './body/StreamBodyReader';\n\n/**\n * The cap on an inbound body, in bytes. Reading stops and the request is refused the moment a body\n * crosses it — the bytes already read are dropped, nothing further is buffered.\n *\n * There was NO limit at all before, which was a latent memory DoS on every route and an outright one\n * on a webhook route: `{ rawBody: true }` retains what it reads, and a webhook url is public by\n * construction, so the endpoint most likely to be flooded was also the one that held on to the flood.\n * 10 MiB is comfortably above any api DTO and well under Cloud Run's own 32 MiB request limit.\n *\n * Read this as FRAMEWORK-FIXED, because today an app has no knob for it. The only seam that accepts a\n * different number is the {@link ExpressWrapper} constructor parameter, and nothing production reaches\n * it: `WebpiecesMiddleware.createExpressWrapper` forwards no such argument, and neither\n * `ExpressWrapper` nor this constant is exported from this package's barrel (`src/index.ts`), so the\n * only caller that can vary the cap is a spec inside this package. An app that legitimately needs to\n * accept a larger body therefore cannot unblock itself and has to open an issue. Making it tunable is a\n * code change, not a config one — a follow-up has to thread a value through `createExpressWrapper` and\n * decide where an app declares it (per-route, most likely, since that is the granularity the need has).\n */\nexport const MAX_BODY_BYTES = 10 * 1024 * 1024;\n\n/**\n * What {@link ExpressWrapper.parseBody} produced: the DTO the controller receives, the verbatim\n * {@link RawRequest} when the route asked for one, and the held parse failure on a raw-body route.\n *\n * Data-only, so a class rather than an inline object literal, per the webpieces guidelines.\n */\nclass ParsedBody {\n constructor(\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n public readonly requestDto: unknown,\n public readonly raw: RawRequest | undefined,\n /** Set only on a raw-body route, where the failure is HELD for AuthFilter instead of thrown. */\n public readonly parseError?: Error,\n ) {}\n}\n\nexport class ExpressWrapper {\n private readonly responseWriter = new ExpressResponseWriter();\n\n constructor(\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n private clientMethod: (...args: unknown[]) => Promise<unknown>,\n private path: string,\n /** Owns the wire<->context transfer, both directions. Stateless framework singleton. */\n private headers: RequestContextHeaders,\n /**\n * True for an @Endpoint(..., { formPost: true }) route: parse the body as\n * application/x-www-form-urlencoded (flat) instead of JSON. Driven by the ANNOTATION, not\n * the request Content-Type header — the annotation is the single source of truth.\n */\n private formPost: boolean = false,\n /**\n * True for an @Endpoint(..., { rawBody: true }) route: RETAIN the verbatim bytes + the\n * absolute url on the published {@link HttpRequest}, so an @WpAuthWebhook hook can verify a\n * vendor signature over what the sender actually transmitted. Also switches the JSON parse\n * failure from \"throw now\" to \"hold it for AuthFilter\" — see {@link RawRequest.bodyParseError}.\n */\n private rawBody: boolean = false,\n /**\n * The inbound body cap for this route. See {@link MAX_BODY_BYTES}.\n *\n * No production caller passes this — `WebpiecesMiddleware.createExpressWrapper` builds every\n * wrapper without it, so every live route runs on the default. The parameter exists so the\n * refusal path can be tested against a small cap instead of a 10 MiB fixture, and the specs in\n * this package are its only callers; it is not an app-facing tuning point, since the class is\n * not exported from `src/index.ts`. If a route ever needs a different cap, thread it through\n * `createExpressWrapper` rather than reaching around the middleware to construct a wrapper.\n */\n private maxBodyBytes: number = MAX_BODY_BYTES,\n /** Exact shared contract route; omitted only by focused legacy wrapper unit tests. */\n private readonly routeMeta?: RouteMetadata,\n /**\n * Where the body bytes come from. The default reads the request stream; a host that parses\n * the body before webpieces runs (Cloud Functions gen2) needs `PreConsumedBodyReader`,\n * chosen via `WebpiecesExpressRouter.setBodyReader`. See {@link RequestBodyReader}.\n */\n private readonly bodyReader: RequestBodyReader = new StreamBodyReader(),\n ) {}\n\n public async execute(req: Request, res: Response, next: NextFunction): Promise<void> {\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 // 0. PUBLISH the transport-neutral request BEFORE anything that can throw.\n //\n // This is the ordering bug issue #862 was filed for. An app's ErrorTranslator.toWire has\n // to be able to tell WHICH request it is answering for — the surface, the route, the\n // method — and the only place that lives is RequestContext.getRequest(). Publishing it at\n // step 3 (below) meant a body that failed to parse at step 1 reached handleError with an\n // EMPTY scope, so a translator asked \"is this my surface?\" could only answer \"I don't\n // know\" and step aside. The translator was never too late; the context it needs was.\n //\n // No `raw` yet — the bytes have not been read. Step 3 republishes WITH them, below the\n // same request scope and still above the filter chain, so @WpAuthWebhook signature\n // verification sees exactly what it saw before.\n //\n // KNOWN ISSUE, ACCEPTED AND NOT FIXED (issue #862): this publishes the request but does\n // NOT mint the transaction id — that is `fillFromRequest`'s job and it stays at step 3,\n // below the body read. So a caller whose body is malformed or oversize, and who sent no\n // x-request-id of its own, gets an error response with NO transaction id to quote at\n // support. Moving the whole fill up here would drag the raw-body republish into every\n // route, which is the complexity this change deliberately declines. Pinned by a test so a\n // future change to it is a decision rather than an accident.\n RequestContext.setRequest(this.toWebpiecesRequest(req));\n\n // 1. Parse the request body (see parseBody: the PARSER is chosen by the @Endpoint\n // annotation, never by the request Content-Type header).\n const parsed = await this.parseBody(req);\n\n // 2. Translate express's request into the transport-neutral HttpRequest webpieces speaks —\n // now WITH the raw bytes, when the route asked for them.\n const httpRequest = this.toWebpiecesRequest(req, parsed.raw);\n\n // 3. Re-publish the transport-neutral HttpRequest, then move its headers into the context and\n // mint a request id if the caller sent none. BOTH happen above the api boundary, because\n // http-routing requires an already-established, already-filled request scope — it never\n // builds one for you. This is the \"translation layer\" every transport must provide.\n this.headers.fillFromRequest(httpRequest);\n\n // 4. Invoke the api CLIENT method — the SAME proxy tests use. Its filter chain + controller\n // run here, reading the context filled above; the chain never touches express `req`.\n const args = this.routeMeta\n ? HttpContractMapper.fromWire(\n this.routeMeta.parameterBindings,\n this.routeMeta.bodyParameterIndex,\n parsed.requestDto,\n this.pathValues(req),\n this.queryValues(req),\n )\n : [parsed.requestDto];\n const result = await this.clientMethod(...args);\n\n // 5. The contract chooses body-only 200 JSON or caller-owned status/headers/body.\n if (this.routeMeta?.responseType === 'full') {\n if (!(result instanceof HttpResponseDto)) {\n throw new ApiImplementationError(\n `${this.routeMeta.apiName}.${this.routeMeta.methodName} declares responseType:'full' ` +\n `but returned ${result instanceof Object ? result.constructor.name : typeof result}; ` +\n `return HttpResponseDto.`,\n );\n }\n this.send(res, result);\n return;\n }\n if (result instanceof HttpResponseDto) {\n throw new ApiImplementationError(\n `${this.routeMeta?.apiName ?? 'API'}.${this.routeMeta?.methodName ?? 'method'} returned ` +\n `HttpResponseDto but its @Endpoint does not declare responseType:'full'.`,\n );\n }\n this.send(res, new HttpResponseDto(new HttpResponseStatus(200, 'OK'), [], result));\n }\n\n /**\n * Read and parse the request body: the DTO the controller receives, plus the verbatim\n * {@link RawRequest} when the route asked for one.\n *\n * The PARSER is chosen by the `@Endpoint` annotation (`this.formPost`), NOT by the request\n * Content-Type header — the annotation is the single source of truth.\n */\n private async parseBody(req: Request): Promise<ParsedBody> {\n if (!['POST', 'PUT', 'PATCH'].includes(req.method)) {\n return new ParsedBody({}, undefined);\n }\n\n // Read BYTES, not text. Concatenating per-chunk toString() corrupted any multi-byte\n // character that straddled a chunk boundary — invisible on small bodies, and fatal for a\n // signature computed over the bytes.\n const bodyBytes = await this.bodyReader.read(req, this.maxBodyBytes);\n const bodyText = bodyBytes.toString('utf8');\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n let requestDto: unknown;\n let parseError: Error | undefined;\n if (this.formPost) {\n // application/x-www-form-urlencoded → flat key→value. URLSearchParams is lenient\n // (never throws) — right for EXTERNAL webhooks (e.g. Twilio) that post form-encoded.\n requestDto = this.formBody(bodyText);\n } else {\n const json = this.parseJson(bodyText);\n requestDto = json.requestDto;\n parseError = json.parseError;\n }\n\n const raw = this.rawBody\n ? new RawRequest(\n this.absoluteUrl(req),\n bodyBytes,\n req.socket?.remoteAddress,\n parseError,\n )\n : undefined;\n return new ParsedBody(requestDto, raw);\n }\n\n /**\n * JSON (the default, SYMMETRIC with the client's JSON.stringify). A non-JSON body is a CLIENT\n * error → 400, not the raw 500 an unguarded JSON.parse would throw.\n *\n * On a raw-body (webhook) route the failure is HELD, not thrown: AuthFilter must answer 401 to an\n * unauthenticated caller rather than 400, because \"your JSON was bad\" tells that caller it got\n * past auth. Everywhere else, fail now.\n */\n private parseJson(bodyText: string): ParsedBody {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- translate parse failure to an ApiBadRequestError\n try {\n return new ParsedBody(bodyText ? JSON.parse(bodyText) : {}, undefined);\n } catch (err: unknown) {\n const error = toError(err);\n if (!this.rawBody) {\n throw new ApiBadRequestError(\n 'Request body is not valid JSON',\n undefined,\n undefined,\n error,\n );\n }\n return new ParsedBody({}, undefined, error);\n }\n }\n\n /** Preserve repeated form keys as arrays while keeping the historical flat scalar shape. */\n private formBody(bodyText: string): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n new URLSearchParams(bodyText).forEach((value: string, key: string) => {\n const existing = result[key];\n if (existing === undefined) result[key] = value;\n else if (Array.isArray(existing)) existing.push(value);\n else result[key] = [existing, value];\n });\n return result;\n }\n\n /** Express has already URL-decoded route placeholders by the time a handler runs. */\n private pathValues(req: Request): Map<string, string | readonly string[]> {\n const values = new Map<string, string | readonly string[]>();\n const params = req.params ?? {};\n for (const name of Object.keys(params)) {\n values.set(name, params[name]);\n }\n return values;\n }\n\n /** Parse the original query string so repeated keys survive instead of being collapsed. */\n private queryValues(req: Request): Map<string, string | readonly string[]> {\n const values = new Map<string, string | readonly string[]>();\n const rawUrl = req.originalUrl ?? req.url ?? '';\n const query = rawUrl.includes('?') ? rawUrl.slice(rawUrl.indexOf('?') + 1) : '';\n new URLSearchParams(query).forEach((value: string, name: string) => {\n const existing = values.get(name);\n if (existing === undefined) values.set(name, value);\n else if (typeof existing === 'string') values.set(name, [existing, value]);\n else values.set(name, [...existing, value]);\n });\n return values;\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 /**\n * express Request -> webpieces {@link HttpRequest}. THE translation layer: below this line the\n * filter chain and controllers never see express, which is what lets the same chain run\n * in-process with no transport at all.\n */\n private toWebpiecesRequest(req: Request, raw?: RawRequest): HttpRequest {\n return new HttpRequest(\n req.method,\n req.path ?? this.path,\n this.readExpressHeaders(req),\n raw,\n );\n }\n\n /**\n * The absolute url AS THE SENDER ADDRESSED IT — the string a vendor like Twilio signed.\n *\n * `x-forwarded-proto` / `x-forwarded-host` WIN when present, because behind a TLS-terminating\n * proxy (Cloud Run, any load balancer) express's own view is wrong in both halves: `req.protocol`\n * reads `http` and the Host header is the internal one, while the vendor signed the public\n * `https://...` url the customer configured. Reconstructing naively therefore fails 100% of the\n * time in production and works 100% of the time locally — the worst possible pairing, so this is\n * stated here and pinned by a test rather than left to each app.\n *\n * These headers are attacker-controllable when nothing strips them, and that is ACCEPTABLE here\n * precisely because of what the value is used for: a forged url produces a signature that does not\n * verify, i.e. a 401. It grants nothing. (It is used for verification only — never for a redirect.)\n */\n private absoluteUrl(req: Request): string {\n const forwardedProto = req.headers['x-forwarded-proto'];\n const forwardedHost = req.headers['x-forwarded-host'];\n // A proxy chain sends a comma-separated list; the FIRST entry is the original client's hop.\n const proto = this.firstForwarded(forwardedProto) ?? req.protocol;\n const host = this.firstForwarded(forwardedHost) ?? req.get('host') ?? '';\n return `${proto}://${host}${req.originalUrl ?? req.url ?? this.path}`;\n }\n\n private firstForwarded(value: string | string[] | undefined): string | undefined {\n const raw = Array.isArray(value) ? value[0] : value;\n const first = raw?.split(',')[0]?.trim();\n return first === undefined || first === '' ? undefined : first;\n }\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 * Turn a thrown value into the response the caller sees — the SERVER half of webpieces' symmetric\n * error handling (the CLIENT half is `ClientErrorTranslator.throwIfFailure`, and the two speak\n * the same {@link HttpResponseDto}).\n *\n * PUBLIC so wrapExpress can call it for symmetric error handling.\n *\n * ONE source, unconditionally: `ClientRegistry.getErrorTranslator()`. It is never undefined — it\n * holds {@link WebpiecesDefaultErrorTranslator} until an app replaces it, and an app's own\n * translator declines an error by DELEGATING to that default. So there is no \"did this process\n * install one\" branch and no per-error \"not mine\" branch; the app owns the WHOLE response —\n * status code, reason phrase, headers and body — which is what lets it publish its own envelope\n * AND its own `Retry-After` / `WWW-Authenticate` / trace header / cookie.\n *\n * Nothing status-specific is decided here — read `WebpiecesDefaultErrorTranslator` for the full\n * rule and the list of statuses, which must stay in step with its own `fromWire`.\n */\n // webpieces-disable no-any-unknown -- a thrown value is genuinely unknown until toError narrows it\n public handleError(res: Response, thrown: unknown): void {\n if (res.headersSent) {\n return;\n }\n // ONE narrowing, at the top, and every layer below it is honest about holding an `Error`.\n const wire = ClientRegistry.getErrorTranslator().toWire(toError(thrown));\n this.send(res, this.publishedForCallerSurface(wire));\n }\n\n /**\n * Republish an end-user answer at the status THIS CALLER's surface expects — the per-request\n * replacement for the deleted per-router `setEndUserStatus`.\n *\n * The same endpoint is reached by a GUI, by an LLM through the MCP bridge and by a partner\n * against a published REST contract, so 266-or-real-4xx was never a property the ROUTER could\n * know. `WebpiecesCoreHeaders.SURFACE` is set at the edge by `AuthFilter` from the auth mode that\n * matched and propagates unchanged; {@link SurfaceEndUserStatus} turns it into the status.\n *\n * It runs over the translator's output rather than inside it, which is deliberate and fixes a\n * footgun: an app translator that declines by delegating used to have to REPEAT its router's mode\n * (`new ApiErrorHttpMapper('edge')`) and repeating it wrongly was silent. Now every end-user\n * answer — webpieces' own and an app's — is republished at the caller's status in one place.\n *\n * Only a webpieces `end-user` payload is touched. An app that publishes its OWN 266 body owns it.\n */\n private publishedForCallerSurface(response: HttpResponseDto): HttpResponseDto {\n if (response.status.code !== 266 || !ApiErrorCodec.isPayload(response.body)) {\n return response;\n }\n const payload = response.body as ApiErrorPayload;\n const status = SurfaceEndUserStatus.statusFor(\n RequestContext.isActive()\n ? RequestContext.getTrusted(WebpiecesCoreHeaders.SURFACE)\n : undefined,\n payload.edgeHttpStatus,\n );\n if (status === 266) {\n return response;\n }\n return new HttpResponseDto(\n new HttpResponseStatus(status, WEBPIECES_DEFAULT_ERROR_TRANSLATOR.genericMessage(status)),\n response.headers,\n response.body,\n );\n }\n\n private send(res: Response, response: HttpResponseDto): void {\n this.stampResponseContext(res);\n this.responseWriter.write(res, response);\n }\n\n /**\n * THE response choke point: every context key that declares a `responseHeader` goes onto EVERY\n * response — success and error, webpieces' default body and an app's own. It is INFRASTRUCTURE,\n * not app policy: an app that overrides what an error looks like must not thereby lose the\n * headers its support desk quotes back. That is why this lives here and not in\n * {@link WebpiecesDefaultErrorTranslator.toWire} or in an app's translators.\n *\n * This method names NO key. It used to be `stampTransactionId`, a hard-coded\n * `res.setHeader('x-request-id', ...)` that nothing else could join without editing it;\n * `WebpiecesCoreHeaders.REQUEST_ID` now declares `responseHeader: 'x-request-id'` like any other\n * key and arrives through the loop in `RequestContextHeaders.buildResponseHeaders`. A second\n * response key needs no edit here at all — which is the whole test of whether the generalisation\n * is real.\n *\n * Silently empty when there is no context, which is exactly the accepted known issue recorded at\n * step 0 of {@link executeImpl}: a malformed or oversize body fails before `fillFromRequest`\n * mints an id.\n */\n private stampResponseContext(res: Response): void {\n for (const entry of this.headers.buildResponseHeaders().entries()) {\n res.setHeader(entry[0], entry[1]);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ExpressWrapper.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/ExpressWrapper.ts"],"names":[],"mappings":"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","sourcesContent":["import { Request, Response, NextFunction } from 'express';\nimport {\n ClientRegistry,\n ApiBadRequestError,\n HttpContractMapper,\n HttpResponseDto,\n HttpResponseStatus,\n RouteMetadata,\n ApiImplementationError,\n toError,\n WebpiecesCoreHeaders,\n ApiErrorCodec,\n ApiErrorPayload,\n SurfaceEndUserStatus,\n WEBPIECES_DEFAULT_ERROR_TRANSLATOR,\n} from '@webpieces/core-util';\nimport {\n RequestContext,\n HttpRequest,\n RawRequest,\n RequestContextHeaders,\n} from '@webpieces/core-context';\nimport { ExpressResponseWriter } from './ExpressResponseWriter';\nimport { RequestBodyReader } from './body/RequestBodyReader';\nimport { StreamBodyReader } from './body/StreamBodyReader';\n\n/**\n * The cap on an inbound body, in bytes. Reading stops and the request is refused the moment a body\n * crosses it — the bytes already read are dropped, nothing further is buffered.\n *\n * There was NO limit at all before, which was a latent memory DoS on every route and an outright one\n * on a webhook route: `{ rawBody: true }` retains what it reads, and a webhook url is public by\n * construction, so the endpoint most likely to be flooded was also the one that held on to the flood.\n * 10 MiB is comfortably above any api DTO and well under Cloud Run's own 32 MiB request limit.\n *\n * Read this as FRAMEWORK-FIXED, because today an app has no knob for it. The only seam that accepts a\n * different number is the {@link ExpressWrapper} constructor parameter, and nothing production reaches\n * it: `WebpiecesMiddleware.createExpressWrapper` forwards no such argument, and neither\n * `ExpressWrapper` nor this constant is exported from this package's barrel (`src/index.ts`), so the\n * only caller that can vary the cap is a spec inside this package. An app that legitimately needs to\n * accept a larger body therefore cannot unblock itself and has to open an issue. Making it tunable is a\n * code change, not a config one — a follow-up has to thread a value through `createExpressWrapper` and\n * decide where an app declares it (per-route, most likely, since that is the granularity the need has).\n */\nexport const MAX_BODY_BYTES = 10 * 1024 * 1024;\n\n/**\n * What {@link ExpressWrapper.parseBody} produced: the DTO the controller receives, the verbatim\n * {@link RawRequest} when the route asked for one, and the held parse failure on a raw-body route.\n *\n * Data-only, so a class rather than an inline object literal, per the webpieces guidelines.\n */\nclass ParsedBody {\n constructor(\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n public readonly requestDto: unknown,\n public readonly raw: RawRequest | undefined,\n /** Set only on a raw-body route, where the failure is HELD for AuthFilter instead of thrown. */\n public readonly parseError?: Error,\n ) {}\n}\n\nexport class ExpressWrapper {\n private readonly responseWriter = new ExpressResponseWriter();\n\n constructor(\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n private clientMethod: (...args: unknown[]) => Promise<unknown>,\n private path: string,\n /** Owns the wire<->context transfer, both directions. Stateless framework singleton. */\n private headers: RequestContextHeaders,\n /**\n * True for an @Endpoint(..., { formPost: true }) route: parse the body as\n * application/x-www-form-urlencoded (flat) instead of JSON. Driven by the ANNOTATION, not\n * the request Content-Type header — the annotation is the single source of truth.\n */\n private formPost: boolean = false,\n /**\n * True for an @Endpoint(..., { rawBody: true }) route: RETAIN the verbatim bytes + the\n * absolute url on the published {@link HttpRequest}, so an webhook(...) hook can verify a\n * vendor signature over what the sender actually transmitted. Also switches the JSON parse\n * failure from \"throw now\" to \"hold it for AuthFilter\" — see {@link RawRequest.bodyParseError}.\n */\n private rawBody: boolean = false,\n /**\n * The inbound body cap for this route. See {@link MAX_BODY_BYTES}.\n *\n * No production caller passes this — `WebpiecesMiddleware.createExpressWrapper` builds every\n * wrapper without it, so every live route runs on the default. The parameter exists so the\n * refusal path can be tested against a small cap instead of a 10 MiB fixture, and the specs in\n * this package are its only callers; it is not an app-facing tuning point, since the class is\n * not exported from `src/index.ts`. If a route ever needs a different cap, thread it through\n * `createExpressWrapper` rather than reaching around the middleware to construct a wrapper.\n */\n private maxBodyBytes: number = MAX_BODY_BYTES,\n /** Exact shared contract route; omitted only by focused legacy wrapper unit tests. */\n private readonly routeMeta?: RouteMetadata,\n /**\n * Where the body bytes come from. The default reads the request stream; a host that parses\n * the body before webpieces runs (Cloud Functions gen2) needs `PreConsumedBodyReader`,\n * chosen via `WebpiecesExpressRouter.setBodyReader`. See {@link RequestBodyReader}.\n */\n private readonly bodyReader: RequestBodyReader = new StreamBodyReader(),\n ) {}\n\n public async execute(req: Request, res: Response, next: NextFunction): Promise<void> {\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 // 0. PUBLISH the transport-neutral request BEFORE anything that can throw.\n //\n // This is the ordering bug issue #862 was filed for. An app's ErrorTranslator.toWire has\n // to be able to tell WHICH request it is answering for — the surface, the route, the\n // method — and the only place that lives is RequestContext.getRequest(). Publishing it at\n // step 3 (below) meant a body that failed to parse at step 1 reached handleError with an\n // EMPTY scope, so a translator asked \"is this my surface?\" could only answer \"I don't\n // know\" and step aside. The translator was never too late; the context it needs was.\n //\n // No `raw` yet — the bytes have not been read. Step 3 republishes WITH them, below the\n // same request scope and still above the filter chain, so webhook(...) signature\n // verification sees exactly what it saw before.\n //\n // KNOWN ISSUE, ACCEPTED AND NOT FIXED (issue #862): this publishes the request but does\n // NOT mint the transaction id — that is `fillFromRequest`'s job and it stays at step 3,\n // below the body read. So a caller whose body is malformed or oversize, and who sent no\n // x-request-id of its own, gets an error response with NO transaction id to quote at\n // support. Moving the whole fill up here would drag the raw-body republish into every\n // route, which is the complexity this change deliberately declines. Pinned by a test so a\n // future change to it is a decision rather than an accident.\n RequestContext.setRequest(this.toWebpiecesRequest(req));\n\n // 1. Parse the request body (see parseBody: the PARSER is chosen by the @Endpoint\n // annotation, never by the request Content-Type header).\n const parsed = await this.parseBody(req);\n\n // 2. Translate express's request into the transport-neutral HttpRequest webpieces speaks —\n // now WITH the raw bytes, when the route asked for them.\n const httpRequest = this.toWebpiecesRequest(req, parsed.raw);\n\n // 3. Re-publish the transport-neutral HttpRequest, then move its headers into the context and\n // mint a request id if the caller sent none. BOTH happen above the api boundary, because\n // http-routing requires an already-established, already-filled request scope — it never\n // builds one for you. This is the \"translation layer\" every transport must provide.\n this.headers.fillFromRequest(httpRequest);\n\n // 4. Invoke the api CLIENT method — the SAME proxy tests use. Its filter chain + controller\n // run here, reading the context filled above; the chain never touches express `req`.\n const args = this.routeMeta\n ? HttpContractMapper.fromWire(\n this.routeMeta.parameterBindings,\n this.routeMeta.bodyParameterIndex,\n parsed.requestDto,\n this.pathValues(req),\n this.queryValues(req),\n )\n : [parsed.requestDto];\n const result = await this.clientMethod(...args);\n\n // 5. The contract chooses body-only 200 JSON or caller-owned status/headers/body.\n if (this.routeMeta?.responseType === 'full') {\n if (!(result instanceof HttpResponseDto)) {\n throw new ApiImplementationError(\n `${this.routeMeta.apiName}.${this.routeMeta.methodName} declares responseType:'full' ` +\n `but returned ${result instanceof Object ? result.constructor.name : typeof result}; ` +\n `return HttpResponseDto.`,\n );\n }\n this.send(res, result);\n return;\n }\n if (result instanceof HttpResponseDto) {\n throw new ApiImplementationError(\n `${this.routeMeta?.apiName ?? 'API'}.${this.routeMeta?.methodName ?? 'method'} returned ` +\n `HttpResponseDto but its @Endpoint does not declare responseType:'full'.`,\n );\n }\n this.send(res, new HttpResponseDto(new HttpResponseStatus(200, 'OK'), [], result));\n }\n\n /**\n * Read and parse the request body: the DTO the controller receives, plus the verbatim\n * {@link RawRequest} when the route asked for one.\n *\n * The PARSER is chosen by the `@Endpoint` annotation (`this.formPost`), NOT by the request\n * Content-Type header — the annotation is the single source of truth.\n */\n private async parseBody(req: Request): Promise<ParsedBody> {\n if (!['POST', 'PUT', 'PATCH'].includes(req.method)) {\n return new ParsedBody({}, undefined);\n }\n\n // Read BYTES, not text. Concatenating per-chunk toString() corrupted any multi-byte\n // character that straddled a chunk boundary — invisible on small bodies, and fatal for a\n // signature computed over the bytes.\n const bodyBytes = await this.bodyReader.read(req, this.maxBodyBytes);\n const bodyText = bodyBytes.toString('utf8');\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n let requestDto: unknown;\n let parseError: Error | undefined;\n if (this.formPost) {\n // application/x-www-form-urlencoded → flat key→value. URLSearchParams is lenient\n // (never throws) — right for EXTERNAL webhooks (e.g. Twilio) that post form-encoded.\n requestDto = this.formBody(bodyText);\n } else {\n const json = this.parseJson(bodyText);\n requestDto = json.requestDto;\n parseError = json.parseError;\n }\n\n const raw = this.rawBody\n ? new RawRequest(\n this.absoluteUrl(req),\n bodyBytes,\n req.socket?.remoteAddress,\n parseError,\n )\n : undefined;\n return new ParsedBody(requestDto, raw);\n }\n\n /**\n * JSON (the default, SYMMETRIC with the client's JSON.stringify). A non-JSON body is a CLIENT\n * error → 400, not the raw 500 an unguarded JSON.parse would throw.\n *\n * On a raw-body (webhook) route the failure is HELD, not thrown: AuthFilter must answer 401 to an\n * unauthenticated caller rather than 400, because \"your JSON was bad\" tells that caller it got\n * past auth. Everywhere else, fail now.\n */\n private parseJson(bodyText: string): ParsedBody {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- translate parse failure to an ApiBadRequestError\n try {\n return new ParsedBody(bodyText ? JSON.parse(bodyText) : {}, undefined);\n } catch (err: unknown) {\n const error = toError(err);\n if (!this.rawBody) {\n throw new ApiBadRequestError(\n 'Request body is not valid JSON',\n undefined,\n undefined,\n error,\n );\n }\n return new ParsedBody({}, undefined, error);\n }\n }\n\n /** Preserve repeated form keys as arrays while keeping the historical flat scalar shape. */\n private formBody(bodyText: string): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n new URLSearchParams(bodyText).forEach((value: string, key: string) => {\n const existing = result[key];\n if (existing === undefined) result[key] = value;\n else if (Array.isArray(existing)) existing.push(value);\n else result[key] = [existing, value];\n });\n return result;\n }\n\n /** Express has already URL-decoded route placeholders by the time a handler runs. */\n private pathValues(req: Request): Map<string, string | readonly string[]> {\n const values = new Map<string, string | readonly string[]>();\n const params = req.params ?? {};\n for (const name of Object.keys(params)) {\n values.set(name, params[name]);\n }\n return values;\n }\n\n /** Parse the original query string so repeated keys survive instead of being collapsed. */\n private queryValues(req: Request): Map<string, string | readonly string[]> {\n const values = new Map<string, string | readonly string[]>();\n const rawUrl = req.originalUrl ?? req.url ?? '';\n const query = rawUrl.includes('?') ? rawUrl.slice(rawUrl.indexOf('?') + 1) : '';\n new URLSearchParams(query).forEach((value: string, name: string) => {\n const existing = values.get(name);\n if (existing === undefined) values.set(name, value);\n else if (typeof existing === 'string') values.set(name, [existing, value]);\n else values.set(name, [...existing, value]);\n });\n return values;\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 /**\n * express Request -> webpieces {@link HttpRequest}. THE translation layer: below this line the\n * filter chain and controllers never see express, which is what lets the same chain run\n * in-process with no transport at all.\n */\n private toWebpiecesRequest(req: Request, raw?: RawRequest): HttpRequest {\n return new HttpRequest(\n req.method,\n req.path ?? this.path,\n this.readExpressHeaders(req),\n raw,\n );\n }\n\n /**\n * The absolute url AS THE SENDER ADDRESSED IT — the string a vendor like Twilio signed.\n *\n * `x-forwarded-proto` / `x-forwarded-host` WIN when present, because behind a TLS-terminating\n * proxy (Cloud Run, any load balancer) express's own view is wrong in both halves: `req.protocol`\n * reads `http` and the Host header is the internal one, while the vendor signed the public\n * `https://...` url the customer configured. Reconstructing naively therefore fails 100% of the\n * time in production and works 100% of the time locally — the worst possible pairing, so this is\n * stated here and pinned by a test rather than left to each app.\n *\n * These headers are attacker-controllable when nothing strips them, and that is ACCEPTABLE here\n * precisely because of what the value is used for: a forged url produces a signature that does not\n * verify, i.e. a 401. It grants nothing. (It is used for verification only — never for a redirect.)\n */\n private absoluteUrl(req: Request): string {\n const forwardedProto = req.headers['x-forwarded-proto'];\n const forwardedHost = req.headers['x-forwarded-host'];\n // A proxy chain sends a comma-separated list; the FIRST entry is the original client's hop.\n const proto = this.firstForwarded(forwardedProto) ?? req.protocol;\n const host = this.firstForwarded(forwardedHost) ?? req.get('host') ?? '';\n return `${proto}://${host}${req.originalUrl ?? req.url ?? this.path}`;\n }\n\n private firstForwarded(value: string | string[] | undefined): string | undefined {\n const raw = Array.isArray(value) ? value[0] : value;\n const first = raw?.split(',')[0]?.trim();\n return first === undefined || first === '' ? undefined : first;\n }\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 * Turn a thrown value into the response the caller sees — the SERVER half of webpieces' symmetric\n * error handling (the CLIENT half is `ClientErrorTranslator.throwIfFailure`, and the two speak\n * the same {@link HttpResponseDto}).\n *\n * PUBLIC so wrapExpress can call it for symmetric error handling.\n *\n * ONE source, unconditionally: `ClientRegistry.getErrorTranslator()`. It is never undefined — it\n * holds {@link WebpiecesDefaultErrorTranslator} until an app replaces it, and an app's own\n * translator declines an error by DELEGATING to that default. So there is no \"did this process\n * install one\" branch and no per-error \"not mine\" branch; the app owns the WHOLE response —\n * status code, reason phrase, headers and body — which is what lets it publish its own envelope\n * AND its own `Retry-After` / `WWW-Authenticate` / trace header / cookie.\n *\n * Nothing status-specific is decided here — read `WebpiecesDefaultErrorTranslator` for the full\n * rule and the list of statuses, which must stay in step with its own `fromWire`.\n */\n // webpieces-disable no-any-unknown -- a thrown value is genuinely unknown until toError narrows it\n public handleError(res: Response, thrown: unknown): void {\n if (res.headersSent) {\n return;\n }\n // ONE narrowing, at the top, and every layer below it is honest about holding an `Error`.\n const wire = ClientRegistry.getErrorTranslator().toWire(toError(thrown));\n this.send(res, this.publishedForCallerSurface(wire));\n }\n\n /**\n * Republish an end-user answer at the status THIS CALLER's surface expects — the per-request\n * replacement for the deleted per-router `setEndUserStatus`.\n *\n * The same endpoint is reached by a GUI, by an LLM through the MCP bridge and by a partner\n * against a published REST contract, so 266-or-real-4xx was never a property the ROUTER could\n * know. `WebpiecesCoreHeaders.SURFACE` is set at the edge by `AuthFilter` from the auth mode that\n * matched and propagates unchanged; {@link SurfaceEndUserStatus} turns it into the status.\n *\n * It runs over the translator's output rather than inside it, which is deliberate and fixes a\n * footgun: an app translator that declines by delegating used to have to REPEAT its router's mode\n * (`new ApiErrorHttpMapper('edge')`) and repeating it wrongly was silent. Now every end-user\n * answer — webpieces' own and an app's — is republished at the caller's status in one place.\n *\n * Only a webpieces `end-user` payload is touched. An app that publishes its OWN 266 body owns it.\n */\n private publishedForCallerSurface(response: HttpResponseDto): HttpResponseDto {\n if (response.status.code !== 266 || !ApiErrorCodec.isPayload(response.body)) {\n return response;\n }\n const payload = response.body as ApiErrorPayload;\n const status = SurfaceEndUserStatus.statusFor(\n RequestContext.isActive()\n ? RequestContext.getTrusted(WebpiecesCoreHeaders.SURFACE)\n : undefined,\n payload.edgeHttpStatus,\n );\n if (status === 266) {\n return response;\n }\n return new HttpResponseDto(\n new HttpResponseStatus(status, WEBPIECES_DEFAULT_ERROR_TRANSLATOR.genericMessage(status)),\n response.headers,\n response.body,\n );\n }\n\n private send(res: Response, response: HttpResponseDto): void {\n this.stampResponseContext(res);\n this.responseWriter.write(res, response);\n }\n\n /**\n * THE response choke point: every context key that declares a `responseHeader` goes onto EVERY\n * response — success and error, webpieces' default body and an app's own. It is INFRASTRUCTURE,\n * not app policy: an app that overrides what an error looks like must not thereby lose the\n * headers its support desk quotes back. That is why this lives here and not in\n * {@link WebpiecesDefaultErrorTranslator.toWire} or in an app's translators.\n *\n * This method names NO key. It used to be `stampTransactionId`, a hard-coded\n * `res.setHeader('x-request-id', ...)` that nothing else could join without editing it;\n * `WebpiecesCoreHeaders.REQUEST_ID` now declares `responseHeader: 'x-request-id'` like any other\n * key and arrives through the loop in `RequestContextHeaders.buildResponseHeaders`. A second\n * response key needs no edit here at all — which is the whole test of whether the generalisation\n * is real.\n *\n * Silently empty when there is no context, which is exactly the accepted known issue recorded at\n * step 0 of {@link executeImpl}: a malformed or oversize body fails before `fillFromRequest`\n * mints an id.\n */\n private stampResponseContext(res: Response): void {\n for (const entry of this.headers.buildResponseHeaders().entries()) {\n res.setHeader(entry[0], entry[1]);\n }\n }\n}\n"]}
|
|
@@ -142,7 +142,7 @@ class WebpiecesExpressRouter {
|
|
|
142
142
|
for (const route of apiClient.routes) {
|
|
143
143
|
const path = this.expressPath(route);
|
|
144
144
|
// The parser is chosen by the @Endpoint annotation, not the request Content-Type — and
|
|
145
|
-
// so is whether the verbatim bytes survive the parse for an
|
|
145
|
+
// so is whether the verbatim bytes survive the parse for an webhook(...) hook to verify.
|
|
146
146
|
const wrapper = route.streaming
|
|
147
147
|
? this.middleware.createStreamExpressWrapper(apiClient.client[route.methodName], route)
|
|
148
148
|
: this.middleware.createExpressWrapper(apiClient.client[route.methodName], route, this.bodyReader);
|
|
@@ -1 +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, WebpiecesConfig } from '@webpieces/http-routing';\nimport { LogManager, RouteMetadata } from '@webpieces/core-util';\nimport { WebpiecesMiddleware, ExpressRouteHandler } from './WebpiecesMiddleware';\nimport { RequestBodyReader } from './body/RequestBodyReader';\nimport { StreamBodyReader } from './body/StreamBodyReader';\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 /** Where every mounted route reads its body bytes from. See {@link setBodyReader}. */\n private bodyReader: RequestBodyReader = new StreamBodyReader();\n /** Set by {@link bindExpress}; a body reader chosen after that would reach no route. */\n private bound = false;\n\n constructor(private readonly apiFactory: ApiFactory) {}\n\n /**\n * Choose how routes read the request body. Call it BEFORE {@link bindExpress}: each route captures\n * the reader when it is mounted.\n *\n * The default ({@link StreamBodyReader}) reads the request stream, which is right whenever\n * webpieces is mounted ahead of any body parser. A host that parses the body before the app runs\n * (Cloud Functions gen2 / Firebase, which keep the verbatim bytes on `req.rawBody`) needs\n * `router.setBodyReader(new PreConsumedBodyReader())`, or every POST fails fast (issue #937).\n *\n * @throws Error when called after {@link bindExpress}, because the already-mounted routes would\n * silently keep the old reader.\n */\n setBodyReader(bodyReader: RequestBodyReader): void {\n if (this.bound) {\n throw new Error(\n 'setBodyReader() was called after bindExpress(); the mounted routes already captured ' +\n 'their body reader. Call setBodyReader() before bindExpress(app).',\n );\n }\n this.bodyReader = bodyReader;\n }\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 this.bound = true;\n let count = 0;\n for (const apiClient of this.apiFactory.apiClients()) {\n count += this.mountApiClient(app, apiClient);\n }\n log.info(`Mounted ${count} webpieces route(s) onto express`);\n }\n\n /**\n * Add the webpieces global middleware (optional CORS), bind the routes, mount the top-level\n * error handler AFTER them, then app.listen(port). Convenience for a non-legacy webpieces server where\n * webpieces owns the whole express app. Resolves with the http.Server once listening.\n *\n * CORS is mounted ONLY when `config.corsOrigins` is non-empty — see the note below and\n * {@link WebpiecesMiddleware.corsMiddleware}.\n */\n async bindAndStartExpress(\n app: Express,\n port: number = 8080,\n config?: WebpiecesConfig,\n ): Promise<HttpServer> {\n // Global middleware layers (outermost first) — only for a webpieces-owned app.\n // CORS is OPT-IN, and stays OFF in production. A server that serves its own browser app does\n // not need it — a browser applies no cors check to a same-origin request — so mounting it\n // would only hand credentialed cross-origin read access to whatever it allows, for nothing.\n // It is needed solely when a browser on ANOTHER origin calls this api: `ng serve` in dev, or\n // a UI hosted on a different host. Those say so via corsOrigins.\n const corsOrigins = config?.corsOrigins ?? [];\n if (corsOrigins.length > 0) {\n app.use(this.middleware.corsMiddleware(config));\n }\n\n this.bindExpress(app);\n\n // Top-level error handler is mounted LAST (AFTER the routes). Express only forwards a\n // downstream failure to a 4-arg error middleware that sits BELOW the failing route — it does\n // NOT bubble errors back up through next(). See WebpiecesMiddleware.errorHandler.\n app.use(this.middleware.errorHandler.bind(this.middleware));\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(`Failed to start on port ${port}:`, error);\n reject(error);\n return;\n }\n log.info(`Listening on http://localhost:${port}`);\n this.logStartupBanner(port);\n resolve(server);\n });\n },\n );\n }\n\n /**\n * The \"Svr Ready!!\" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and\n * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied\n * verbatim from the production service it was ported from, so a familiar splash marks \"the server is up and reachable\".\n */\n private logStartupBanner(port: number): void {\n if (process.env['K_SERVICE']) {\n return;\n }\n log.info(`\n ___ _____ _\n/ _| | _ \\\\ | |\n\\\\ \\`--. _ _ ___ ___ _ _ | |_/ /_ _ _ _| |_ _\n \\`--. \\\\/ _ \\\\ '_\\\\ \\\\ / / _ \\\\ '_| | // _ \\\\/ _\\` |/ _\\` | | | |\n/\\\\_/ / _/ | \\\\ V / _/ | | |\\\\ \\\\ _/ (_| | (_| | |_| |\n\\\\___/ \\\\_|_| \\\\_/ \\\\_|_| \\\\_| \\\\_\\\\_|\\\\_,_|\\\\_,_|\\\\_, |\n _/ |\n |_/\n\n Svr Ready!! port=${port}\n`);\n }\n\n /**\n * Bind EACH method of one ApiClient. The api's @ApiPath/@Endpoint decorators give the paths;\n * for each we wrap the matching client method (the proxy — RequestContext.run + header read +\n * JSON body parse + error→ApiErrorPayload all live in the wrapper/chain) and register the route.\n * This is one-to-one with a test: an HTTP POST maps straight to `client[method](dto)`.\n *\n * @returns the number of routes mounted for this api.\n */\n private mountApiClient(app: Express, apiClient: ApiClient): number {\n let count = 0;\n for (const route of apiClient.routes) {\n const path = this.expressPath(route);\n // The parser is chosen by the @Endpoint annotation, not the request Content-Type — and\n // so is whether the verbatim bytes survive the parse for an @WpAuthWebhook hook to verify.\n const wrapper = route.streaming\n ? this.middleware.createStreamExpressWrapper(\n apiClient.client[route.methodName],\n route,\n )\n : this.middleware.createExpressWrapper(\n apiClient.client[route.methodName],\n route,\n this.bodyReader,\n );\n this.registerHandler(app, route.httpMethod, path, wrapper.execute.bind(wrapper));\n count++;\n }\n return count;\n }\n\n /**\n * Contract `{name}` placeholders -> Express 5 `:name` route syntax. An EMPTY contract path\n * (`@ApiPath('')` + `@Endpoint(POST, '', ...)`, legal since #944) is served at the root, because every\n * request path Express sees starts with `/` and an empty route could never match.\n */\n private expressPath(route: RouteMetadata): string {\n if (route.path === '') return '/';\n return route.path.replace(/\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, ':$1');\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(`Unknown HTTP method: ${httpMethod}`);\n }\n }\n}\n"]}
|
|
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, WebpiecesConfig } from '@webpieces/http-routing';\nimport { LogManager, RouteMetadata } from '@webpieces/core-util';\nimport { WebpiecesMiddleware, ExpressRouteHandler } from './WebpiecesMiddleware';\nimport { RequestBodyReader } from './body/RequestBodyReader';\nimport { StreamBodyReader } from './body/StreamBodyReader';\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 /** Where every mounted route reads its body bytes from. See {@link setBodyReader}. */\n private bodyReader: RequestBodyReader = new StreamBodyReader();\n /** Set by {@link bindExpress}; a body reader chosen after that would reach no route. */\n private bound = false;\n\n constructor(private readonly apiFactory: ApiFactory) {}\n\n /**\n * Choose how routes read the request body. Call it BEFORE {@link bindExpress}: each route captures\n * the reader when it is mounted.\n *\n * The default ({@link StreamBodyReader}) reads the request stream, which is right whenever\n * webpieces is mounted ahead of any body parser. A host that parses the body before the app runs\n * (Cloud Functions gen2 / Firebase, which keep the verbatim bytes on `req.rawBody`) needs\n * `router.setBodyReader(new PreConsumedBodyReader())`, or every POST fails fast (issue #937).\n *\n * @throws Error when called after {@link bindExpress}, because the already-mounted routes would\n * silently keep the old reader.\n */\n setBodyReader(bodyReader: RequestBodyReader): void {\n if (this.bound) {\n throw new Error(\n 'setBodyReader() was called after bindExpress(); the mounted routes already captured ' +\n 'their body reader. Call setBodyReader() before bindExpress(app).',\n );\n }\n this.bodyReader = bodyReader;\n }\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 this.bound = true;\n let count = 0;\n for (const apiClient of this.apiFactory.apiClients()) {\n count += this.mountApiClient(app, apiClient);\n }\n log.info(`Mounted ${count} webpieces route(s) onto express`);\n }\n\n /**\n * Add the webpieces global middleware (optional CORS), bind the routes, mount the top-level\n * error handler AFTER them, then app.listen(port). Convenience for a non-legacy webpieces server where\n * webpieces owns the whole express app. Resolves with the http.Server once listening.\n *\n * CORS is mounted ONLY when `config.corsOrigins` is non-empty — see the note below and\n * {@link WebpiecesMiddleware.corsMiddleware}.\n */\n async bindAndStartExpress(\n app: Express,\n port: number = 8080,\n config?: WebpiecesConfig,\n ): Promise<HttpServer> {\n // Global middleware layers (outermost first) — only for a webpieces-owned app.\n // CORS is OPT-IN, and stays OFF in production. A server that serves its own browser app does\n // not need it — a browser applies no cors check to a same-origin request — so mounting it\n // would only hand credentialed cross-origin read access to whatever it allows, for nothing.\n // It is needed solely when a browser on ANOTHER origin calls this api: `ng serve` in dev, or\n // a UI hosted on a different host. Those say so via corsOrigins.\n const corsOrigins = config?.corsOrigins ?? [];\n if (corsOrigins.length > 0) {\n app.use(this.middleware.corsMiddleware(config));\n }\n\n this.bindExpress(app);\n\n // Top-level error handler is mounted LAST (AFTER the routes). Express only forwards a\n // downstream failure to a 4-arg error middleware that sits BELOW the failing route — it does\n // NOT bubble errors back up through next(). See WebpiecesMiddleware.errorHandler.\n app.use(this.middleware.errorHandler.bind(this.middleware));\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(`Failed to start on port ${port}:`, error);\n reject(error);\n return;\n }\n log.info(`Listening on http://localhost:${port}`);\n this.logStartupBanner(port);\n resolve(server);\n });\n },\n );\n }\n\n /**\n * The \"Svr Ready!!\" ASCII banner, LOCAL DEV ONLY (skipped on Cloud Run, where `K_SERVICE` is set and\n * every line becomes its own structured log entry — a multi-line banner there is pure noise). Copied\n * verbatim from the production service it was ported from, so a familiar splash marks \"the server is up and reachable\".\n */\n private logStartupBanner(port: number): void {\n if (process.env['K_SERVICE']) {\n return;\n }\n log.info(`\n ___ _____ _\n/ _| | _ \\\\ | |\n\\\\ \\`--. _ _ ___ ___ _ _ | |_/ /_ _ _ _| |_ _\n \\`--. \\\\/ _ \\\\ '_\\\\ \\\\ / / _ \\\\ '_| | // _ \\\\/ _\\` |/ _\\` | | | |\n/\\\\_/ / _/ | \\\\ V / _/ | | |\\\\ \\\\ _/ (_| | (_| | |_| |\n\\\\___/ \\\\_|_| \\\\_/ \\\\_|_| \\\\_| \\\\_\\\\_|\\\\_,_|\\\\_,_|\\\\_, |\n _/ |\n |_/\n\n Svr Ready!! port=${port}\n`);\n }\n\n /**\n * Bind EACH method of one ApiClient. The api's @ApiPath/@Endpoint decorators give the paths;\n * for each we wrap the matching client method (the proxy — RequestContext.run + header read +\n * JSON body parse + error→ApiErrorPayload all live in the wrapper/chain) and register the route.\n * This is one-to-one with a test: an HTTP POST maps straight to `client[method](dto)`.\n *\n * @returns the number of routes mounted for this api.\n */\n private mountApiClient(app: Express, apiClient: ApiClient): number {\n let count = 0;\n for (const route of apiClient.routes) {\n const path = this.expressPath(route);\n // The parser is chosen by the @Endpoint annotation, not the request Content-Type — and\n // so is whether the verbatim bytes survive the parse for an webhook(...) hook to verify.\n const wrapper = route.streaming\n ? this.middleware.createStreamExpressWrapper(\n apiClient.client[route.methodName],\n route,\n )\n : this.middleware.createExpressWrapper(\n apiClient.client[route.methodName],\n route,\n this.bodyReader,\n );\n this.registerHandler(app, route.httpMethod, path, wrapper.execute.bind(wrapper));\n count++;\n }\n return count;\n }\n\n /**\n * Contract `{name}` placeholders -> Express 5 `:name` route syntax. An EMPTY contract path\n * (`@ApiPath('')` + `@Endpoint(POST, '', ...)`, legal since #944) is served at the root, because every\n * request path Express sees starts with `/` and an empty route could never match.\n */\n private expressPath(route: RouteMetadata): string {\n if (route.path === '') return '/';\n return route.path.replace(/\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, ':$1');\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(`Unknown HTTP method: ${httpMethod}`);\n }\n }\n}\n"]}
|
|
@@ -115,7 +115,7 @@ export declare class WebpiecesMiddleware {
|
|
|
115
115
|
* @param formPost - True for an @Endpoint(..., { formPost: true }) route (parse body as
|
|
116
116
|
* urlencoded, not JSON). Default false = JSON.
|
|
117
117
|
* @param rawBody - True for an @Endpoint(..., { rawBody: true }) route: retain the verbatim
|
|
118
|
-
* bytes + absolute url on the HttpRequest so an
|
|
118
|
+
* bytes + absolute url on the HttpRequest so an webhook(...) hook can verify a vendor
|
|
119
119
|
* signature over them. Default false = the bytes are dropped once parsed.
|
|
120
120
|
* @param bodyReader - Where the body bytes come from (the router's choice, see
|
|
121
121
|
* `WebpiecesExpressRouter.setBodyReader`). Required so the router's choice can never be
|
|
@@ -195,7 +195,7 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
|
|
|
195
195
|
* @param formPost - True for an @Endpoint(..., { formPost: true }) route (parse body as
|
|
196
196
|
* urlencoded, not JSON). Default false = JSON.
|
|
197
197
|
* @param rawBody - True for an @Endpoint(..., { rawBody: true }) route: retain the verbatim
|
|
198
|
-
* bytes + absolute url on the HttpRequest so an
|
|
198
|
+
* bytes + absolute url on the HttpRequest so an webhook(...) hook can verify a vendor
|
|
199
199
|
* signature over them. Default false = the bytes are dropped once parsed.
|
|
200
200
|
* @param bodyReader - Where the body bytes come from (the router's choice, see
|
|
201
201
|
* `WebpiecesExpressRouter.setBodyReader`). Required so the router's choice can never be
|
|
@@ -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 { provideFrameworkSingleton, WebpiecesConfig } from '@webpieces/http-routing';\nimport { RouteMetadata, toError } from '@webpieces/core-util';\nimport { RequestContextHeaders } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\nimport { ExpressWrapper } from './ExpressWrapper';\nimport { StreamExpressWrapper } from './StreamExpressWrapper';\nimport { RequestBodyReader } from './body/RequestBodyReader';\n\nconst log = LogManager.getLogger('WebpiecesMiddleware');\n// CORS mount/allow/block lines log under their own name (not [WebpiecesMiddleware]); the backend\n// prepends \"[CORS]\" for us, so the message strings below carry no literal prefix of their own.\nconst corsLog = LogManager.getLogger('CORS');\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\n/**\n * WebpiecesMiddleware - Express middleware for WebPieces server.\n *\n * This class contains all Express middleware used by WebpiecesServer:\n * 1. errorHandler - Top-level 4-arg error middleware, returns HTML 500 page. Mounted AFTER the\n * routes so express can forward downstream failures to it (express routes errors DOWN a\n * separate pipeline, it does not bubble them back up through next()). Last-resort net only:\n * api routes translate their own errors to JSON inside the filter chain (ExpressWrapper).\n * 2. corsMiddleware - Opt-in CORS (mounted only when corsOrigins is non-empty).\n *\n * Per-request logging is intentionally NOT here — the api filter chain's LogApiCall already logs\n * every request/response with full context (requestId, method, path, body), so a plain express\n * START/END line would only duplicate it with less information.\n *\n * Route dispatch happens via Express's registered route handlers (the per-route ExpressWrapper),\n * NOT via this middleware.\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()\nexport class WebpiecesMiddleware {\n /** The ONE wire<->context transfer, handed to every route's ExpressWrapper. Stateless. */\n private readonly headers = new RequestContextHeaders();\n\n /**\n * Top-level error handler — the last-ditch catch-all. MUST be mounted AFTER all routes (see\n * {@link WebpiecesExpressRouter}). The 4-argument `(err, req, res, next)` signature is what\n * tells express this is an error-handling middleware: express's router forwards ANY downstream\n * failure to it — synchronous throws AND rejected async-handler promises alike (the router does\n * `promise.then(null, err => next(err))`, and a `next(err)` with a truthy arg jumps straight to\n * the first 4-arg middleware). This is why a `try { await next() } catch` wrapper is NOT needed\n * (and would not work) — express `next()` is not promise-aware, so it never hands the parent the\n * downstream promise; errors travel down this separate pipeline instead of bubbling back up.\n *\n * Returns an HTML 500 page. Api routes translate their own errors to JSON inside the filter\n * chain (JsonFilter/ExpressWrapper), so this normally only fires for failures OUTSIDE a route\n * (body parsing, unmatched paths, a bug in the wrapper itself).\n *\n * The page carries NO `error.message`. It used to render one into a `<pre>` block, which is the\n * same leak `WebpiecesDefaultErrorTranslator` closes on the JSON side and a worse one here: the errors that\n * reach THIS handler are the unhandled ones, whose messages are stack-adjacent internals nobody\n * wrote for a caller to read. The message is logged one line above, which is where it belongs.\n */\n // webpieces-disable no-any-unknown -- a thrown/forwarded express error is genuinely unknown until narrowed\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- express needs the 4-arg (err,req,res,next) arity to recognize this as error-handling middleware\n errorHandler(err: unknown, req: Request, res: Response, next: NextFunction): void {\n const error = toError(err);\n log.error(`Unhandled error: ${req.method} ${req.path}`, error);\n if (res.headersSent) {\n return;\n }\n // Return HTML error page (not JSON - api routes translate JSON errors in their filter chain)\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 </body>\n </html>\n `);\n }\n\n /**\n * CORS middleware. DO NOT MOUNT UNCONDITIONALLY — {@link WebpiecesExpressRouter.bindAndStartExpress}\n * mounts it ONLY when {@link WebpiecesConfig.corsOrigins} is non-empty, and that is the point.\n *\n * CORS exists solely to let a browser on a DIFFERENT origin call this api — in practice\n * `ng serve` on :4200 hitting an api on :8080 during development, or a UI hosted on a different\n * host than the api. A server that serves its own browser app needs NO cors at all, because a\n * browser does not apply cors to a same-origin request. So in production this middleware is\n * normally ABSENT, and absent is the safe state: every origin it allows gains the right to make\n * CREDENTIALED cross-origin calls and READ the responses. Mounting it unconditionally (as the\n * old corsForLocalhost did) handed that right to anything on the victim's localhost, in prod,\n * for no benefit whatsoever.\n *\n * When mounted, allows: a request with NO Origin (curl, server-to-server, a CLI); the server's\n * OWN origin; and EXACTLY the origins in `corsOrigins` — nothing is implicit. Anything else gets\n * a clean 403, never the HTML 500 the old `callback(new Error(...))` produced.\n *\n * SAME-ORIGIN MUST STAY ALLOWED even though a same-origin request needs no cors headers, because\n * a browser attaches an `Origin` header to EVERY POST — including a same-origin POST — and every\n * webpieces route is a POST. Once mounted, this middleware SEES that origin, so if it did not\n * allow it, it would 403 the server's own UI. That was the production bug.\n *\n * The same-origin test compares HOST ONLY, deliberately. Behind a TLS-terminating proxy (Cloud\n * Run, any load balancer) `req.protocol` is `http` while the browser's `Origin` says `https`, so\n * comparing full origins would reject the server's own origin on every deploy.\n *\n * @returns Express middleware handler for CORS\n */\n corsMiddleware(config?: WebpiecesConfig): RequestHandler {\n const allowedOrigins = config?.corsOrigins ?? [];\n corsLog.info(\n `CORS MOUNTED. Allowing same-origin + [${allowedOrigins.join(', ')}]. ` +\n `Every other browser origin gets a 403.`,\n );\n\n const handler = cors({\n origin: true, // reflect the request origin — we have already vetted it below\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],\n allowedHeaders: '*',\n exposedHeaders: '*',\n maxAge: 3600,\n });\n\n return (req: Request, res: Response, next: NextFunction): void => {\n const origin = req.headers.origin;\n if (!origin) {\n // No Origin -> not a browser cross-origin request; nothing to negotiate.\n next();\n return;\n }\n if (this.isOriginAllowed(origin, req.get('host'), allowedOrigins)) {\n handler(req, res, next);\n return;\n }\n corsLog.info(`Blocked origin: ${origin}`);\n res.status(403).json({\n name: 'CorsError',\n message: `CORS not allowed for origin: ${origin}`,\n });\n };\n }\n\n /**\n * Same-origin (HOST ONLY — see corsMiddleware() on why the scheme is deliberately ignored), or an\n * explicit entry in corsOrigins. NOTHING is implicit: localhost is allowed only if the config\n * asked for it, so a production server that enables cors for a cross-host UI does not silently\n * open the door to localhost as well.\n */\n private isOriginAllowed(\n origin: string,\n host: string | undefined,\n allowedOrigins: string[],\n ): boolean {\n let originHost: string;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a malformed Origin is untrusted browser input, not a server fault; it must become a 403 here, never bubble to the 500 chokepoint\n try {\n originHost = new URL(origin).host;\n } catch (err: unknown) {\n const error = toError(err);\n corsLog.info(`Malformed Origin header '${origin}': ${error.message}`);\n return false;\n }\n if (host !== undefined && originHost === host) {\n return true; // same-origin: the server's own UI calling its own api\n }\n return allowedOrigins.some((allowed: string): boolean =>\n this.matchesOrigin(origin, allowed),\n );\n }\n\n /**\n * Exact origin match, except a `*` in the PORT position matches any port: `http://localhost:*`\n * is what a developer writes, because the angular dev-server port moves around.\n *\n * The `*` is deliberately NOT a general wildcard — it never spans a host, and what follows the\n * prefix must be a real (digits-only) port. So `http://localhost:*` cannot be tricked into\n * matching `http://localhost.evil.com`, and a bare `*` matches nothing at all.\n */\n private matchesOrigin(origin: string, allowed: string): boolean {\n if (allowed === origin) {\n return true;\n }\n const wildcardSuffix = ':*';\n if (!allowed.endsWith(wildcardSuffix)) {\n return false;\n }\n const prefix = allowed.slice(0, -wildcardSuffix.length);\n if (!origin.startsWith(`${prefix}:`)) {\n return false;\n }\n const port = origin.slice(prefix.length + 1);\n return /^\\d+$/.test(port);\n }\n\n /**\n * Create an ExpressWrapper for a route.\n * The wrapper handles the full request/response cycle (symmetric design): it publishes the\n * HttpRequest + fills the context, then invokes the api client method (the proxy).\n *\n * @param clientMethod - The api client's method for this route (dto → response); the proxy\n * runs the filter chain + controller.\n * @param path - The route path (used to build the HttpRequest).\n * @param formPost - True for an @Endpoint(..., { formPost: true }) route (parse body as\n * urlencoded, not JSON). Default false = JSON.\n * @param rawBody - True for an @Endpoint(..., { rawBody: true }) route: retain the verbatim\n * bytes + absolute url on the HttpRequest so an @WpAuthWebhook hook can verify a vendor\n * signature over them. Default false = the bytes are dropped once parsed.\n * @param bodyReader - Where the body bytes come from (the router's choice, see\n * `WebpiecesExpressRouter.setBodyReader`). Required so the router's choice can never be\n * silently dropped on the way to the wrapper.\n * @returns ExpressWrapper instance\n */\n createExpressWrapper(\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n clientMethod: (...args: unknown[]) => Promise<unknown>,\n route: RouteMetadata,\n bodyReader: RequestBodyReader,\n ): ExpressWrapper {\n return new ExpressWrapper(\n clientMethod,\n route.path,\n this.headers,\n route.formPost,\n route.rawBody,\n undefined,\n route,\n bodyReader,\n );\n }\n\n createStreamExpressWrapper(\n // webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary\n clientMethod: (...args: unknown[]) => Promise<unknown>,\n route: RouteMetadata,\n ): StreamExpressWrapper {\n return new StreamExpressWrapper(clientMethod, route, this.headers);\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 { provideFrameworkSingleton, WebpiecesConfig } from '@webpieces/http-routing';\nimport { RouteMetadata, toError } from '@webpieces/core-util';\nimport { RequestContextHeaders } from '@webpieces/core-context';\nimport { LogManager } from '@webpieces/core-util';\nimport { ExpressWrapper } from './ExpressWrapper';\nimport { StreamExpressWrapper } from './StreamExpressWrapper';\nimport { RequestBodyReader } from './body/RequestBodyReader';\n\nconst log = LogManager.getLogger('WebpiecesMiddleware');\n// CORS mount/allow/block lines log under their own name (not [WebpiecesMiddleware]); the backend\n// prepends \"[CORS]\" for us, so the message strings below carry no literal prefix of their own.\nconst corsLog = LogManager.getLogger('CORS');\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\n/**\n * WebpiecesMiddleware - Express middleware for WebPieces server.\n *\n * This class contains all Express middleware used by WebpiecesServer:\n * 1. errorHandler - Top-level 4-arg error middleware, returns HTML 500 page. Mounted AFTER the\n * routes so express can forward downstream failures to it (express routes errors DOWN a\n * separate pipeline, it does not bubble them back up through next()). Last-resort net only:\n * api routes translate their own errors to JSON inside the filter chain (ExpressWrapper).\n * 2. corsMiddleware - Opt-in CORS (mounted only when corsOrigins is non-empty).\n *\n * Per-request logging is intentionally NOT here — the api filter chain's LogApiCall already logs\n * every request/response with full context (requestId, method, path, body), so a plain express\n * START/END line would only duplicate it with less information.\n *\n * Route dispatch happens via Express's registered route handlers (the per-route ExpressWrapper),\n * NOT via this middleware.\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()\nexport class WebpiecesMiddleware {\n /** The ONE wire<->context transfer, handed to every route's ExpressWrapper. Stateless. */\n private readonly headers = new RequestContextHeaders();\n\n /**\n * Top-level error handler — the last-ditch catch-all. MUST be mounted AFTER all routes (see\n * {@link WebpiecesExpressRouter}). The 4-argument `(err, req, res, next)` signature is what\n * tells express this is an error-handling middleware: express's router forwards ANY downstream\n * failure to it — synchronous throws AND rejected async-handler promises alike (the router does\n * `promise.then(null, err => next(err))`, and a `next(err)` with a truthy arg jumps straight to\n * the first 4-arg middleware). This is why a `try { await next() } catch` wrapper is NOT needed\n * (and would not work) — express `next()` is not promise-aware, so it never hands the parent the\n * downstream promise; errors travel down this separate pipeline instead of bubbling back up.\n *\n * Returns an HTML 500 page. Api routes translate their own errors to JSON inside the filter\n * chain (JsonFilter/ExpressWrapper), so this normally only fires for failures OUTSIDE a route\n * (body parsing, unmatched paths, a bug in the wrapper itself).\n *\n * The page carries NO `error.message`. It used to render one into a `<pre>` block, which is the\n * same leak `WebpiecesDefaultErrorTranslator` closes on the JSON side and a worse one here: the errors that\n * reach THIS handler are the unhandled ones, whose messages are stack-adjacent internals nobody\n * wrote for a caller to read. The message is logged one line above, which is where it belongs.\n */\n // webpieces-disable no-any-unknown -- a thrown/forwarded express error is genuinely unknown until narrowed\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- express needs the 4-arg (err,req,res,next) arity to recognize this as error-handling middleware\n errorHandler(err: unknown, req: Request, res: Response, next: NextFunction): void {\n const error = toError(err);\n log.error(`Unhandled error: ${req.method} ${req.path}`, error);\n if (res.headersSent) {\n return;\n }\n // Return HTML error page (not JSON - api routes translate JSON errors in their filter chain)\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 </body>\n </html>\n `);\n }\n\n /**\n * CORS middleware. DO NOT MOUNT UNCONDITIONALLY — {@link WebpiecesExpressRouter.bindAndStartExpress}\n * mounts it ONLY when {@link WebpiecesConfig.corsOrigins} is non-empty, and that is the point.\n *\n * CORS exists solely to let a browser on a DIFFERENT origin call this api — in practice\n * `ng serve` on :4200 hitting an api on :8080 during development, or a UI hosted on a different\n * host than the api. A server that serves its own browser app needs NO cors at all, because a\n * browser does not apply cors to a same-origin request. So in production this middleware is\n * normally ABSENT, and absent is the safe state: every origin it allows gains the right to make\n * CREDENTIALED cross-origin calls and READ the responses. Mounting it unconditionally (as the\n * old corsForLocalhost did) handed that right to anything on the victim's localhost, in prod,\n * for no benefit whatsoever.\n *\n * When mounted, allows: a request with NO Origin (curl, server-to-server, a CLI); the server's\n * OWN origin; and EXACTLY the origins in `corsOrigins` — nothing is implicit. Anything else gets\n * a clean 403, never the HTML 500 the old `callback(new Error(...))` produced.\n *\n * SAME-ORIGIN MUST STAY ALLOWED even though a same-origin request needs no cors headers, because\n * a browser attaches an `Origin` header to EVERY POST — including a same-origin POST — and every\n * webpieces route is a POST. Once mounted, this middleware SEES that origin, so if it did not\n * allow it, it would 403 the server's own UI. That was the production bug.\n *\n * The same-origin test compares HOST ONLY, deliberately. Behind a TLS-terminating proxy (Cloud\n * Run, any load balancer) `req.protocol` is `http` while the browser's `Origin` says `https`, so\n * comparing full origins would reject the server's own origin on every deploy.\n *\n * @returns Express middleware handler for CORS\n */\n corsMiddleware(config?: WebpiecesConfig): RequestHandler {\n const allowedOrigins = config?.corsOrigins ?? [];\n corsLog.info(\n `CORS MOUNTED. Allowing same-origin + [${allowedOrigins.join(', ')}]. ` +\n `Every other browser origin gets a 403.`,\n );\n\n const handler = cors({\n origin: true, // reflect the request origin — we have already vetted it below\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],\n allowedHeaders: '*',\n exposedHeaders: '*',\n maxAge: 3600,\n });\n\n return (req: Request, res: Response, next: NextFunction): void => {\n const origin = req.headers.origin;\n if (!origin) {\n // No Origin -> not a browser cross-origin request; nothing to negotiate.\n next();\n return;\n }\n if (this.isOriginAllowed(origin, req.get('host'), allowedOrigins)) {\n handler(req, res, next);\n return;\n }\n corsLog.info(`Blocked origin: ${origin}`);\n res.status(403).json({\n name: 'CorsError',\n message: `CORS not allowed for origin: ${origin}`,\n });\n };\n }\n\n /**\n * Same-origin (HOST ONLY — see corsMiddleware() on why the scheme is deliberately ignored), or an\n * explicit entry in corsOrigins. NOTHING is implicit: localhost is allowed only if the config\n * asked for it, so a production server that enables cors for a cross-host UI does not silently\n * open the door to localhost as well.\n */\n private isOriginAllowed(\n origin: string,\n host: string | undefined,\n allowedOrigins: string[],\n ): boolean {\n let originHost: string;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a malformed Origin is untrusted browser input, not a server fault; it must become a 403 here, never bubble to the 500 chokepoint\n try {\n originHost = new URL(origin).host;\n } catch (err: unknown) {\n const error = toError(err);\n corsLog.info(`Malformed Origin header '${origin}': ${error.message}`);\n return false;\n }\n if (host !== undefined && originHost === host) {\n return true; // same-origin: the server's own UI calling its own api\n }\n return allowedOrigins.some((allowed: string): boolean =>\n this.matchesOrigin(origin, allowed),\n );\n }\n\n /**\n * Exact origin match, except a `*` in the PORT position matches any port: `http://localhost:*`\n * is what a developer writes, because the angular dev-server port moves around.\n *\n * The `*` is deliberately NOT a general wildcard — it never spans a host, and what follows the\n * prefix must be a real (digits-only) port. So `http://localhost:*` cannot be tricked into\n * matching `http://localhost.evil.com`, and a bare `*` matches nothing at all.\n */\n private matchesOrigin(origin: string, allowed: string): boolean {\n if (allowed === origin) {\n return true;\n }\n const wildcardSuffix = ':*';\n if (!allowed.endsWith(wildcardSuffix)) {\n return false;\n }\n const prefix = allowed.slice(0, -wildcardSuffix.length);\n if (!origin.startsWith(`${prefix}:`)) {\n return false;\n }\n const port = origin.slice(prefix.length + 1);\n return /^\\d+$/.test(port);\n }\n\n /**\n * Create an ExpressWrapper for a route.\n * The wrapper handles the full request/response cycle (symmetric design): it publishes the\n * HttpRequest + fills the context, then invokes the api client method (the proxy).\n *\n * @param clientMethod - The api client's method for this route (dto → response); the proxy\n * runs the filter chain + controller.\n * @param path - The route path (used to build the HttpRequest).\n * @param formPost - True for an @Endpoint(..., { formPost: true }) route (parse body as\n * urlencoded, not JSON). Default false = JSON.\n * @param rawBody - True for an @Endpoint(..., { rawBody: true }) route: retain the verbatim\n * bytes + absolute url on the HttpRequest so an webhook(...) hook can verify a vendor\n * signature over them. Default false = the bytes are dropped once parsed.\n * @param bodyReader - Where the body bytes come from (the router's choice, see\n * `WebpiecesExpressRouter.setBodyReader`). Required so the router's choice can never be\n * silently dropped on the way to the wrapper.\n * @returns ExpressWrapper instance\n */\n createExpressWrapper(\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n clientMethod: (...args: unknown[]) => Promise<unknown>,\n route: RouteMetadata,\n bodyReader: RequestBodyReader,\n ): ExpressWrapper {\n return new ExpressWrapper(\n clientMethod,\n route.path,\n this.headers,\n route.formPost,\n route.rawBody,\n undefined,\n route,\n bodyReader,\n );\n }\n\n createStreamExpressWrapper(\n // webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary\n clientMethod: (...args: unknown[]) => Promise<unknown>,\n route: RouteMetadata,\n ): StreamExpressWrapper {\n return new StreamExpressWrapper(clientMethod, route, this.headers);\n }\n}\n"]}
|