Initial Commit
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# `request-pipeline-helpers` — pure utilities for the request pipeline
|
||||
|
||||
## Purpose
|
||||
|
||||
This module concentrates the pure (no-`this`, no side-effecting) helpers the §6 request pipeline relies on: request-id generation, URL parsing tolerant of relative forms, JSON body reader with a size cap, `Response` wrapping for handler-returned values, an `X-Request-Id` header stamper, and the `RequestContext` builder handed to plugin handlers. The split is mechanical — every function used to live inside `request-pipeline.ts` and was extracted verbatim when the file outgrew the CLAUDE.md per-file LOC budget.
|
||||
|
||||
Keeping these helpers in their own module lets the driver, the step gates, and the outcome emitters all consume them without circular imports, and lets each helper be unit-tested via the existing end-to-end pipeline tests with zero behavioural drift.
|
||||
|
||||
## Features
|
||||
|
||||
- `defaultRequestId()` — 32-char hex string drawn from `crypto.getRandomValues`, with a `Math.random` fallback only when WebCrypto is missing.
|
||||
- `parseUrl(url)` — returns `URL` or `null`; tolerates absolute and relative inputs without throwing.
|
||||
- `safeReadBody(request)` — JSON body reader that enforces a 5 MiB cap and converts size or parser errors into `HOST.REQUEST_VALIDATION_FAILED`.
|
||||
- `toResponse(value)` — wraps handler return values as `Response` (passes through existing `Response` unchanged).
|
||||
- `withRequestHeaders(response, id)` — clones a `Response` with `X-Request-Id` set.
|
||||
- `buildRequestContext(args)` — assembles the typed `RequestContext` (including the documented `tenant`-may-be-null escape hatch for the foundation-gap-5 default behaviour).
|
||||
- `DEFAULT_IDEMPOTENCY_TTL_MS` (10 minutes) and `HANDLER_VALIDATION_LIMIT_BYTES` (5 MiB) constants.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildRequestContext,
|
||||
defaultRequestId,
|
||||
parseUrl,
|
||||
toResponse,
|
||||
withRequestHeaders,
|
||||
} from "@suaveplan/api-host/request-pipeline-helpers";
|
||||
|
||||
const id = defaultRequestId();
|
||||
const url = parseUrl(request.url);
|
||||
const response = withRequestHeaders(toResponse({ ok: true }), id);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
`safeReadBody` is the size-bounded JSON reader the request-validation step uses; consumers needing identical semantics outside the pipeline (e.g. an admin endpoint) can call it directly:
|
||||
|
||||
```ts
|
||||
import { safeReadBody } from "@suaveplan/api-host/request-pipeline-helpers";
|
||||
|
||||
try {
|
||||
const body = await safeReadBody(request);
|
||||
// body is `unknown`; downstream Zod parse expected.
|
||||
} catch (cause) {
|
||||
// HOST.REQUEST_VALIDATION_FAILED — already shaped for the boundary.
|
||||
throw cause;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
```ts
|
||||
const DEFAULT_IDEMPOTENCY_TTL_MS: number;
|
||||
const HANDLER_VALIDATION_LIMIT_BYTES: number;
|
||||
function defaultRequestId(): string;
|
||||
function parseUrl(url: string): URL | null;
|
||||
function safeReadBody(request: Request): Promise<unknown>;
|
||||
function toResponse(value: unknown): Response;
|
||||
function withRequestHeaders(response: Response, requestId: string): Response;
|
||||
function buildRequestContext(args: BuildRequestContextArgs): RequestContext;
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Every helper is referentially transparent except `safeReadBody` (which reads the request stream) and `defaultRequestId` (which calls into WebCrypto). All other functions take their inputs by parameter and never touch module-scoped state, so they are safe to call from any tier — universal, server, browser, or test.
|
||||
|
||||
`buildRequestContext` performs a documented cast for `tenant`/`tenantId`: the shared `RequestContext` type declares both as non-nullable for typed-handler ergonomics, but foundation-gap-5 made the runtime value nullable when the resolver chain misses and `defaultTenant` is `null` (the new default). Handlers consuming tenant-bound capabilities must defensively gate on `ctx.tenant === null`.
|
||||
|
||||
The 5 MiB limit on `safeReadBody` is intentional — larger payloads should use a streaming upload route (`streaming: true`) that bypasses request validation buffering entirely. The constant is exported so operators can audit it without grepping the implementation.
|
||||
Reference in New Issue
Block a user