New:Socket for Asana Is Now Available.Learn more
Sign In

@ultimat3/http

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/http - npm Package Compare versions

Comparing version
16.0.0
to
17.0.0
+49
src/route-cache.ts
// Single responsibility: the screen a route's `cache` hint gets where it is DECLARED.
//
// The layered form the rest of the framework uses: refuse where a value is WRITTEN, be total where
// it is USED. `cacheControl` is the total half — it drops a field it cannot emit and warns — and
// this is the half that throws, because at registration the author is standing right there.
import { routeCacheInvalid } from './errors';
import type { CacheHint } from './response';
import { isDeltaSeconds } from './response';
/**
* The three `CacheHint` fields that reach a `cache-control` directive as a NUMBER. `tags` and
* `vary` are string lists and `mode` is a closed union the compiler already decides.
*
* A tuple of literals rather than `Object.keys(hint)`: the set is the emitter's, not the caller's,
* so a field added to `CacheHint` and forgotten here is a compile error at the emitter's next edit
* rather than a silently unscreened age.
*/
const DELTA_SECONDS_FIELDS = [
'maxAgeSeconds',
'sMaxAgeSeconds',
'staleWhileRevalidateSeconds',
] as const;
/**
* `isDeltaSeconds` is IMPORTED from `response.ts`, never restated: that file owns what may be
* emitted, this one what may be declared, and a byte-identical predicate in both is how a screen
* and its emitter come to accept different sets — a route that registers cleanly and then emits no
* age at all. **Zero stays legal at both ends**: `max-age=0` means "revalidate every time" and the
* framework's own defaults declare it — `PRIVATE_CACHE`, `defaultCache`'s anonymous hint and the
* CLI's authorized-object hint all carry `maxAgeSeconds: 0`, so a floor of 1 would refuse the
* framework at its own boot.
*
* Called once per route by `createRouter`, which is the one way a `Route` becomes matchable — so
* every hint an app can serve has been through here, including the ones `@ultimat3/cli` mints for
* favicons, dev assets and storage objects.
*/
export const assertRouteCache = (
hint: CacheHint | undefined,
route: string,
path: string,
): void => {
if (hint === undefined) return;
for (const field of DELTA_SECONDS_FIELDS) {
const value = hint[field];
if (value === undefined || isDeltaSeconds(value)) continue;
throw routeCacheInvalid(route, path, field, value);
}
};
+53
-0

@@ -22,2 +22,37 @@ # @ultimat3/http

