@webpieces/http-server 0.4.790 → 0.4.792
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 +20 -12
- package/package.json +5 -5
- package/src/ExpressResponseWriter.d.ts +1 -1
- package/src/ExpressResponseWriter.js +1 -1
- package/src/ExpressResponseWriter.js.map +1 -1
- package/src/ExpressWrapper.d.ts +27 -30
- package/src/ExpressWrapper.js +42 -41
- package/src/ExpressWrapper.js.map +1 -1
- package/src/StreamExpressWrapper.d.ts +1 -4
- package/src/StreamExpressWrapper.js +2 -4
- package/src/StreamExpressWrapper.js.map +1 -1
- package/src/WebpiecesExpressRouter.d.ts +1 -17
- package/src/WebpiecesExpressRouter.js +3 -24
- package/src/WebpiecesExpressRouter.js.map +1 -1
- package/src/WebpiecesMiddleware.d.ts +3 -6
- package/src/WebpiecesMiddleware.js +5 -7
- package/src/WebpiecesMiddleware.js.map +1 -1
- package/src/index.d.ts +0 -2
- package/src/index.js +3 -4
- package/src/index.js.map +1 -1
- package/src/ApiErrorHttpMapper.d.ts +0 -49
- package/src/ApiErrorHttpMapper.js +0 -73
- package/src/ApiErrorHttpMapper.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamExpressWrapper.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/StreamExpressWrapper.ts"],"names":[],"mappings":"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","sourcesContent":["import { once } from 'node:events';\nimport { NextFunction, Request, Response } from 'express';\nimport {\n ApiBadRequestError,\n ApiErrorCodec,\n DtoValue,\n RequestStream,\n ResponseStream,\n RouteMetadata,\n StreamCorrelation,\n StreamEnvelope,\n StreamEventValidator,\n StreamTransportError,\n StreamWriter,\n toError,\n} from '@webpieces/core-util';\nimport { HttpRequest, RequestContext, RequestContextHeaders } from '@webpieces/core-context';\nimport { ExpressWrapper } from './ExpressWrapper';\nimport { EndUserStatus } from './ApiErrorHttpMapper';\n\nconst KEEP_ALIVE_MILLIS = 15_000;\nconst MAX_LINE_BYTES = 1024 * 1024;\nconst MAX_HANDSHAKE_BUFFER_BYTES = 64 * 1024;\n\n/** Owns ordered SSE writes and the small pre-handshake response buffer. */\nclass SseResponseChannel {\n private opened = false;\n private ended = false;\n private queuedBytes = 0;\n private readonly queued: string[] = [];\n private finish: () => void = () => undefined;\n private pendingWrite: Promise<void> = Promise.resolve();\n readonly finished = new Promise<void>((resolve: () => void): void => {\n this.finish = resolve;\n });\n readonly writer: StreamWriter<DtoValue>;\n\n constructor(\n private readonly res: Response,\n route: RouteMetadata,\n validator: StreamEventValidator,\n ) {\n this.writer = new StreamWriter<DtoValue>(\n (envelope: StreamEnvelope<DtoValue>): Promise<void> => this.deliver(envelope),\n (value: DtoValue): void =>\n validator.validate(route.streaming!.responseEventClass, value, 'response'),\n route.streaming?.supportsNonTerminalFailures,\n );\n }\n\n isEnded(): boolean {\n return this.ended;\n }\n\n async open(): Promise<void> {\n this.res.status(200);\n this.res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n this.res.setHeader('Cache-Control', 'no-cache, no-transform');\n this.res.setHeader('Connection', 'keep-alive');\n this.res.setHeader('X-Accel-Buffering', 'no');\n this.res.flushHeaders();\n this.opened = true;\n for (const frame of this.queued) await this.enqueueWrite(frame);\n this.queued.length = 0;\n if (this.ended) this.res.end();\n }\n\n async keepAlive(): Promise<void> {\n if (!this.ended) await this.enqueueWrite(': keepalive\\n\\n');\n }\n\n // webpieces-disable no-any-unknown -- cancellation reasons are transport-neutral\n async cancel(reason?: unknown): Promise<void> {\n await this.writer.cancel(reason);\n this.finish();\n }\n\n private async deliver(envelope: StreamEnvelope<DtoValue>): Promise<void> {\n const frame = `event: message\\ndata: ${JSON.stringify(envelope)}\\n\\n`;\n if (this.opened) {\n await this.enqueueWrite(frame);\n } else {\n this.queuedBytes += Buffer.byteLength(frame);\n if (this.queuedBytes > MAX_HANDSHAKE_BUFFER_BYTES) {\n throw new StreamTransportError(\n `Streaming controller wrote more than ${MAX_HANDSHAKE_BUFFER_BYTES} bytes before its request writer was ready.`,\n );\n }\n this.queued.push(frame);\n }\n if (envelope.kind === 'complete' || (envelope.kind === 'failure' && envelope.terminal)) {\n this.ended = true;\n if (this.opened) this.res.end();\n this.finish();\n }\n }\n\n private enqueueWrite(data: string): Promise<void> {\n const delivery = this.pendingWrite.then((): Promise<void> => this.write(data));\n this.pendingWrite = delivery.catch((): void => undefined);\n return delivery;\n }\n\n private async write(data: string): Promise<void> {\n if (this.res.destroyed || this.res.writableEnded) {\n throw new StreamTransportError('Cannot write to a closed streaming HTTP response.');\n }\n if (this.res.write(data)) return;\n await Promise.race([\n once(this.res, 'drain').then((): void => undefined),\n once(this.res, 'close').then((): never => {\n throw new StreamTransportError('Streaming HTTP peer disconnected during write.');\n }),\n once(this.res, 'error').then((args): never => {\n throw new StreamTransportError(\n 'Streaming HTTP response failed.',\n undefined,\n undefined,\n toError(args[0]),\n );\n }),\n ]);\n }\n}\n\n/** Express adapter for a `@WpStream` request-NDJSON / response-SSE contract. */\nexport class StreamExpressWrapper {\n private readonly ordinaryErrors: ExpressWrapper;\n private readonly validator = new StreamEventValidator();\n\n constructor(\n // webpieces-disable no-any-unknown -- stream contract types are erased at the express boundary\n private readonly clientMethod: (...args: unknown[]) => Promise<unknown>,\n private readonly route: RouteMetadata,\n private readonly headers: RequestContextHeaders,\n /** How a handshake-time `ApiEndUserError` is answered; see {@link EndUserStatus}. */\n endUserStatus: EndUserStatus = 'gui',\n ) {\n this.ordinaryErrors = new ExpressWrapper(\n clientMethod,\n route.path,\n headers,\n false,\n false,\n undefined,\n route,\n undefined,\n endUserStatus,\n );\n }\n\n async execute(req: Request, res: Response, next: NextFunction): Promise<void> {\n void next;\n await RequestContext.run(async () => this.executeInContext(req, res));\n }\n\n private async executeInContext(req: Request, res: Response): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- owning streaming HTTP error boundary; handshake failures still use ordinary status/error mapping\n try {\n this.headers.fillFromRequest(this.toWebpiecesRequest(req));\n this.requireNdjson(req);\n const outbound = new SseResponseChannel(res, this.route, this.validator);\n const candidate = await this.clientMethod(outbound.writer as ResponseStream<DtoValue>);\n if (!this.isRequestWriter(candidate)) {\n throw new StreamTransportError(\n `${this.route.apiName ?? 'API'}.${this.route.methodName} did not return a RequestStream.`,\n );\n }\n const inbound = candidate;\n await outbound.open();\n if (outbound.isEnded()) {\n await inbound.cancel(new StreamTransportError('Response stream completed.'));\n return;\n }\n await this.runOpenStream(req, res, inbound, outbound);\n } catch (err: unknown) {\n const error = toError(err);\n this.ordinaryErrors.handleError(res, error);\n }\n }\n\n private async runOpenStream(\n req: Request,\n res: Response,\n inbound: RequestStream<DtoValue>,\n outbound: SseResponseChannel,\n ): Promise<void> {\n let disconnected = false;\n const cancel = async (): Promise<void> => {\n if (disconnected || outbound.isEnded()) return;\n disconnected = true;\n const error = new StreamTransportError('Streaming HTTP peer disconnected.');\n await inbound.cancel(error);\n await outbound.cancel(error);\n };\n req.once('aborted', (): void => void cancel());\n res.once('close', (): void => void cancel());\n const keepAlive = setInterval((): void => {\n if (!disconnected) void outbound.keepAlive().catch(() => cancel());\n }, KEEP_ALIVE_MILLIS);\n keepAlive.unref();\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- after SSE opens, wire failures must become an in-band typed terminal event\n try {\n const inboundFinished = this.consumeNdjson(req, inbound);\n const responseFinishedFirst = await Promise.race([\n inboundFinished.then((): boolean => false),\n outbound.finished.then((): boolean => true),\n ]);\n if (responseFinishedFirst) {\n if (!disconnected) {\n await inbound.cancel(new StreamTransportError('Response stream completed.'));\n }\n req.destroy();\n await inboundFinished.catch((): void => undefined);\n return;\n }\n await outbound.finished;\n } catch (err: unknown) {\n const error = toError(err);\n const streamError =\n err instanceof StreamTransportError\n ? err\n : new StreamTransportError(\n 'Streaming HTTP request failed.',\n undefined,\n undefined,\n error,\n );\n if (!disconnected) {\n if (!outbound.isEnded()) await outbound.writer.fail(streamError);\n await inbound.fail(streamError);\n }\n } finally {\n clearInterval(keepAlive);\n }\n }\n\n private async consumeNdjson(req: Request, writer: RequestStream<DtoValue>): Promise<void> {\n let pending = Buffer.alloc(0);\n for await (const raw of req) {\n const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);\n pending = Buffer.concat([pending, chunk]);\n if (pending.length > MAX_LINE_BYTES && pending.indexOf(10) < 0) {\n throw new StreamTransportError(`NDJSON frame exceeds ${MAX_LINE_BYTES} bytes.`);\n }\n let newline = pending.indexOf(10);\n while (newline >= 0) {\n const line = pending.subarray(0, newline).toString('utf8').trim();\n pending = pending.subarray(newline + 1);\n if (line !== '') await this.dispatchLine(line, writer);\n newline = pending.indexOf(10);\n }\n }\n const finalLine = pending.toString('utf8').trim();\n if (finalLine !== '') await this.dispatchLine(finalLine, writer);\n }\n\n private async dispatchLine(line: string, writer: RequestStream<DtoValue>): Promise<void> {\n let value: DtoValue;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted NDJSON boundary and is translated to StreamTransportError\n try {\n value = JSON.parse(line) as DtoValue;\n } catch (err: unknown) {\n const error = toError(err);\n throw new StreamTransportError(\n 'Malformed NDJSON stream frame.',\n undefined,\n undefined,\n error,\n );\n }\n const envelope = this.parseEnvelope(value);\n if (envelope.kind === 'event') {\n this.validator.validate(\n this.route.streaming!.requestEventClass,\n envelope.value,\n 'request',\n );\n await writer.event(envelope.value!, envelope.correlation);\n } else if (envelope.kind === 'failure') {\n await writer.fail(ApiErrorCodec.decode(envelope.error), envelope.correlation, {\n terminal: envelope.terminal,\n });\n } else {\n await writer.complete();\n }\n }\n\n private parseEnvelope(value: DtoValue): StreamEnvelope<DtoValue> {\n if (typeof value !== 'object' || value === null) {\n throw new StreamTransportError('NDJSON stream frame must be an object.');\n }\n const record = value as Record<string, DtoValue | undefined>;\n const kind = record['kind'];\n const correlation = this.parseCorrelation(record['correlation']);\n if (kind === 'event')\n return new StreamEnvelope('event', record['value'], undefined, correlation);\n if (kind === 'complete') {\n return new StreamEnvelope<DtoValue>('complete', undefined, undefined, correlation);\n }\n if (kind === 'failure' && ApiErrorCodec.isPayload(record['error'])) {\n const terminal = record['terminal'];\n if (terminal !== undefined && typeof terminal !== 'boolean') {\n throw new StreamTransportError('NDJSON failure terminal flag must be boolean.');\n }\n const error = ApiErrorCodec.encode(ApiErrorCodec.decode(record['error']));\n return new StreamEnvelope<DtoValue>('failure', undefined, error, correlation, terminal);\n }\n throw new StreamTransportError(\n `Unknown or malformed NDJSON stream frame kind '${String(kind)}'.`,\n );\n }\n\n private parseCorrelation(value: DtoValue | undefined): StreamCorrelation | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null) {\n throw new StreamTransportError('Stream correlation must be an object.');\n }\n const record = value as Record<string, DtoValue | undefined>;\n const key = record['key'];\n const requestId = record['requestId'];\n if (\n (typeof key !== 'string' && typeof key !== 'number') ||\n (requestId !== undefined && typeof requestId !== 'string')\n ) {\n throw new StreamTransportError('Stream correlation has an invalid key or requestId.');\n }\n return new StreamCorrelation(key, requestId);\n }\n\n private requireNdjson(req: Request): void {\n const contentType = req.get('content-type')?.split(';')[0]?.trim().toLowerCase();\n if (contentType !== 'application/x-ndjson') {\n throw new ApiBadRequestError(\n 'Streaming endpoints require Content-Type application/x-ndjson.',\n );\n }\n }\n\n // webpieces-disable no-any-unknown -- runtime guard narrows an erased API return value\n private isRequestWriter(value: unknown): value is RequestStream<DtoValue> {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Record<string, DtoValue | undefined>;\n return ['event', 'fail', 'complete', 'cancel'].every(\n (name: string) => typeof candidate[name] === 'function',\n );\n }\n\n private toWebpiecesRequest(req: Request): HttpRequest {\n const headers = new Map<string, string[]>();\n for (const [name, value] of Object.entries(req.headers)) {\n if (typeof value === 'string') headers.set(name.toLowerCase(), [value]);\n else if (Array.isArray(value)) headers.set(name.toLowerCase(), value);\n }\n return new HttpRequest(req.method, req.path ?? this.route.path, headers);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"StreamExpressWrapper.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/StreamExpressWrapper.ts"],"names":[],"mappings":"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","sourcesContent":["import { once } from 'node:events';\nimport { NextFunction, Request, Response } from 'express';\nimport {\n ApiBadRequestError,\n ApiErrorCodec,\n DtoValue,\n RequestStream,\n ResponseStream,\n RouteMetadata,\n StreamCorrelation,\n StreamEnvelope,\n StreamEventValidator,\n StreamTransportError,\n StreamWriter,\n toError,\n} from '@webpieces/core-util';\nimport { HttpRequest, RequestContext, RequestContextHeaders } from '@webpieces/core-context';\nimport { ExpressWrapper } from './ExpressWrapper';\n\nconst KEEP_ALIVE_MILLIS = 15_000;\nconst MAX_LINE_BYTES = 1024 * 1024;\nconst MAX_HANDSHAKE_BUFFER_BYTES = 64 * 1024;\n\n/** Owns ordered SSE writes and the small pre-handshake response buffer. */\nclass SseResponseChannel {\n private opened = false;\n private ended = false;\n private queuedBytes = 0;\n private readonly queued: string[] = [];\n private finish: () => void = () => undefined;\n private pendingWrite: Promise<void> = Promise.resolve();\n readonly finished = new Promise<void>((resolve: () => void): void => {\n this.finish = resolve;\n });\n readonly writer: StreamWriter<DtoValue>;\n\n constructor(\n private readonly res: Response,\n route: RouteMetadata,\n validator: StreamEventValidator,\n ) {\n this.writer = new StreamWriter<DtoValue>(\n (envelope: StreamEnvelope<DtoValue>): Promise<void> => this.deliver(envelope),\n (value: DtoValue): void =>\n validator.validate(route.streaming!.responseEventClass, value, 'response'),\n route.streaming?.supportsNonTerminalFailures,\n );\n }\n\n isEnded(): boolean {\n return this.ended;\n }\n\n async open(): Promise<void> {\n this.res.status(200);\n this.res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n this.res.setHeader('Cache-Control', 'no-cache, no-transform');\n this.res.setHeader('Connection', 'keep-alive');\n this.res.setHeader('X-Accel-Buffering', 'no');\n this.res.flushHeaders();\n this.opened = true;\n for (const frame of this.queued) await this.enqueueWrite(frame);\n this.queued.length = 0;\n if (this.ended) this.res.end();\n }\n\n async keepAlive(): Promise<void> {\n if (!this.ended) await this.enqueueWrite(': keepalive\\n\\n');\n }\n\n // webpieces-disable no-any-unknown -- cancellation reasons are transport-neutral\n async cancel(reason?: unknown): Promise<void> {\n await this.writer.cancel(reason);\n this.finish();\n }\n\n private async deliver(envelope: StreamEnvelope<DtoValue>): Promise<void> {\n const frame = `event: message\\ndata: ${JSON.stringify(envelope)}\\n\\n`;\n if (this.opened) {\n await this.enqueueWrite(frame);\n } else {\n this.queuedBytes += Buffer.byteLength(frame);\n if (this.queuedBytes > MAX_HANDSHAKE_BUFFER_BYTES) {\n throw new StreamTransportError(\n `Streaming controller wrote more than ${MAX_HANDSHAKE_BUFFER_BYTES} bytes before its request writer was ready.`,\n );\n }\n this.queued.push(frame);\n }\n if (envelope.kind === 'complete' || (envelope.kind === 'failure' && envelope.terminal)) {\n this.ended = true;\n if (this.opened) this.res.end();\n this.finish();\n }\n }\n\n private enqueueWrite(data: string): Promise<void> {\n const delivery = this.pendingWrite.then((): Promise<void> => this.write(data));\n this.pendingWrite = delivery.catch((): void => undefined);\n return delivery;\n }\n\n private async write(data: string): Promise<void> {\n if (this.res.destroyed || this.res.writableEnded) {\n throw new StreamTransportError('Cannot write to a closed streaming HTTP response.');\n }\n if (this.res.write(data)) return;\n await Promise.race([\n once(this.res, 'drain').then((): void => undefined),\n once(this.res, 'close').then((): never => {\n throw new StreamTransportError('Streaming HTTP peer disconnected during write.');\n }),\n once(this.res, 'error').then((args): never => {\n throw new StreamTransportError(\n 'Streaming HTTP response failed.',\n undefined,\n undefined,\n toError(args[0]),\n );\n }),\n ]);\n }\n}\n\n/** Express adapter for a `@WpStream` request-NDJSON / response-SSE contract. */\nexport class StreamExpressWrapper {\n private readonly ordinaryErrors: ExpressWrapper;\n private readonly validator = new StreamEventValidator();\n\n constructor(\n // webpieces-disable no-any-unknown -- stream contract types are erased at the express boundary\n private readonly clientMethod: (...args: unknown[]) => Promise<unknown>,\n private readonly route: RouteMetadata,\n private readonly headers: RequestContextHeaders,\n ) {\n this.ordinaryErrors = new ExpressWrapper(\n clientMethod,\n route.path,\n headers,\n false,\n false,\n undefined,\n route,\n );\n }\n\n async execute(req: Request, res: Response, next: NextFunction): Promise<void> {\n void next;\n await RequestContext.run(async () => this.executeInContext(req, res));\n }\n\n private async executeInContext(req: Request, res: Response): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- owning streaming HTTP error boundary; handshake failures still use ordinary status/error mapping\n try {\n this.headers.fillFromRequest(this.toWebpiecesRequest(req));\n this.requireNdjson(req);\n const outbound = new SseResponseChannel(res, this.route, this.validator);\n const candidate = await this.clientMethod(outbound.writer as ResponseStream<DtoValue>);\n if (!this.isRequestWriter(candidate)) {\n throw new StreamTransportError(\n `${this.route.apiName ?? 'API'}.${this.route.methodName} did not return a RequestStream.`,\n );\n }\n const inbound = candidate;\n await outbound.open();\n if (outbound.isEnded()) {\n await inbound.cancel(new StreamTransportError('Response stream completed.'));\n return;\n }\n await this.runOpenStream(req, res, inbound, outbound);\n } catch (err: unknown) {\n const error = toError(err);\n this.ordinaryErrors.handleError(res, error);\n }\n }\n\n private async runOpenStream(\n req: Request,\n res: Response,\n inbound: RequestStream<DtoValue>,\n outbound: SseResponseChannel,\n ): Promise<void> {\n let disconnected = false;\n const cancel = async (): Promise<void> => {\n if (disconnected || outbound.isEnded()) return;\n disconnected = true;\n const error = new StreamTransportError('Streaming HTTP peer disconnected.');\n await inbound.cancel(error);\n await outbound.cancel(error);\n };\n req.once('aborted', (): void => void cancel());\n res.once('close', (): void => void cancel());\n const keepAlive = setInterval((): void => {\n if (!disconnected) void outbound.keepAlive().catch(() => cancel());\n }, KEEP_ALIVE_MILLIS);\n keepAlive.unref();\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- after SSE opens, wire failures must become an in-band typed terminal event\n try {\n const inboundFinished = this.consumeNdjson(req, inbound);\n const responseFinishedFirst = await Promise.race([\n inboundFinished.then((): boolean => false),\n outbound.finished.then((): boolean => true),\n ]);\n if (responseFinishedFirst) {\n if (!disconnected) {\n await inbound.cancel(new StreamTransportError('Response stream completed.'));\n }\n req.destroy();\n await inboundFinished.catch((): void => undefined);\n return;\n }\n await outbound.finished;\n } catch (err: unknown) {\n const error = toError(err);\n const streamError =\n err instanceof StreamTransportError\n ? err\n : new StreamTransportError(\n 'Streaming HTTP request failed.',\n undefined,\n undefined,\n error,\n );\n if (!disconnected) {\n if (!outbound.isEnded()) await outbound.writer.fail(streamError);\n await inbound.fail(streamError);\n }\n } finally {\n clearInterval(keepAlive);\n }\n }\n\n private async consumeNdjson(req: Request, writer: RequestStream<DtoValue>): Promise<void> {\n let pending = Buffer.alloc(0);\n for await (const raw of req) {\n const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);\n pending = Buffer.concat([pending, chunk]);\n if (pending.length > MAX_LINE_BYTES && pending.indexOf(10) < 0) {\n throw new StreamTransportError(`NDJSON frame exceeds ${MAX_LINE_BYTES} bytes.`);\n }\n let newline = pending.indexOf(10);\n while (newline >= 0) {\n const line = pending.subarray(0, newline).toString('utf8').trim();\n pending = pending.subarray(newline + 1);\n if (line !== '') await this.dispatchLine(line, writer);\n newline = pending.indexOf(10);\n }\n }\n const finalLine = pending.toString('utf8').trim();\n if (finalLine !== '') await this.dispatchLine(finalLine, writer);\n }\n\n private async dispatchLine(line: string, writer: RequestStream<DtoValue>): Promise<void> {\n let value: DtoValue;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- JSON.parse is the untrusted NDJSON boundary and is translated to StreamTransportError\n try {\n value = JSON.parse(line) as DtoValue;\n } catch (err: unknown) {\n const error = toError(err);\n throw new StreamTransportError(\n 'Malformed NDJSON stream frame.',\n undefined,\n undefined,\n error,\n );\n }\n const envelope = this.parseEnvelope(value);\n if (envelope.kind === 'event') {\n this.validator.validate(\n this.route.streaming!.requestEventClass,\n envelope.value,\n 'request',\n );\n await writer.event(envelope.value!, envelope.correlation);\n } else if (envelope.kind === 'failure') {\n await writer.fail(ApiErrorCodec.decode(envelope.error), envelope.correlation, {\n terminal: envelope.terminal,\n });\n } else {\n await writer.complete();\n }\n }\n\n private parseEnvelope(value: DtoValue): StreamEnvelope<DtoValue> {\n if (typeof value !== 'object' || value === null) {\n throw new StreamTransportError('NDJSON stream frame must be an object.');\n }\n const record = value as Record<string, DtoValue | undefined>;\n const kind = record['kind'];\n const correlation = this.parseCorrelation(record['correlation']);\n if (kind === 'event')\n return new StreamEnvelope('event', record['value'], undefined, correlation);\n if (kind === 'complete') {\n return new StreamEnvelope<DtoValue>('complete', undefined, undefined, correlation);\n }\n if (kind === 'failure' && ApiErrorCodec.isPayload(record['error'])) {\n const terminal = record['terminal'];\n if (terminal !== undefined && typeof terminal !== 'boolean') {\n throw new StreamTransportError('NDJSON failure terminal flag must be boolean.');\n }\n const error = ApiErrorCodec.encode(ApiErrorCodec.decode(record['error']));\n return new StreamEnvelope<DtoValue>('failure', undefined, error, correlation, terminal);\n }\n throw new StreamTransportError(\n `Unknown or malformed NDJSON stream frame kind '${String(kind)}'.`,\n );\n }\n\n private parseCorrelation(value: DtoValue | undefined): StreamCorrelation | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null) {\n throw new StreamTransportError('Stream correlation must be an object.');\n }\n const record = value as Record<string, DtoValue | undefined>;\n const key = record['key'];\n const requestId = record['requestId'];\n if (\n (typeof key !== 'string' && typeof key !== 'number') ||\n (requestId !== undefined && typeof requestId !== 'string')\n ) {\n throw new StreamTransportError('Stream correlation has an invalid key or requestId.');\n }\n return new StreamCorrelation(key, requestId);\n }\n\n private requireNdjson(req: Request): void {\n const contentType = req.get('content-type')?.split(';')[0]?.trim().toLowerCase();\n if (contentType !== 'application/x-ndjson') {\n throw new ApiBadRequestError(\n 'Streaming endpoints require Content-Type application/x-ndjson.',\n );\n }\n }\n\n // webpieces-disable no-any-unknown -- runtime guard narrows an erased API return value\n private isRequestWriter(value: unknown): value is RequestStream<DtoValue> {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Record<string, DtoValue | undefined>;\n return ['event', 'fail', 'complete', 'cancel'].every(\n (name: string) => typeof candidate[name] === 'function',\n );\n }\n\n private toWebpiecesRequest(req: Request): HttpRequest {\n const headers = new Map<string, string[]>();\n for (const [name, value] of Object.entries(req.headers)) {\n if (typeof value === 'string') headers.set(name.toLowerCase(), [value]);\n else if (Array.isArray(value)) headers.set(name.toLowerCase(), value);\n }\n return new HttpRequest(req.method, req.path ?? this.route.path, headers);\n }\n}\n"]}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Express } from 'express';
|
|
2
2
|
import { ApiFactory, WebpiecesConfig } from '@webpieces/http-routing';
|
|
3
3
|
import { RequestBodyReader } from './body/RequestBodyReader';
|
|
4
|
-
import { EndUserStatus } from './ApiErrorHttpMapper';
|
|
5
4
|
/** The value returned by express `app.listen(...)` (a node http.Server). */
|
|
6
5
|
type HttpServer = ReturnType<Express['listen']>;
|
|
7
6
|
/**
|
|
@@ -30,9 +29,7 @@ export declare class WebpiecesExpressRouter {
|
|
|
30
29
|
private readonly middleware;
|
|
31
30
|
/** Where every mounted route reads its body bytes from. See {@link setBodyReader}. */
|
|
32
31
|
private bodyReader;
|
|
33
|
-
/**
|
|
34
|
-
private endUserStatus;
|
|
35
|
-
/** Set by {@link bindExpress}; a reader or end-user status chosen after that would reach no route. */
|
|
32
|
+
/** Set by {@link bindExpress}; a body reader chosen after that would reach no route. */
|
|
36
33
|
private bound;
|
|
37
34
|
constructor(apiFactory: ApiFactory);
|
|
38
35
|
/**
|
|
@@ -48,19 +45,6 @@ export declare class WebpiecesExpressRouter {
|
|
|
48
45
|
* silently keep the old reader.
|
|
49
46
|
*/
|
|
50
47
|
setBodyReader(bodyReader: RequestBodyReader): void;
|
|
51
|
-
/**
|
|
52
|
-
* Choose how routes answer an `ApiEndUserError`. Call it BEFORE {@link bindExpress}: each route
|
|
53
|
-
* captures the choice when it is mounted.
|
|
54
|
-
*
|
|
55
|
-
* The default, `'gui'`, answers 266 — right for a GUI backend and for every internal
|
|
56
|
-
* server-to-server hop. A partner/public REST edge whose contract promises 400/404/409/422 calls
|
|
57
|
-
* `router.setEndUserStatus('edge')`, and the response then carries the thrower's
|
|
58
|
-
* `ApiEndUserError.edgeHttpStatus` (400 when it set none). See {@link EndUserStatus}.
|
|
59
|
-
*
|
|
60
|
-
* @throws Error when called after {@link bindExpress}, because the already-mounted routes would
|
|
61
|
-
* silently keep the old choice.
|
|
62
|
-
*/
|
|
63
|
-
setEndUserStatus(endUserStatus: EndUserStatus): void;
|
|
64
48
|
/**
|
|
65
49
|
* Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,
|
|
66
50
|
* express-tier + api-tier filter chain, error→JSON) onto the caller's express app.
|
|
@@ -31,9 +31,7 @@ class WebpiecesExpressRouter {
|
|
|
31
31
|
middleware = new WebpiecesMiddleware_1.WebpiecesMiddleware();
|
|
32
32
|
/** Where every mounted route reads its body bytes from. See {@link setBodyReader}. */
|
|
33
33
|
bodyReader = new StreamBodyReader_1.StreamBodyReader();
|
|
34
|
-
/**
|
|
35
|
-
endUserStatus = 'gui';
|
|
36
|
-
/** Set by {@link bindExpress}; a reader or end-user status chosen after that would reach no route. */
|
|
34
|
+
/** Set by {@link bindExpress}; a body reader chosen after that would reach no route. */
|
|
37
35
|
bound = false;
|
|
38
36
|
constructor(apiFactory) {
|
|
39
37
|
this.apiFactory = apiFactory;
|
|
@@ -57,25 +55,6 @@ class WebpiecesExpressRouter {
|
|
|
57
55
|
}
|
|
58
56
|
this.bodyReader = bodyReader;
|
|
59
57
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Choose how routes answer an `ApiEndUserError`. Call it BEFORE {@link bindExpress}: each route
|
|
62
|
-
* captures the choice when it is mounted.
|
|
63
|
-
*
|
|
64
|
-
* The default, `'gui'`, answers 266 — right for a GUI backend and for every internal
|
|
65
|
-
* server-to-server hop. A partner/public REST edge whose contract promises 400/404/409/422 calls
|
|
66
|
-
* `router.setEndUserStatus('edge')`, and the response then carries the thrower's
|
|
67
|
-
* `ApiEndUserError.edgeHttpStatus` (400 when it set none). See {@link EndUserStatus}.
|
|
68
|
-
*
|
|
69
|
-
* @throws Error when called after {@link bindExpress}, because the already-mounted routes would
|
|
70
|
-
* silently keep the old choice.
|
|
71
|
-
*/
|
|
72
|
-
setEndUserStatus(endUserStatus) {
|
|
73
|
-
if (this.bound) {
|
|
74
|
-
throw new Error('setEndUserStatus() was called after bindExpress(); the mounted routes already ' +
|
|
75
|
-
'captured their end-user status. Call setEndUserStatus() before bindExpress(app).');
|
|
76
|
-
}
|
|
77
|
-
this.endUserStatus = endUserStatus;
|
|
78
|
-
}
|
|
79
58
|
/**
|
|
80
59
|
* Mount the webpieces routes (each fully self-contained: own body parse, RequestContext,
|
|
81
60
|
* express-tier + api-tier filter chain, error→JSON) onto the caller's express app.
|
|
@@ -165,8 +144,8 @@ class WebpiecesExpressRouter {
|
|
|
165
144
|
// The parser is chosen by the @Endpoint annotation, not the request Content-Type — and
|
|
166
145
|
// so is whether the verbatim bytes survive the parse for an @WpAuthWebhook hook to verify.
|
|
167
146
|
const wrapper = route.streaming
|
|
168
|
-
? this.middleware.createStreamExpressWrapper(apiClient.client[route.methodName], route
|
|
169
|
-
: this.middleware.createExpressWrapper(apiClient.client[route.methodName], route, this.bodyReader
|
|
147
|
+
? this.middleware.createStreamExpressWrapper(apiClient.client[route.methodName], route)
|
|
148
|
+
: this.middleware.createExpressWrapper(apiClient.client[route.methodName], route, this.bodyReader);
|
|
170
149
|
this.registerHandler(app, route.httpMethod, path, wrapper.execute.bind(wrapper));
|
|
171
150
|
count++;
|
|
172
151
|
}
|
|
@@ -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';\nimport { EndUserStatus } from './ApiErrorHttpMapper';\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 /** How every mounted route answers an `ApiEndUserError`. See {@link setEndUserStatus}. */\n private endUserStatus: EndUserStatus = 'gui';\n /** Set by {@link bindExpress}; a reader or end-user status 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 * Choose how routes answer an `ApiEndUserError`. Call it BEFORE {@link bindExpress}: each route\n * captures the choice when it is mounted.\n *\n * The default, `'gui'`, answers 266 — right for a GUI backend and for every internal\n * server-to-server hop. A partner/public REST edge whose contract promises 400/404/409/422 calls\n * `router.setEndUserStatus('edge')`, and the response then carries the thrower's\n * `ApiEndUserError.edgeHttpStatus` (400 when it set none). See {@link EndUserStatus}.\n *\n * @throws Error when called after {@link bindExpress}, because the already-mounted routes would\n * silently keep the old choice.\n */\n setEndUserStatus(endUserStatus: EndUserStatus): void {\n if (this.bound) {\n throw new Error(\n 'setEndUserStatus() was called after bindExpress(); the mounted routes already ' +\n 'captured their end-user status. Call setEndUserStatus() before bindExpress(app).',\n );\n }\n this.endUserStatus = endUserStatus;\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 this.endUserStatus,\n )\n : this.middleware.createExpressWrapper(\n apiClient.client[route.methodName],\n route,\n this.bodyReader,\n this.endUserStatus,\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('')`, 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 @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('')`, 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"]}
|
|
@@ -4,7 +4,6 @@ import { RouteMetadata } from '@webpieces/core-util';
|
|
|
4
4
|
import { ExpressWrapper } from './ExpressWrapper';
|
|
5
5
|
import { StreamExpressWrapper } from './StreamExpressWrapper';
|
|
6
6
|
import { RequestBodyReader } from './body/RequestBodyReader';
|
|
7
|
-
import { EndUserStatus } from './ApiErrorHttpMapper';
|
|
8
7
|
/**
|
|
9
8
|
* Express route handler function type. Lives in http-server (the express adapter),
|
|
10
9
|
* NOT in the node-only http-routing package, so http-routing stays express-free.
|
|
@@ -55,7 +54,7 @@ export declare class WebpiecesMiddleware {
|
|
|
55
54
|
* (body parsing, unmatched paths, a bug in the wrapper itself).
|
|
56
55
|
*
|
|
57
56
|
* The page carries NO `error.message`. It used to render one into a `<pre>` block, which is the
|
|
58
|
-
* same leak `
|
|
57
|
+
* same leak `WebpiecesDefaultErrorTranslator` closes on the JSON side and a worse one here: the errors that
|
|
59
58
|
* reach THIS handler are the unhandled ones, whose messages are stack-adjacent internals nobody
|
|
60
59
|
* wrote for a caller to read. The message is logged one line above, which is where it belongs.
|
|
61
60
|
*/
|
|
@@ -121,10 +120,8 @@ export declare class WebpiecesMiddleware {
|
|
|
121
120
|
* @param bodyReader - Where the body bytes come from (the router's choice, see
|
|
122
121
|
* `WebpiecesExpressRouter.setBodyReader`). Required so the router's choice can never be
|
|
123
122
|
* silently dropped on the way to the wrapper.
|
|
124
|
-
* @param endUserStatus - How an `ApiEndUserError` is answered (the router's choice, see
|
|
125
|
-
* `WebpiecesExpressRouter.setEndUserStatus`). Required for the same reason as `bodyReader`.
|
|
126
123
|
* @returns ExpressWrapper instance
|
|
127
124
|
*/
|
|
128
|
-
createExpressWrapper(clientMethod: (...args: unknown[]) => Promise<unknown>, route: RouteMetadata, bodyReader: RequestBodyReader
|
|
129
|
-
createStreamExpressWrapper(clientMethod: (...args: unknown[]) => Promise<unknown>, route: RouteMetadata
|
|
125
|
+
createExpressWrapper(clientMethod: (...args: unknown[]) => Promise<unknown>, route: RouteMetadata, bodyReader: RequestBodyReader): ExpressWrapper;
|
|
126
|
+
createStreamExpressWrapper(clientMethod: (...args: unknown[]) => Promise<unknown>, route: RouteMetadata): StreamExpressWrapper;
|
|
130
127
|
}
|
|
@@ -57,7 +57,7 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
|
|
|
57
57
|
* (body parsing, unmatched paths, a bug in the wrapper itself).
|
|
58
58
|
*
|
|
59
59
|
* The page carries NO `error.message`. It used to render one into a `<pre>` block, which is the
|
|
60
|
-
* same leak `
|
|
60
|
+
* same leak `WebpiecesDefaultErrorTranslator` closes on the JSON side and a worse one here: the errors that
|
|
61
61
|
* reach THIS handler are the unhandled ones, whose messages are stack-adjacent internals nobody
|
|
62
62
|
* wrote for a caller to read. The message is logged one line above, which is where it belongs.
|
|
63
63
|
*/
|
|
@@ -200,19 +200,17 @@ let WebpiecesMiddleware = class WebpiecesMiddleware {
|
|
|
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
|
|
202
202
|
* silently dropped on the way to the wrapper.
|
|
203
|
-
* @param endUserStatus - How an `ApiEndUserError` is answered (the router's choice, see
|
|
204
|
-
* `WebpiecesExpressRouter.setEndUserStatus`). Required for the same reason as `bodyReader`.
|
|
205
203
|
* @returns ExpressWrapper instance
|
|
206
204
|
*/
|
|
207
205
|
createExpressWrapper(
|
|
208
206
|
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
209
|
-
clientMethod, route, bodyReader
|
|
210
|
-
return new ExpressWrapper_1.ExpressWrapper(clientMethod, route.path, this.headers, route.formPost, route.rawBody, undefined, route, bodyReader
|
|
207
|
+
clientMethod, route, bodyReader) {
|
|
208
|
+
return new ExpressWrapper_1.ExpressWrapper(clientMethod, route.path, this.headers, route.formPost, route.rawBody, undefined, route, bodyReader);
|
|
211
209
|
}
|
|
212
210
|
createStreamExpressWrapper(
|
|
213
211
|
// webpieces-disable no-any-unknown -- stream DTOs are erased at the routing boundary
|
|
214
|
-
clientMethod, route
|
|
215
|
-
return new StreamExpressWrapper_1.StreamExpressWrapper(clientMethod, route, this.headers
|
|
212
|
+
clientMethod, route) {
|
|
213
|
+
return new StreamExpressWrapper_1.StreamExpressWrapper(clientMethod, route, this.headers);
|
|
216
214
|
}
|
|
217
215
|
};
|
|
218
216
|
exports.WebpiecesMiddleware = WebpiecesMiddleware;
|
|
@@ -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';\nimport { EndUserStatus } from './ApiErrorHttpMapper';\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 * @param endUserStatus - How an `ApiEndUserError` is answered (the router's choice, see\n * `WebpiecesExpressRouter.setEndUserStatus`). Required for the same reason as `bodyReader`.\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 endUserStatus: EndUserStatus,\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 endUserStatus,\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 endUserStatus: EndUserStatus,\n ): StreamExpressWrapper {\n return new StreamExpressWrapper(clientMethod, route, this.headers, endUserStatus);\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 @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"]}
|
package/src/index.d.ts
CHANGED
|
@@ -4,9 +4,7 @@ export { RequestBodyReader } from './body/RequestBodyReader';
|
|
|
4
4
|
export { StreamBodyReader } from './body/StreamBodyReader';
|
|
5
5
|
export { PreConsumedBodyReader } from './body/PreConsumedBodyReader';
|
|
6
6
|
export { RecordingFilter } from './filters/RecordingFilter';
|
|
7
|
-
export { ApiErrorHttpMapper } from './ApiErrorHttpMapper';
|
|
8
7
|
export { ExpressResponseWriter } from './ExpressResponseWriter';
|
|
9
|
-
export type { EndUserStatus } from './ApiErrorHttpMapper';
|
|
10
8
|
export { TestCaseRecorderImpl } from './recorder/TestCaseRecorderImpl';
|
|
11
9
|
export { SpecGenerator } from './recorder/SpecGenerator';
|
|
12
10
|
export { recordable } from './recorder/recordable';
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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.ExpressResponseWriter = exports.
|
|
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.ExpressResponseWriter = 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; } });
|
|
@@ -16,12 +16,11 @@ Object.defineProperty(exports, "PreConsumedBodyReader", { enumerable: true, get:
|
|
|
16
16
|
var RecordingFilter_1 = require("./filters/RecordingFilter");
|
|
17
17
|
Object.defineProperty(exports, "RecordingFilter", { enumerable: true, get: function () { return RecordingFilter_1.RecordingFilter; } });
|
|
18
18
|
// The webpieces DEFAULT error response, exported as a DELEGABLE object: it returns the same
|
|
19
|
-
// HttpResponseDto an app's
|
|
19
|
+
// HttpResponseDto an app's ErrorTranslator.toWire returns, so an app wraps it ("webpieces' answer,
|
|
20
20
|
// plus one header") rather than copying its status-to-message table.
|
|
21
|
-
var ApiErrorHttpMapper_1 = require("./ApiErrorHttpMapper");
|
|
22
|
-
Object.defineProperty(exports, "ApiErrorHttpMapper", { enumerable: true, get: function () { return ApiErrorHttpMapper_1.ApiErrorHttpMapper; } });
|
|
23
21
|
var ExpressResponseWriter_1 = require("./ExpressResponseWriter");
|
|
24
22
|
Object.defineProperty(exports, "ExpressResponseWriter", { enumerable: true, get: function () { return ExpressResponseWriter_1.ExpressResponseWriter; } });
|
|
23
|
+
// How ApiEndUserError is answered: 'gui' (266, default) or 'edge' (its edgeHttpStatus, else 400).
|
|
25
24
|
// Test-case recording (contract lives in @webpieces/core-util)
|
|
26
25
|
var TestCaseRecorderImpl_1 = require("./recorder/TestCaseRecorderImpl");
|
|
27
26
|
Object.defineProperty(exports, "TestCaseRecorderImpl", { enumerable: true, get: function () { return TestCaseRecorderImpl_1.TestCaseRecorderImpl; } });
|
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;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,
|
|
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,mGAAmG;AACnG,qEAAqE;AACrE,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA;AAC9B,kGAAkG;AAElG,+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 ErrorTranslator.toWire returns, so an app wraps it (\"webpieces' answer,\n// plus one header\") rather than copying its status-to-message table.\nexport { ExpressResponseWriter } from './ExpressResponseWriter';\n// How ApiEndUserError is answered: 'gui' (266, default) or 'edge' (its edgeHttpStatus, else 400).\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,49 +0,0 @@
|
|
|
1
|
-
import { HttpResponseDto } from '@webpieces/core-util';
|
|
2
|
-
/**
|
|
3
|
-
* How a server answers an {@link ApiEndUserError}.
|
|
4
|
-
*
|
|
5
|
-
* - `'gui'` (the default): 266. The call succeeded; the GUI (or any webpieces client, including the
|
|
6
|
-
* next server-to-server hop) decodes the error and shows its message. `edgeHttpStatus` is ignored,
|
|
7
|
-
* and still travels in the body so a downstream edge can use it.
|
|
8
|
-
* - `'edge'`: a partner/public REST edge whose published contract promises real 4xx statuses. The
|
|
9
|
-
* response carries `edgeHttpStatus`, or 400 when the thrower set none (still a caller-fixable
|
|
10
|
-
* end-user outcome). Only for the OUTERMOST server: a webpieces client receiving an end-user body at
|
|
11
|
-
* anything other than 266 treats it as a mismatched response, so internal hops stay `'gui'`.
|
|
12
|
-
*
|
|
13
|
-
* Chosen per router with `WebpiecesExpressRouter.setEndUserStatus`.
|
|
14
|
-
*/
|
|
15
|
-
export type EndUserStatus = 'gui' | 'edge';
|
|
16
|
-
/**
|
|
17
|
-
* HTTP adapter for the transport-neutral API error taxonomy, and the webpieces DEFAULT
|
|
18
|
-
* {@link ErrorTranslators} half on the server: an error in, the exact HTTP response out.
|
|
19
|
-
*
|
|
20
|
-
* An app that registers its own translator declines by DELEGATING here —
|
|
21
|
-
* `new ApiErrorHttpMapper('gui').toResponse(error)`, naming the mode its router uses — so "no
|
|
22
|
-
* translator registered" and "a translator that does not claim this error" produce byte-identical
|
|
23
|
-
* responses.
|
|
24
|
-
*/
|
|
25
|
-
export declare class ApiErrorHttpMapper {
|
|
26
|
-
private readonly endUserStatus;
|
|
27
|
-
/**
|
|
28
|
-
* @param endUserStatus - REQUIRED, with no default. A router configured with
|
|
29
|
-
* `setEndUserStatus('edge')` must pass `'edge'` here too when it delegates from its own
|
|
30
|
-
* translator, and a default would make `new ApiErrorHttpMapper()` publish 266 for a
|
|
31
|
-
* delegated end-user error on an edge router with nothing to grep for — a widening that is an
|
|
32
|
-
* ABSENCE rather than a token (`.claude/rules/no-backwards-compat.md`, shim shape #5). Saying
|
|
33
|
-
* the mode out loud keeps the mismatch a thing you can see at the call site.
|
|
34
|
-
*/
|
|
35
|
-
constructor(endUserStatus: EndUserStatus);
|
|
36
|
-
private readonly boundary;
|
|
37
|
-
private readonly genericMessages;
|
|
38
|
-
/**
|
|
39
|
-
* The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared
|
|
40
|
-
* rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as
|
|
41
|
-
* `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload
|
|
42
|
-
* carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and
|
|
43
|
-
* `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw
|
|
44
|
-
* error; unknown throws and caller-local failures come out as a 500 implementation failure
|
|
45
|
-
* because that is what the payload says.
|
|
46
|
-
*/
|
|
47
|
-
toResponse(error: Error): HttpResponseDto;
|
|
48
|
-
genericMessage(code: number): string;
|
|
49
|
-
}
|