@webpieces/http-server 0.4.773 → 0.4.775

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.
@@ -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';\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 `ApiErrorHttpMapper` 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 * @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 ): ExpressWrapper {\n return new ExpressWrapper(\n clientMethod,\n route.path,\n this.headers,\n route.formPost,\n route.rawBody,\n undefined,\n route,\n );\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 `ApiErrorHttpMapper` 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"]}
@@ -0,0 +1,24 @@
1
+ import { Request } from 'express';
2
+ import { RequestBodyReader } from './RequestBodyReader';
3
+ /**
4
+ * OPT-IN {@link RequestBodyReader} for hosts that parse the body BEFORE webpieces runs: Cloud
5
+ * Functions gen2 / Firebase (the Functions Framework), or an app that mounts
6
+ * `express.json({ verify })` ahead of webpieces. Enable it with
7
+ * `router.setBodyReader(new PreConsumedBodyReader())` before `bindExpress(app)`.
8
+ *
9
+ * Decision order:
10
+ * 1. `req.rawBody` is a `Buffer`: use it. These are the exact bytes the sender transmitted, so a
11
+ * `{ rawBody: true }` webhook route still verifies its signature. The size cap is still enforced.
12
+ * 2. the stream was NOT consumed: read it normally (same as {@link StreamBodyReader}).
13
+ * 3. otherwise: fail fast naming the cure. The host must expose `req.rawBody`.
14
+ *
15
+ * Deliberately NO `JSON.stringify(req.body)` fallback. Re-serialized bytes differ from what was sent
16
+ * (whitespace, key order, number formatting), which silently breaks webhook signature verification,
17
+ * and the host this exists for (#937) always sets `req.rawBody`.
18
+ *
19
+ * Opt-in rather than the default so every correctly mounted server keeps the plain stream read.
20
+ */
21
+ export declare class PreConsumedBodyReader implements RequestBodyReader {
22
+ private readonly streamReader;
23
+ read(req: Request, maxBodyBytes: number): Promise<Buffer>;
24
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PreConsumedBodyReader = void 0;
4
+ const core_util_1 = require("@webpieces/core-util");
5
+ const StreamBodyReader_1 = require("./StreamBodyReader");
6
+ /**
7
+ * OPT-IN {@link RequestBodyReader} for hosts that parse the body BEFORE webpieces runs: Cloud
8
+ * Functions gen2 / Firebase (the Functions Framework), or an app that mounts
9
+ * `express.json({ verify })` ahead of webpieces. Enable it with
10
+ * `router.setBodyReader(new PreConsumedBodyReader())` before `bindExpress(app)`.
11
+ *
12
+ * Decision order:
13
+ * 1. `req.rawBody` is a `Buffer`: use it. These are the exact bytes the sender transmitted, so a
14
+ * `{ rawBody: true }` webhook route still verifies its signature. The size cap is still enforced.
15
+ * 2. the stream was NOT consumed: read it normally (same as {@link StreamBodyReader}).
16
+ * 3. otherwise: fail fast naming the cure. The host must expose `req.rawBody`.
17
+ *
18
+ * Deliberately NO `JSON.stringify(req.body)` fallback. Re-serialized bytes differ from what was sent
19
+ * (whitespace, key order, number formatting), which silently breaks webhook signature verification,
20
+ * and the host this exists for (#937) always sets `req.rawBody`.
21
+ *
22
+ * Opt-in rather than the default so every correctly mounted server keeps the plain stream read.
23
+ */
24
+ class PreConsumedBodyReader {
25
+ streamReader = new StreamBodyReader_1.StreamBodyReader();
26
+ read(req, maxBodyBytes) {
27
+ // webpieces-disable no-any-unknown -- `rawBody` is set by the host, absent from the express types
28
+ const rawBody = req['rawBody'];
29
+ if (Buffer.isBuffer(rawBody)) {
30
+ if (rawBody.length > maxBodyBytes) {
31
+ return Promise.reject(new core_util_1.ApiBadRequestError(`Request body exceeds the ${maxBodyBytes} byte limit`));
32
+ }
33
+ return Promise.resolve(rawBody);
34
+ }
35
+ if (!this.streamReader.isConsumed(req)) {
36
+ return this.streamReader.read(req, maxBodyBytes);
37
+ }
38
+ return Promise.reject(new core_util_1.ApiImplementationError('PreConsumedBodyReader: the request body stream was already consumed and ' +
39
+ '`req.rawBody` is not a Buffer, so the verbatim body bytes are unavailable. ' +
40
+ 'Fix: have the host keep the bytes on `req.rawBody`, e.g. ' +
41
+ '`express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })`, ' +
42
+ 'or mount webpieces BEFORE the body parser.'));
43
+ }
44
+ }
45
+ exports.PreConsumedBodyReader = PreConsumedBodyReader;
46
+ //# sourceMappingURL=PreConsumedBodyReader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PreConsumedBodyReader.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/body/PreConsumedBodyReader.ts"],"names":[],"mappings":";;;AACA,oDAAkF;AAElF,yDAAsD;AAEtD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAa,qBAAqB;IACb,YAAY,GAAG,IAAI,mCAAgB,EAAE,CAAC;IAEvD,IAAI,CAAC,GAAY,EAAE,YAAoB;QACnC,kGAAkG;QAClG,MAAM,OAAO,GAAI,GAA0C,CAAC,SAAS,CAAC,CAAC;QACvE,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,IAAI,OAAO,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;gBAChC,OAAO,OAAO,CAAC,MAAM,CACjB,IAAI,8BAAkB,CAAC,4BAA4B,YAAY,aAAa,CAAC,CAChF,CAAC;YACN,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACpC,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACrC,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,OAAO,CAAC,MAAM,CACjB,IAAI,kCAAsB,CACtB,0EAA0E;YACtE,6EAA6E;YAC7E,2DAA2D;YAC3D,0EAA0E;YAC1E,4CAA4C,CACnD,CACJ,CAAC;IACN,CAAC;CACJ;AA3BD,sDA2BC","sourcesContent":["import { Request } from 'express';\nimport { ApiBadRequestError, ApiImplementationError } from '@webpieces/core-util';\nimport { RequestBodyReader } from './RequestBodyReader';\nimport { StreamBodyReader } from './StreamBodyReader';\n\n/**\n * OPT-IN {@link RequestBodyReader} for hosts that parse the body BEFORE webpieces runs: Cloud\n * Functions gen2 / Firebase (the Functions Framework), or an app that mounts\n * `express.json({ verify })` ahead of webpieces. Enable it with\n * `router.setBodyReader(new PreConsumedBodyReader())` before `bindExpress(app)`.\n *\n * Decision order:\n * 1. `req.rawBody` is a `Buffer`: use it. These are the exact bytes the sender transmitted, so a\n * `{ rawBody: true }` webhook route still verifies its signature. The size cap is still enforced.\n * 2. the stream was NOT consumed: read it normally (same as {@link StreamBodyReader}).\n * 3. otherwise: fail fast naming the cure. The host must expose `req.rawBody`.\n *\n * Deliberately NO `JSON.stringify(req.body)` fallback. Re-serialized bytes differ from what was sent\n * (whitespace, key order, number formatting), which silently breaks webhook signature verification,\n * and the host this exists for (#937) always sets `req.rawBody`.\n *\n * Opt-in rather than the default so every correctly mounted server keeps the plain stream read.\n */\nexport class PreConsumedBodyReader implements RequestBodyReader {\n private readonly streamReader = new StreamBodyReader();\n\n read(req: Request, maxBodyBytes: number): Promise<Buffer> {\n // webpieces-disable no-any-unknown -- `rawBody` is set by the host, absent from the express types\n const rawBody = (req as unknown as Record<string, unknown>)['rawBody'];\n if (Buffer.isBuffer(rawBody)) {\n if (rawBody.length > maxBodyBytes) {\n return Promise.reject(\n new ApiBadRequestError(`Request body exceeds the ${maxBodyBytes} byte limit`),\n );\n }\n return Promise.resolve(rawBody);\n }\n if (!this.streamReader.isConsumed(req)) {\n return this.streamReader.read(req, maxBodyBytes);\n }\n return Promise.reject(\n new ApiImplementationError(\n 'PreConsumedBodyReader: the request body stream was already consumed and ' +\n '`req.rawBody` is not a Buffer, so the verbatim body bytes are unavailable. ' +\n 'Fix: have the host keep the bytes on `req.rawBody`, e.g. ' +\n '`express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })`, ' +\n 'or mount webpieces BEFORE the body parser.',\n ),\n );\n }\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import { Request } from 'express';
2
+ /**
3
+ * How a webpieces route obtains the inbound body BYTES from express.
4
+ *
5
+ * webpieces parses bodies itself (it never mounts `express.json()`), so by default it reads the
6
+ * request stream. That only works when nothing upstream read the stream first. Some hosts do:
7
+ * Cloud Functions gen2 / Firebase run a body parser BEFORE the app sees the request and leave the
8
+ * verbatim bytes on `req.rawBody`. Reading the drained stream there waits for an `end` event that
9
+ * already fired, so the request hangs until the platform timeout (issue #937).
10
+ *
11
+ * The reader is chosen ONCE per router via `WebpiecesExpressRouter.setBodyReader(...)`:
12
+ * - {@link StreamBodyReader}: the default. Reads the stream, fails fast if it was already consumed.
13
+ * - {@link PreConsumedBodyReader}: opt-in for hosts that parse first. Uses `req.rawBody`.
14
+ *
15
+ * An interface (not a class) because it is behaviour: an implementation decides WHERE the bytes come
16
+ * from. Whatever it returns must be the bytes the sender transmitted, because a `{ rawBody: true }`
17
+ * webhook route verifies a vendor signature over exactly this buffer.
18
+ */
19
+ export interface RequestBodyReader {
20
+ /**
21
+ * @param req - the express request of a POST/PUT/PATCH route.
22
+ * @param maxBodyBytes - the inbound cap. An implementation MUST refuse a larger body with an
23
+ * `ApiBadRequestError` rather than buffer it.
24
+ * @returns the verbatim body bytes (empty for an empty body).
25
+ */
26
+ read(req: Request, maxBodyBytes: number): Promise<Buffer>;
27
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=RequestBodyReader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RequestBodyReader.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/body/RequestBodyReader.ts"],"names":[],"mappings":"","sourcesContent":["import { Request } from 'express';\n\n/**\n * How a webpieces route obtains the inbound body BYTES from express.\n *\n * webpieces parses bodies itself (it never mounts `express.json()`), so by default it reads the\n * request stream. That only works when nothing upstream read the stream first. Some hosts do:\n * Cloud Functions gen2 / Firebase run a body parser BEFORE the app sees the request and leave the\n * verbatim bytes on `req.rawBody`. Reading the drained stream there waits for an `end` event that\n * already fired, so the request hangs until the platform timeout (issue #937).\n *\n * The reader is chosen ONCE per router via `WebpiecesExpressRouter.setBodyReader(...)`:\n * - {@link StreamBodyReader}: the default. Reads the stream, fails fast if it was already consumed.\n * - {@link PreConsumedBodyReader}: opt-in for hosts that parse first. Uses `req.rawBody`.\n *\n * An interface (not a class) because it is behaviour: an implementation decides WHERE the bytes come\n * from. Whatever it returns must be the bytes the sender transmitted, because a `{ rawBody: true }`\n * webhook route verifies a vendor signature over exactly this buffer.\n */\nexport interface RequestBodyReader {\n /**\n * @param req - the express request of a POST/PUT/PATCH route.\n * @param maxBodyBytes - the inbound cap. An implementation MUST refuse a larger body with an\n * `ApiBadRequestError` rather than buffer it.\n * @returns the verbatim body bytes (empty for an empty body).\n */\n read(req: Request, maxBodyBytes: number): Promise<Buffer>;\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import { Request } from 'express';
2
+ import { RequestBodyReader } from './RequestBodyReader';
3
+ /**
4
+ * The DEFAULT {@link RequestBodyReader}: read the body off the express request stream.
5
+ *
6
+ * This is the logic `ExpressWrapper.readRequestBody` always had, unchanged, plus ONE guard. A stream
7
+ * that something upstream already consumed (a body parser mounted ahead of webpieces, or a host like
8
+ * Cloud Functions gen2 that parses before the app runs) never emits `end` again, so waiting on it
9
+ * hung the request until the platform timeout with nothing in the logs (issue #937). That case now
10
+ * throws immediately, and the message names both cures.
11
+ */
12
+ export declare class StreamBodyReader implements RequestBodyReader {
13
+ read(req: Request, maxBodyBytes: number): Promise<Buffer>;
14
+ /**
15
+ * True when the request stream can no longer produce a body.
16
+ *
17
+ * `readableEnded` is set once `end` has been emitted, which every complete read (the Functions
18
+ * Framework, body-parser 2's raw-body) leaves behind. body-parser 1.x also marks the request with
19
+ * `_body = true` after parsing it. That marker is checked together with `complete` so a stray
20
+ * `_body` field on a request nobody has read yet is not mistaken for a consumed stream.
21
+ */
22
+ isConsumed(req: Request): boolean;
23
+ /**
24
+ * Read the raw request body as BYTES (we parse manually rather than mounting express.json()).
25
+ *
26
+ * Bytes, not a growing string: a per-chunk `toString()` splits any multi-byte character that
27
+ * straddles a chunk boundary into two replacement characters, so the body a webhook hook verified
28
+ * would not be the body the vendor signed.
29
+ *
30
+ * REFUSES a body over `maxBodyBytes` the moment it crosses the line. The chunks read so far are
31
+ * dropped and the stream is destroyed, so an oversize body is never fully buffered. It answers
32
+ * 400 rather than 401 even on a webhook route, unavoidably: there is no way to authenticate a
33
+ * caller whose request we are refusing to finish reading, and that ordering is the point.
34
+ */
35
+ private readStream;
36
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StreamBodyReader = void 0;
4
+ const core_util_1 = require("@webpieces/core-util");
5
+ /**
6
+ * The DEFAULT {@link RequestBodyReader}: read the body off the express request stream.
7
+ *
8
+ * This is the logic `ExpressWrapper.readRequestBody` always had, unchanged, plus ONE guard. A stream
9
+ * that something upstream already consumed (a body parser mounted ahead of webpieces, or a host like
10
+ * Cloud Functions gen2 that parses before the app runs) never emits `end` again, so waiting on it
11
+ * hung the request until the platform timeout with nothing in the logs (issue #937). That case now
12
+ * throws immediately, and the message names both cures.
13
+ */
14
+ class StreamBodyReader {
15
+ read(req, maxBodyBytes) {
16
+ if (this.isConsumed(req)) {
17
+ return Promise.reject(new core_util_1.ApiImplementationError('The request body stream was already consumed before webpieces could read it ' +
18
+ '(a body parser or the hosting platform read it first), and reading it again ' +
19
+ 'would hang forever. Fix: call ' +
20
+ '`router.setBodyReader(new PreConsumedBodyReader())` before ' +
21
+ '`bindExpress(app)` when the host keeps the verbatim bytes on `req.rawBody` ' +
22
+ '(Cloud Functions gen2 / Firebase), or mount webpieces BEFORE the body parser.'));
23
+ }
24
+ return this.readStream(req, maxBodyBytes);
25
+ }
26
+ /**
27
+ * True when the request stream can no longer produce a body.
28
+ *
29
+ * `readableEnded` is set once `end` has been emitted, which every complete read (the Functions
30
+ * Framework, body-parser 2's raw-body) leaves behind. body-parser 1.x also marks the request with
31
+ * `_body = true` after parsing it. That marker is checked together with `complete` so a stray
32
+ * `_body` field on a request nobody has read yet is not mistaken for a consumed stream.
33
+ */
34
+ isConsumed(req) {
35
+ if (req.readableEnded) {
36
+ return true;
37
+ }
38
+ // webpieces-disable no-any-unknown -- `_body` is body-parser 1.x's private marker, absent from the express types
39
+ const marker = req['_body'];
40
+ return req.complete === true && marker === true;
41
+ }
42
+ /**
43
+ * Read the raw request body as BYTES (we parse manually rather than mounting express.json()).
44
+ *
45
+ * Bytes, not a growing string: a per-chunk `toString()` splits any multi-byte character that
46
+ * straddles a chunk boundary into two replacement characters, so the body a webhook hook verified
47
+ * would not be the body the vendor signed.
48
+ *
49
+ * REFUSES a body over `maxBodyBytes` the moment it crosses the line. The chunks read so far are
50
+ * dropped and the stream is destroyed, so an oversize body is never fully buffered. It answers
51
+ * 400 rather than 401 even on a webhook route, unavoidably: there is no way to authenticate a
52
+ * caller whose request we are refusing to finish reading, and that ordering is the point.
53
+ */
54
+ readStream(req, maxBodyBytes) {
55
+ return new Promise((resolve, reject) => {
56
+ let chunks = [];
57
+ let size = 0;
58
+ // A socket emits Buffers; a stream someone put in string mode (or a test's Readable.from)
59
+ // emits strings. Normalize to bytes ONCE, here, so everything downstream counts and
60
+ // concatenates the same units.
61
+ req.on('data', (data) => {
62
+ const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
63
+ size += chunk.length;
64
+ if (size > maxBodyBytes) {
65
+ chunks = [];
66
+ req.destroy();
67
+ reject(new core_util_1.ApiBadRequestError(`Request body exceeds the ${maxBodyBytes} byte limit`));
68
+ return;
69
+ }
70
+ chunks.push(chunk);
71
+ });
72
+ req.on('end', () => {
73
+ resolve(Buffer.concat(chunks));
74
+ });
75
+ req.on('error', (err) => {
76
+ reject(err);
77
+ });
78
+ });
79
+ }
80
+ }
81
+ exports.StreamBodyReader = StreamBodyReader;
82
+ //# sourceMappingURL=StreamBodyReader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StreamBodyReader.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/body/StreamBodyReader.ts"],"names":[],"mappings":";;;AACA,oDAAkF;AAGlF;;;;;;;;GAQG;AACH,MAAa,gBAAgB;IACzB,IAAI,CAAC,GAAY,EAAE,YAAoB;QACnC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC,MAAM,CACjB,IAAI,kCAAsB,CACtB,8EAA8E;gBAC1E,8EAA8E;gBAC9E,gCAAgC;gBAChC,6DAA6D;gBAC7D,6EAA6E;gBAC7E,+EAA+E,CACtF,CACJ,CAAC;QACN,CAAC;QACD,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC9C,CAAC;IAED;;;;;;;OAOG;IACH,UAAU,CAAC,GAAY;QACnB,IAAI,GAAG,CAAC,aAAa,EAAE,CAAC;YACpB,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,iHAAiH;QACjH,MAAM,MAAM,GAAI,GAA0C,CAAC,OAAO,CAAC,CAAC;QACpE,OAAO,GAAG,CAAC,QAAQ,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,CAAC;IACpD,CAAC;IAED;;;;;;;;;;;OAWG;IACK,UAAU,CAAC,GAAY,EAAE,YAAoB;QACjD,OAAO,IAAI,OAAO,CAAC,CAAC,OAA+B,EAAE,MAA4B,EAAE,EAAE;YACjF,IAAI,MAAM,GAAa,EAAE,CAAC;YAC1B,IAAI,IAAI,GAAG,CAAC,CAAC;YACb,0FAA0F;YAC1F,oFAAoF;YACpF,+BAA+B;YAC/B,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAqB,EAAE,EAAE;gBACrC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;gBACvE,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC;gBACrB,IAAI,IAAI,GAAG,YAAY,EAAE,CAAC;oBACtB,MAAM,GAAG,EAAE,CAAC;oBACZ,GAAG,CAAC,OAAO,EAAE,CAAC;oBACd,MAAM,CACF,IAAI,8BAAkB,CAAC,4BAA4B,YAAY,aAAa,CAAC,CAChF,CAAC;oBACF,OAAO;gBACX,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC,CAAC,CAAC;YACH,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;gBACf,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YACnC,CAAC,CAAC,CAAC;YACH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;gBAC3B,MAAM,CAAC,GAAG,CAAC,CAAC;YAChB,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AA1ED,4CA0EC","sourcesContent":["import { Request } from 'express';\nimport { ApiBadRequestError, ApiImplementationError } from '@webpieces/core-util';\nimport { RequestBodyReader } from './RequestBodyReader';\n\n/**\n * The DEFAULT {@link RequestBodyReader}: read the body off the express request stream.\n *\n * This is the logic `ExpressWrapper.readRequestBody` always had, unchanged, plus ONE guard. A stream\n * that something upstream already consumed (a body parser mounted ahead of webpieces, or a host like\n * Cloud Functions gen2 that parses before the app runs) never emits `end` again, so waiting on it\n * hung the request until the platform timeout with nothing in the logs (issue #937). That case now\n * throws immediately, and the message names both cures.\n */\nexport class StreamBodyReader implements RequestBodyReader {\n read(req: Request, maxBodyBytes: number): Promise<Buffer> {\n if (this.isConsumed(req)) {\n return Promise.reject(\n new ApiImplementationError(\n 'The request body stream was already consumed before webpieces could read it ' +\n '(a body parser or the hosting platform read it first), and reading it again ' +\n 'would hang forever. Fix: call ' +\n '`router.setBodyReader(new PreConsumedBodyReader())` before ' +\n '`bindExpress(app)` when the host keeps the verbatim bytes on `req.rawBody` ' +\n '(Cloud Functions gen2 / Firebase), or mount webpieces BEFORE the body parser.',\n ),\n );\n }\n return this.readStream(req, maxBodyBytes);\n }\n\n /**\n * True when the request stream can no longer produce a body.\n *\n * `readableEnded` is set once `end` has been emitted, which every complete read (the Functions\n * Framework, body-parser 2's raw-body) leaves behind. body-parser 1.x also marks the request with\n * `_body = true` after parsing it. That marker is checked together with `complete` so a stray\n * `_body` field on a request nobody has read yet is not mistaken for a consumed stream.\n */\n isConsumed(req: Request): boolean {\n if (req.readableEnded) {\n return true;\n }\n // webpieces-disable no-any-unknown -- `_body` is body-parser 1.x's private marker, absent from the express types\n const marker = (req as unknown as Record<string, unknown>)['_body'];\n return req.complete === true && marker === true;\n }\n\n /**\n * Read the raw request body as BYTES (we parse manually rather than mounting express.json()).\n *\n * Bytes, not a growing string: a per-chunk `toString()` splits any multi-byte character that\n * straddles a chunk boundary into two replacement characters, so the body a webhook hook verified\n * would not be the body the vendor signed.\n *\n * REFUSES a body over `maxBodyBytes` the moment it crosses the line. The chunks read so far are\n * dropped and the stream is destroyed, so an oversize body is never fully buffered. It answers\n * 400 rather than 401 even on a webhook route, unavoidably: there is no way to authenticate a\n * caller whose request we are refusing to finish reading, and that ordering is the point.\n */\n private readStream(req: Request, maxBodyBytes: number): Promise<Buffer> {\n return new Promise((resolve: (body: Buffer) => void, reject: (err: Error) => void) => {\n let chunks: Buffer[] = [];\n let size = 0;\n // A socket emits Buffers; a stream someone put in string mode (or a test's Readable.from)\n // emits strings. Normalize to bytes ONCE, here, so everything downstream counts and\n // concatenates the same units.\n req.on('data', (data: Buffer | string) => {\n const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');\n size += chunk.length;\n if (size > maxBodyBytes) {\n chunks = [];\n req.destroy();\n reject(\n new ApiBadRequestError(`Request body exceeds the ${maxBodyBytes} byte limit`),\n );\n return;\n }\n chunks.push(chunk);\n });\n req.on('end', () => {\n resolve(Buffer.concat(chunks));\n });\n req.on('error', (err: Error) => {\n reject(err);\n });\n });\n }\n}\n"]}
package/src/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { WebpiecesExpressRouter } from './WebpiecesExpressRouter';
2
2
  export { WebpiecesMiddleware } from './WebpiecesMiddleware';
3
+ export { RequestBodyReader } from './body/RequestBodyReader';
4
+ export { StreamBodyReader } from './body/StreamBodyReader';
5
+ export { PreConsumedBodyReader } from './body/PreConsumedBodyReader';
3
6
  export { RecordingFilter } from './filters/RecordingFilter';
4
7
  export { ApiErrorHttpMapper } from './ApiErrorHttpMapper';
5
8
  export { TestCaseRecorderImpl } from './recorder/TestCaseRecorderImpl';
package/src/index.js CHANGED
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SharedSecrets = exports.AuthenticatedCaller = 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.ApiErrorHttpMapper = exports.RecordingFilter = exports.WebpiecesMiddleware = exports.WebpiecesExpressRouter = void 0;
3
+ exports.SharedSecrets = exports.AuthenticatedCaller = 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.ApiErrorHttpMapper = exports.RecordingFilter = exports.PreConsumedBodyReader = exports.StreamBodyReader = exports.WebpiecesMiddleware = exports.WebpiecesExpressRouter = void 0;
4
4
  // Express adapter (the only place express lifecycle lives) over the node-only ApiFactory
5
5
  var WebpiecesExpressRouter_1 = require("./WebpiecesExpressRouter");
6
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 StreamBodyReader_1 = require("./body/StreamBodyReader");
10
+ Object.defineProperty(exports, "StreamBodyReader", { enumerable: true, get: function () { return StreamBodyReader_1.StreamBodyReader; } });
11
+ var PreConsumedBodyReader_1 = require("./body/PreConsumedBodyReader");
12
+ Object.defineProperty(exports, "PreConsumedBodyReader", { enumerable: true, get: function () { return PreConsumedBodyReader_1.PreConsumedBodyReader; } });
9
13
  // LogApiFilter is now a FIXED framework filter auto-installed by WebpiecesRouter (outermost, at
10
14
  // priority 1,000,000, above AuthFilter). Apps must NOT install it themselves — it is no longer
11
15
  // exported here. Remove any `router.addFilter(new FilterDefinition(1800, LogApiFilter, '*'))`.
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,yFAAyF;AACzF,mEAAkE;AAAzD,gIAAA,sBAAsB,OAAA;AAC/B,6DAA4D;AAAnD,0HAAA,mBAAmB,OAAA;AAC5B,gGAAgG;AAChG,+FAA+F;AAC/F,+FAA+F;AAC/F,6DAA4D;AAAnD,kHAAA,eAAe,OAAA;AAExB,4FAA4F;AAC5F,oGAAoG;AACpG,qEAAqE;AACrE,2DAA0D;AAAjD,wHAAA,kBAAkB,OAAA;AAE3B,+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,wDAYiC;AAX7B,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,mHAAA,mBAAmB,OAAA;AACnB,6GAAA,aAAa,OAAA","sourcesContent":["// Express adapter (the only place express lifecycle lives) over the node-only ApiFactory\nexport { WebpiecesExpressRouter } from './WebpiecesExpressRouter';\nexport { WebpiecesMiddleware } from './WebpiecesMiddleware';\n// LogApiFilter is now a FIXED framework filter auto-installed by WebpiecesRouter (outermost, at\n// priority 1,000,000, above AuthFilter). Apps must NOT install it themselves — it is no longer\n// exported here. Remove any `router.addFilter(new FilterDefinition(1800, LogApiFilter, '*'))`.\nexport { RecordingFilter } from './filters/RecordingFilter';\n\n// The webpieces DEFAULT error response, exported as a DELEGABLE object: it returns the same\n// HttpResponseDto an app's ErrorTranslators.toWire returns, so an app wraps it (\"webpieces' answer,\n// plus one header\") rather than copying its status-to-message table.\nexport { ApiErrorHttpMapper } from './ApiErrorHttpMapper';\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 AuthenticatedCaller,\n SharedSecrets,\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
+ {"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;AAI5B,4DAA2D;AAAlD,oHAAA,gBAAgB,OAAA;AACzB,sEAAqE;AAA5D,8HAAA,qBAAqB,OAAA;AAC9B,gGAAgG;AAChG,+FAA+F;AAC/F,+FAA+F;AAC/F,6DAA4D;AAAnD,kHAAA,eAAe,OAAA;AAExB,4FAA4F;AAC5F,oGAAoG;AACpG,qEAAqE;AACrE,2DAA0D;AAAjD,wHAAA,kBAAkB,OAAA;AAE3B,+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,wDAYiC;AAX7B,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,mHAAA,mBAAmB,OAAA;AACnB,6GAAA,aAAa,OAAA","sourcesContent":["// Express adapter (the only place express lifecycle lives) over the node-only ApiFactory\nexport { WebpiecesExpressRouter } from './WebpiecesExpressRouter';\nexport { WebpiecesMiddleware } from './WebpiecesMiddleware';\n// How routes read the body bytes: stream (default) or a host's pre-consumed req.rawBody (issue #937).\n// Chosen via WebpiecesExpressRouter.setBodyReader(...) before bindExpress(app).\nexport { RequestBodyReader } from './body/RequestBodyReader';\nexport { StreamBodyReader } from './body/StreamBodyReader';\nexport { PreConsumedBodyReader } from './body/PreConsumedBodyReader';\n// LogApiFilter is now a FIXED framework filter auto-installed by WebpiecesRouter (outermost, at\n// priority 1,000,000, above AuthFilter). Apps must NOT install it themselves — it is no longer\n// exported here. Remove any `router.addFilter(new FilterDefinition(1800, LogApiFilter, '*'))`.\nexport { RecordingFilter } from './filters/RecordingFilter';\n\n// The webpieces DEFAULT error response, exported as a DELEGABLE object: it returns the same\n// HttpResponseDto an app's ErrorTranslators.toWire returns, so an app wraps it (\"webpieces' answer,\n// plus one header\") rather than copying its status-to-message table.\nexport { ApiErrorHttpMapper } from './ApiErrorHttpMapper';\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 AuthenticatedCaller,\n SharedSecrets,\n} from '@webpieces/http-routing';\n// ExpressRouteHandler now lives in http-server (express adapter), not node-only http-routing\nexport { ExpressRouteHandler } from './WebpiecesMiddleware';\n"]}