- **Every numeric knob `defineHttpConfig` resolves is screened, `As of 2026-08-26`** — `port`,
`bodyLimitBytes`, `requestTimeoutMs`, `maxInflight`, `drainTimeoutMs` and `trustedProxyHops`,
each a whole number in its own domain or `X_CONFIG_INVALID` (core's code, borrowed as
`@ultimat3/auth` borrows it). Measured with `NaN`, which is what `Number(process.env.…)` answers
for an unset variable: `total > NaN` is false so the body cap stopped capping and the whole
payload was buffered; `NaN <= 0` is false so a deadline armed and `setTimeout(fn, NaN)` is 1ms,
which 504s every request; `ceiling > 0` is false so `admit` shed nothing; and
`Math.max(0, Math.floor(NaN))` is `NaN`, so `trustProxy: true` silently trusted no hop and every
caller's ip became the proxy's. `Math.max(0, …)` was the guard for the last of those — a clamp is
not a validator, and this package relied on one. `webhook-verify.ts` screens its own two
(`toleranceMs`, `maxBytes`) and `rate-limit.ts` its `maxKeys`; the helper names carry `Finite`
(`assertFiniteCount`, `assertFiniteKeyCap`, `assertFiniteBodyLimit`) because
`bun run finite-bounds` recognises a repair by the shape of the CALL — spelled `count`, all five
config options read as unchecked to the ratchet while every one was screened. `maxBytes` is
refused HERE as well as inside `readWithinLimit`: that one is a file away and its `fix:` names
core's reader rather than the option the caller wrote.
**The FLOOR is per option, because only the caller knows what zero means** (`As of 2026-08-26`).
`requestTimeoutMs: 0` is "no deadline" and `maxInflight: 0` is "never shed" — decisions the code
reads — so those two floor at 0; `trustedProxyHops` floors at **1**, and it shipped for one day
screened at 0. `forwardedElement` answers `undefined` for `hops < 1`, so
`{ trustProxy: true, trustedProxyHops: 0 }` was byte-for-byte the failure the screen's own comment
names: `clientAddress` falls back to the socket, one rate-limit bucket for everything behind the
ingress, `x-forwarded-proto` untrusted and HSTS never emitted. `-1` and `NaN` were refused for
producing exactly that state and `0` was accepted into it. `resolveTrustedProxyHops` owns both
refusals — unset is `X_TRUST_PROXY_UNSET`, out of domain is `X_CONFIG_INVALID` — and there is no
`?? 0` behind it, because a default of zero reopens the same hole from the other side.
**`MAX_PROXY_HOPS` is EXPORTED, `As of 2026-08-26`**, and that is the point of it. It was module
private, so `@ultimat3/cli`'s `trustedHopsFromEnv` — which screens the same setting arriving as
`TRUSTED_PROXY_HOPS` — restated the literal, and one setting came to have two ceilings: that end
said 16 while this one said 64, so a deployment behind 20 hops was accepted by the library and
refused at boot. `cli` is tier 5 and this is tier 2, so the import is downward and legal. The
number lives here because this is where the setting is.
- Route `meta.auth` is required. Never default a route to public.

@@ -546,2 +581,19 @@ - **An app declares its half of `HttpConfig` through `configureHttp()`, and the boot lays its own

point. `factsOf` therefore reads a borrowed code's title off the error itself, never the map.
- **A `cache-control` age is delta-seconds or it is DROPPED, never emitted** (`As of 2026-08-26`).
`finiteDeltaSeconds` in `response.ts` — `Number.isSafeInteger && >= 0`, per field. `max-age=NaN`
is not a shorter age, it is an unparseable directive a conforming cache IGNORES, so the response
fell back to heuristic caching rather than to the declared age. TOTAL, never a throw: this is the
response path, and a bad cache hint must not become a 500. Every fallback is the SHORTER
direction — `max-age` to 0, `s-maxage` and `stale-while-revalidate` omitted — so nothing here can
lengthen an age the caller did not ask for. `http.cache_hint_not_delta_seconds` names the field.
**The boot-time half is `route-cache.ts`, `As of 2026-08-26`** — the layered form: refuse where
the value is WRITTEN, be total where it is USED. `createRouter` screens `Route.cache`'s three
delta-seconds fields per route and throws `X_CONFIG_INVALID` naming the route and the key, so
`cache: { maxAgeSeconds: Number(process.env.CACHE_AGE) }` fails at boot instead of registering
cleanly and surfacing as one warn line per request, forever. The two screens must accept exactly
the same set: this one decides what may be DECLARED, `finiteDeltaSeconds` what may be WRITTEN,
and a value one accepts and the other drops is a silent hole between them. **Zero stays legal at
both ends** — `max-age=0` is "revalidate every time" and `PRIVATE_CACHE`, `defaultCache`'s
anonymous hint and the CLI's authorized-object hint all declare it. `ctx.cache` is NOT screened
and must not be: it is app-set per request at runtime, which is the total side by definition.
- Tests must not touch the network — the preload seals `fetch`. Socket tests live in

@@ -569,2 +621,3 @@ `e2e/` and run with `bun test packages/http/e2e`, sealed: `start()` calls core's

| `cache-policy.ts` | the default `CacheHint` for a route that declared none — route AND actor |
| `route-cache.ts` | the screen a `Route.cache` hint gets where it is DECLARED, thrown from `createRouter`; the response path's `finiteDeltaSeconds` is the total half of the same rule |
| `rate-limit.ts` | the token-bucket maths, the store interface, the memory driver and `toBucket` |

@@ -571,0 +624,0 @@ | `rate-limit-postgres.ts` | the SHARED store: one table, one `insert … on conflict` per take, over a structural `PgExecutor` |

+5
-5
{
"name": "@ultimat3/http",
"version": "16.0.0",
"version": "17.0.0",
"description": "Owned request lifecycle over Bun.serve: router, ordered pipeline, problem+json errors",

@@ -34,7 +34,7 @@ "license": "MIT",

"dependencies": {
"@ultimat3/core": "16.0.0",
"@ultimat3/i18n": "16.0.0",
"@ultimat3/schema": "16.0.0",
"@ultimat3/time": "16.0.0"
"@ultimat3/core": "17.0.0",
"@ultimat3/i18n": "17.0.0",
"@ultimat3/schema": "17.0.0",
"@ultimat3/time": "17.0.0"
}
}

@@ -9,3 +9,3 @@ // The resolver every HTTP config goes through, so a value is either a locked default or an

import { type CsrfConfig, DEFAULT_CSRF } from './csrf';
import { trustProxyUnset } from './errors';
import { httpCountInvalid, trustProxyUnset } from './errors';
import {

@@ -125,2 +125,74 @@ DEFAULT_LOCALE_CONFIG,

/**
* A whole, in-range count, or the refusal that names it.
*
* `Number.isSafeInteger` and not `Number.isFinite`: these are byte counts, millisecond budgets and
* request ceilings, and above 2^53 a double cannot name its own successor — the same rule
* `@ultimat3/schema` states for an integer at the wire boundary. The `Finite` in the name is
* load-bearing: `bun run finite-bounds` recognises a repair by the shape of the CALL, so a screen
* named `count` left every option below reading as unchecked.
*
* `min` is the CALLER's, exactly as it is on `@ultimat3/core`'s `finiteCount`, because only the
* caller knows what zero means: `requestTimeoutMs: 0` is "no deadline" and `maxInflight: 0` is
* "never shed", both decisions the code reads, while `trustedProxyHops: 0` is a proxy trusted for
* nothing — the state the whole declaration exists to refuse. A helper that picked one bound would
* be wrong at half the call sites, and a second helper for "positive" would be the copy.
*/
const assertFiniteCount = (
name: string,
value: number,
max: number,
expected: string,
example: string,
min: 0 | 1 = 0,
): number => {
if (!Number.isSafeInteger(value) || value < min || value > max) {
throw httpCountInvalid(name, value, expected, example);
}
return value;
};
const MAX_PORT = 65_535;
/**
* Nobody has 64 proxies in front of one process; a bigger number is a typo, not a topology.
*
* EXPORTED, and that is the point of it: `@ultimat3/cli`'s `trustedHopsFromEnv` screens the same
* setting from `TRUSTED_PROXY_HOPS` and had to restate the literal, which is how one setting came
* to have two ceilings — that end said 16 while this one said 64, so a deployment behind 20 hops
* was accepted by the library and refused at boot. `cli` is tier 5 and this is tier 2, so the
* import is downward and legal; the number lives here because this is where the setting is.
*/
export const MAX_PROXY_HOPS = 64;
/**
* Which `x-forwarded-for` entry is the caller, or the refusal that says the declaration is not one.
*
* Screened, not clamped. `Math.max(0, Math.floor(x))` turned `-1` into `0` and `NaN` into `NaN`,
* and BOTH mean "trust nothing" to `forwardedElement` — so the one declaration saying which entry
* the caller wrote silently stopped being made, and every request's client ip became the proxy's
* own. One rate-limit bucket for everything behind the ingress, no word said.
*
* **`0` is that same state and is refused with them**, `As of 2026-08-26`. `forwardedElement`
* answers `undefined` for `hops < 1`, so `{ trustProxy: true, trustedProxyHops: 0 }` produced
* exactly the failure the screen was written for while the screen accepted it. One is the smallest
* topology `trustProxy: true` can describe.
*
* There is no `?? 0` fallback, and that is the point: an undeclared count is `trustProxyUnset()`,
* because "trust the header" and "know which entry of it" are one declaration and half of it is a
* header the caller writes. A default of zero would reopen the same hole from the other side.
*/
const resolveTrustedProxyHops = (trustProxy: boolean, declared: number | undefined): number => {
if (!trustProxy) return 0;
if (declared === undefined) throw trustProxyUnset();
return assertFiniteCount(
'trustedProxyHops',
declared,
MAX_PROXY_HOPS,
'the whole number of proxies that append to x-forwarded-for, at least 1',
'trustProxy: true, trustedProxyHops: 1',
1,
);
};
export const defineHttpConfig = (input: HttpConfigInput = {}): HttpConfig => {

@@ -140,3 +212,3 @@ // `ULTIMATE_ENV` is the framework's one environment key and `NODE_ENV` is only its fallback, so

// one declaration, and half of it is a header the caller writes.
if (trustProxy && input.trustedProxyHops === undefined) throw trustProxyUnset();
const trustedProxyHops = resolveTrustedProxyHops(trustProxy, input.trustedProxyHops);
const csp = { ...DEFAULT_SECURITY.csp, reportOnly: dev, ...input.security?.csp };

@@ -148,3 +220,11 @@ // Beside `assertCorsConfig`, and for its reason: a merged value is the only one that can be

return {
port: input.port ?? Number.parseInt(env('PORT') ?? '3000', 10),
// `Number.parseInt(env('PORT'), 10)` is `NaN` for `PORT=web`, and a config that carries NaN
// into `Bun.serve` binds a port nobody asked for.
port: assertFiniteCount(
'port',
input.port ?? Number.parseInt(env('PORT') ?? '3000', 10),
MAX_PORT,
'a whole port number from 0 to 65535, where 0 asks the OS for a free one',
'port: 3000',
),
hostname: input.hostname ?? env('HOSTNAME') ?? '0.0.0.0',

@@ -157,9 +237,36 @@ basePath: input.basePath ?? '/',

trustProxy,
trustedProxyHops: trustProxy ? Math.max(0, Math.floor(input.trustedProxyHops ?? 0)) : 0,
bodyLimitBytes: input.bodyLimitBytes ?? 1_048_576,
trustedProxyHops,
bodyLimitBytes: assertFiniteCount(
'bodyLimitBytes',
input.bodyLimitBytes ?? 1_048_576,
Number.MAX_SAFE_INTEGER,
'a whole number of bytes',
'bodyLimitBytes: 1_048_576',
),
// 30s: longer than any request a browser waits out, shorter than the 15s drain budget times
// two, so a rolling restart cannot be held open by work started just before SIGTERM.
requestTimeoutMs: input.requestTimeoutMs ?? 30_000,
maxInflight: input.maxInflight ?? 1_000,
drainTimeoutMs: input.drainTimeoutMs ?? null,
requestTimeoutMs: assertFiniteCount(
'requestTimeoutMs',
input.requestTimeoutMs ?? 30_000,
Number.MAX_SAFE_INTEGER,
'a whole number of milliseconds, where 0 means no deadline',
'requestTimeoutMs: 30_000',
),
maxInflight: assertFiniteCount(
'maxInflight',
input.maxInflight ?? 1_000,
Number.MAX_SAFE_INTEGER,
'a whole number of requests, where 0 means never shed',
'maxInflight: 1_000',
),
drainTimeoutMs:
input.drainTimeoutMs === undefined || input.drainTimeoutMs === null
? null
: assertFiniteCount(
'drainTimeoutMs',
input.drainTimeoutMs,
Number.MAX_SAFE_INTEGER,
'a whole number of milliseconds, or null for the lifecycle default',
'drainTimeoutMs: 15_000',
),
locale: { ...DEFAULT_LOCALE_CONFIG, ...input.locale },

@@ -166,0 +273,0 @@ tz: { ...DEFAULT_TZ_CONFIG, ...input.tz },

@@ -41,2 +41,6 @@ // The one place a framework error code becomes an HTTP status. A table, not a

X_CORS_CONFIG_INVALID: 500,
// Core's code, borrowed by `defineHttpConfig` for a numeric knob that is not a count. Same
// construction-time shelf as the row above, and it needs a row for the same reason: this table
// is closed over every code the package can throw, borrowed ones included.
X_CONFIG_INVALID: 500,
X_CSP_DIRECTIVE_INVALID: 500,

@@ -275,2 +279,9 @@ // Thrown while the server is being constructed, so no request is ever answered with it either.

X_LOCALE_UNSUPPORTED: 400,
// @ultimat3/core (declared beside `assertLocale`; `@ultimat3/time` owned it until 16.x) — the
// sibling of the row above, and strictly more the caller's fault: not a tag at all. It was
// pinned as unable to reach a request because the `locale` stage negotiates and never throws,
// which is true of that stage and irrelevant to `?locale=`, a path segment or an action input
// reaching `formatDate` / `formatMoney` / `describeCron`. Those raised it and answered 500,
// paging the on-call for a string the caller typed.
X_LOCALE_INVALID: 400,
// @ultimat3/money — a well-formed code this process carries no row for. The currency table is

@@ -277,0 +288,0 @@ // OPEN (`registerCurrency`), and every surface between the wire and the throw accepts any

@@ -59,2 +59,8 @@ // The HTTP layer's stable error codes. Every throw in this package goes through a

'X_DRAINING',
// Core's, and the code `app.config.ts is invalid` already means — borrowed for the numeric knobs
// `defineHttpConfig` screens, the same way `@ultimat3/auth` borrows it for a `defineAuth`
// declaration it cannot honour. A per-knob code of our own would be a second answer to a
// question core has already answered, and the throw happens while config resolves, so no request
// is ever answered with it.
'X_CONFIG_INVALID',
] as const;

@@ -339,2 +345,48 @@

/**
* A numeric knob that is not a count, refused where `app.config.ts` still names it.
*
* Every one of these arrives as `Number(process.env.X)` as often as a literal, and `NaN` is not
* nullish — so `??` passes it through, `Math.max`/`Math.floor` propagate it, and every comparison
* downstream then answers FALSE. What that produces is never a wrong number, it is the guard
* switching itself off: `bodyLimitBytes` stops capping the body, `maxInflight` stops shedding,
* `trustedProxyHops` stops trusting the proxy it was set for, and `requestTimeoutMs` arms a
* `setTimeout(fn, NaN)` — which is 1ms — so every request 504s. `Math.max(1, x)` is not a
* validator, which is exactly what `trustedProxyHops` was using.
*/
export const httpCountInvalid = (
name: string,
value: number,
expected: string,
example: string,
): HttpError =>
new HttpError({
code: 'X_CONFIG_INVALID',
cause: `http.${name} is ${String(value)}; it must be ${expected}, and NaN is what Number(process.env.…) answers for an unset variable — every comparison against it is false, so the limit it names stops being enforced rather than being enforced wrongly`,
fix: `set ${name} to ${expected} in configureHttp({ ${example} }), and parse an environment value before you pass it: Number.parseInt(process.env.${name.replace(/[A-Z]/g, (c) => `_${c}`).toUpperCase()} ?? '', 10) is NaN when the variable is unset`,
meta: { option: name, value: String(value) },
});
/**
* A route DECLARING a cache age that a cache cannot read. `X_CONFIG_INVALID` and not a code of its
* own, for the reason the borrowed list gives: this is a declaration the framework cannot honour,
* and it is thrown while the route table is built, so no request is ever answered with it.
*
* Thrown here because `cacheControl` cannot throw: the response path is total by design — it drops
* the field and logs — so without this the mistake is a log line once per request, forever, while
* the response falls back to HEURISTIC caching, the one behaviour no `CacheHint` ever asked for.
*/
export const routeCacheInvalid = (
route: string,
path: string,
field: string,
value: number,
): HttpError =>
new HttpError({
code: 'X_CONFIG_INVALID',
cause: `route ${route} (${path}) declares cache.${field} = ${String(value)}, and cache-control delta-seconds is 1*DIGIT — a fraction, a negative and NaN are directives a conforming cache IGNORES, so the response would fall back to heuristic caching instead of the age declared here`,
fix: `in the route file declaring ${route}, set meta.cache.${field} to a whole number of seconds, 0 or more — 0 is legal and means "revalidate every time". If it comes from the environment, give it a default before it reaches the route: Number(process.env.X) is NaN when the variable is unset and ?? does not catch that, because NaN is not nullish`,
meta: { route, path, option: `cache.${field}`, value: String(value) },
});
/**
* SIGTERM has run the `accept` phase: `readyz` is already 503 and the socket is closing, but a

@@ -341,0 +393,0 @@ * connection the load balancer had not yet stopped using still arrives. Answering it with a

@@ -19,3 +19,3 @@ // The public surface of @ultimat3/http. Explicit, never `export *`: what is not

export type { HttpConfig, HttpConfigInput } from './config';
export { defineHttpConfig, stripBasePath } from './config';
export { defineHttpConfig, MAX_PROXY_HOPS, stripBasePath } from './config';
export type { ActorView, RequestContext, RequestContextInit } from './context';

@@ -86,2 +86,3 @@ export {

requestTimedOut,
routeCacheInvalid,
routeConflict,

@@ -88,0 +89,0 @@ routeNotFound,

@@ -6,2 +6,3 @@ // Token-bucket rate limiting. The store is an interface so the same limiter runs in-memory in

import { type Clock, systemClock } from '@ultimat3/core';
import { httpCountInvalid } from './errors';
import {

@@ -236,2 +237,13 @@ rateLimited,

/** A whole positive count, or the config refusal that names it. */
const assertFiniteKeyCap = (option: string, value: number): number => {
if (Number.isSafeInteger(value) && value >= 1) return value;
throw httpCountInvalid(
`rate limit store ${option}`,
value,
'a whole number of at least 1',
`maxKeys: ${DEFAULT_MAX_RATE_LIMIT_KEYS}`,
);
};
/**

@@ -251,3 +263,8 @@ * Default driver: correct for one process, which is exactly dev and tests.

): MemoryRateLimitStore => {
const maxKeys = Math.max(1, Math.floor(options.maxKeys ?? DEFAULT_MAX_RATE_LIMIT_KEYS));
// Screened, not clamped. `Math.max(1, Math.floor(x))` was the guard, and `Math.floor(NaN)` is
// `NaN`: `buckets.size > maxKeys` in `take` is then false so the sweep never runs, and
// `buckets.size <= maxKeys` in the sweep is false so it would evict nothing if it did. The cap
// is the only thing between a rotating-address scan and this process's memory, and the keys are
// addresses the caller chooses — a clamp that quietly answers `NaN` removes it.
const maxKeys = assertFiniteKeyCap('maxKeys', options.maxKeys ?? DEFAULT_MAX_RATE_LIMIT_KEYS);
const evictTo = Math.max(1, Math.floor(maxKeys * 0.9));

@@ -254,0 +271,0 @@ const buckets = new Map<string, BucketState>();

// Response constructors. Every response in the framework is built here so that
// content types, charsets and cache semantics are decided once instead of per route.
import { logger } from '@ultimat3/core';
import { TIMEZONE_HEADER } from '@ultimat3/time';

@@ -119,13 +120,61 @@ import { toProblem } from './error-facts';

/** A year, the only age at which `immutable` says anything a shorter one does not. */
const IMMUTABLE_MAX_AGE_SECONDS = 31_536_000;
/**
* RFC-9111 delta-seconds is `1*DIGIT`, so `max-age=NaN` is not a shorter age or a longer one — it
* is an unparseable directive, and a conforming cache IGNORES a directive it cannot parse. The
* response then falls back to HEURISTIC caching (a fraction of `Last-Modified`'s age), which is
* the one behaviour no `CacheHint` ever asked for and the one nothing downstream can detect.
* `??` guards nullish and `NaN` is not nullish, so an age computed from a timestamp difference or
* read out of an env value arrives here intact; a fraction (`ms / 1000`) and a negative (a clock
* that went backwards) are the same unparseable token.
*
* The name carries `finite` deliberately: `bun run finite-bounds` recognises a repair by the shape
* of the CALL, so this screen spelled `deltaSeconds` read as no screen at all.
*
* `isDeltaSeconds` is EXPORTED and has exactly two readers, which is the point of it: this one
* decides what may be WRITTEN, and `route-cache.ts` decides what may be DECLARED. A byte-identical
* predicate in both files is how the two drift, and a value one accepts and the other drops is a
* silent hole in the middle — a route that registers cleanly and then emits no age.
*
* TOTAL, never a throw: this is the response path, where refusing turns a bad cache hint into a
* 500. A dropped age is always the SAFER direction — no `s-maxage` means a shared cache falls back
* to `max-age`, and a `max-age` of 0 means revalidate — so nothing here can lengthen an age the
* caller did not ask for. The warning is what keeps it from being silent; the fix belongs at the
* declaration, and `field` names which one.
*/
export const isDeltaSeconds = (value: number): boolean => Number.isSafeInteger(value) && value >= 0;
const finiteDeltaSeconds = (field: string, value: number): number | undefined => {
if (isDeltaSeconds(value)) return value;
logger.warn('http.cache_hint_not_delta_seconds', { field, value: String(value) });
return undefined;
};
export const cacheControl = (hint: CacheHint): string => {
if (hint.mode === 'no-store') return 'no-store';
if (hint.mode === 'immutable') {
return `public, max-age=${hint.maxAgeSeconds ?? 31_536_000}, immutable`;
const age = finiteDeltaSeconds(
'maxAgeSeconds',
hint.maxAgeSeconds ?? IMMUTABLE_MAX_AGE_SECONDS,
);
// 0 rather than the year: a declared age that is not a number is not evidence for the longest
// age in the file, and an over-long `immutable` is the one cache mistake a purge cannot undo.
return `public, max-age=${String(age ?? 0)}, immutable`;
}
const parts = [hint.mode, `max-age=${hint.maxAgeSeconds ?? 0}`];
const parts = [
hint.mode,
`max-age=${String(finiteDeltaSeconds('maxAgeSeconds', hint.maxAgeSeconds ?? 0) ?? 0)}`,
];
if (hint.mode === 'public' && hint.sMaxAgeSeconds !== undefined) {
parts.push(`s-maxage=${hint.sMaxAgeSeconds}`);
const shared = finiteDeltaSeconds('sMaxAgeSeconds', hint.sMaxAgeSeconds);
if (shared !== undefined) parts.push(`s-maxage=${String(shared)}`);
}
if (hint.staleWhileRevalidateSeconds !== undefined) {
parts.push(`stale-while-revalidate=${hint.staleWhileRevalidateSeconds}`);
const stale = finiteDeltaSeconds(
'staleWhileRevalidateSeconds',
hint.staleWhileRevalidateSeconds,
);
if (stale !== undefined) parts.push(`stale-while-revalidate=${String(stale)}`);
}

@@ -132,0 +181,0 @@ return parts.join(', ');

@@ -19,2 +19,3 @@ // The route table. A segment trie, not a regex list, so matching cost is bounded by

import type { CacheHint } from './response';
import { assertRouteCache } from './route-cache';
import type { Schema } from './validate';

@@ -133,2 +134,6 @@

for (const route of routes) {
// Before the trie is touched: a hint that cannot be emitted is refused where it was WRITTEN,
// not on the response path — `cacheControl` is total there by design, so the alternative is a
// log line once per request and a response that quietly falls back to heuristic caching.
assertRouteCache(route.meta.cache, route.meta.name, route.path);
let current = root;

@@ -135,0 +140,0 @@ const segments = segmentsOf(route.path);

@@ -25,3 +25,8 @@ // The inbound half of the framework's webhook mechanism: prove a request was signed by the holder

} from '@ultimat3/core';
import { bodyInvalid, webhookSignatureInvalid, webhookSignatureStale } from './errors';
import {
bodyInvalid,
httpCountInvalid,
webhookSignatureInvalid,
webhookSignatureStale,
} from './errors';

@@ -37,2 +42,19 @@ /**

/**
* The screen above `toleranceMs`, kept beside the constant it defaults to.
*
* The `Finite` in the name is load-bearing: `bun run finite-bounds` recognises a repair by the
* shape of the CALL, so a screen named for what it guards is invisible to the ratchet that would
* otherwise catch the next one of these.
*/
const assertFiniteToleranceMs = (toleranceMs: number): number => {
if (Number.isSafeInteger(toleranceMs) && toleranceMs >= 0) return toleranceMs;
throw httpCountInvalid(
'webhook toleranceMs',
toleranceMs,
'a whole number of milliseconds, zero or more',
'toleranceMs: 300_000',
);
};
/**
* Restated rather than read from `HttpConfig.bodyLimitBytes` (same number, `config.ts`): this

@@ -44,2 +66,19 @@ * function runs inside a route handler with a raw `Request` and no pipeline config in scope, and a

/**
* The same screen above `maxBytes`, refused HERE and not only where it lands.
*
* `readWithinLimit` refuses a non-finite limit as well, and that is one file away: its `fix:` names
* core's reader, while the edit the caller has to make is the `maxBytes` written on this call. Zero
* is excluded on purpose — a cap of nothing refuses every delivery a sender can make.
*/
const assertFiniteBodyLimit = (maxBytes: number): number => {
if (Number.isSafeInteger(maxBytes) && maxBytes >= 1) return maxBytes;
throw httpCountInvalid(
'webhook maxBytes',
maxBytes,
'a whole number of bytes, at least 1',
`maxBytes: ${DEFAULT_WEBHOOK_BODY_LIMIT}`,
);
};
export interface WebhookVerifyOptions {

@@ -103,3 +142,3 @@ /** The shared secret for THIS sender. Never logged, never rendered into a refusal. */

const maxBytes = options.maxBytes ?? DEFAULT_WEBHOOK_BODY_LIMIT;
const maxBytes = assertFiniteBodyLimit(options.maxBytes ?? DEFAULT_WEBHOOK_BODY_LIMIT);
// Through core's counting reader, the same one `UltimateRequest.#read` uses: a sender that

@@ -130,3 +169,7 @@ // announces no length must not be able to make this handler hold an unbounded payload before the

const signedAtMs = signature.timestampSeconds * 1_000;
const toleranceMs = options.toleranceMs ?? DEFAULT_WEBHOOK_TOLERANCE_MS;
// The tolerance IS the replay window, so it is screened before it is compared against: `skewMs >
// NaN` is false, which does not widen the window — it removes it, and a webhook captured a year
// ago verifies forever with every other check passing. Refused as the config it is, and not by
// the sender's error: nothing the caller sends can fix it.
const toleranceMs = assertFiniteToleranceMs(options.toleranceMs ?? DEFAULT_WEBHOOK_TOLERANCE_MS);
const skewMs = Math.abs((options.clock ?? systemClock).now().getTime() - signedAtMs);

@@ -133,0 +176,0 @@ // Both directions: a sender whose clock runs ahead is the same replay window pointed the other