@timber-js/app 0.2.0-alpha.213 → 0.2.0-alpha.214
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/dist/_chunks/{actions-CEootpB1.js → actions-CUh3cClk.js} +2 -2
- package/dist/_chunks/{actions-CEootpB1.js.map → actions-CUh3cClk.js.map} +1 -1
- package/dist/_chunks/{canonicalize-CgHoscYO.js → canonicalize-BAWkWiKK.js} +28 -2
- package/dist/_chunks/{canonicalize-CgHoscYO.js.map → canonicalize-BAWkWiKK.js.map} +1 -1
- package/dist/_chunks/{chains-BfoPFraI.js → chains-CjK1Eu6a.js} +2 -2
- package/dist/_chunks/{chains-BfoPFraI.js.map → chains-CjK1Eu6a.js.map} +1 -1
- package/dist/_chunks/{classify-BT66U83D.js → classify-GAdt6aiA.js} +2 -2
- package/dist/_chunks/{classify-BT66U83D.js.map → classify-GAdt6aiA.js.map} +1 -1
- package/dist/_chunks/{cli-check-BfQ54-UJ.js → cli-check-DOzjlycg.js} +3 -3
- package/dist/_chunks/{cli-check-BfQ54-UJ.js.map → cli-check-DOzjlycg.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-czh2dsLs.js → cli-schema-sync-B9wDaQvW.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-czh2dsLs.js.map → cli-schema-sync-B9wDaQvW.js.map} +1 -1
- package/dist/_chunks/{client-dep-entries-CQwpb8dI.js → client-dep-entries-DyDqXOF9.js} +2 -2
- package/dist/_chunks/{client-dep-entries-CQwpb8dI.js.map → client-dep-entries-DyDqXOF9.js.map} +1 -1
- package/dist/_chunks/{convention-lint-BEVW4EID.js → convention-lint-B3QEGJX7.js} +2 -2
- package/dist/_chunks/{convention-lint-BEVW4EID.js.map → convention-lint-B3QEGJX7.js.map} +1 -1
- package/dist/_chunks/{dev-server-FKxptbnI.js → dev-server-_9L4KWC-.js} +3 -3
- package/dist/_chunks/{dev-server-FKxptbnI.js.map → dev-server-_9L4KWC-.js.map} +1 -1
- package/dist/_chunks/{error-boundary-9g_Lb2na.js → error-boundary-xxxLtXt6.js} +38 -5
- package/dist/_chunks/{error-boundary-9g_Lb2na.js.map → error-boundary-xxxLtXt6.js.map} +1 -1
- package/dist/_chunks/{live-graph-C_4v-fHv.js → live-graph-Cd3YHvrH.js} +4 -4
- package/dist/_chunks/{live-graph-C_4v-fHv.js.map → live-graph-Cd3YHvrH.js.map} +1 -1
- package/dist/_chunks/{poison-scan-C92liMAr.js → poison-scan-BnJjBOkn.js} +2 -2
- package/dist/_chunks/{poison-scan-C92liMAr.js.map → poison-scan-BnJjBOkn.js.map} +1 -1
- package/dist/_chunks/{scanner-CQt12vE2.js → scanner-CKAT5gRx.js} +2 -2
- package/dist/_chunks/{scanner-CQt12vE2.js.map → scanner-CKAT5gRx.js.map} +1 -1
- package/dist/_chunks/{walkers-DAT4avhZ.js → walkers-DwCXEyRu.js} +3 -3
- package/dist/_chunks/{walkers-DAT4avhZ.js.map → walkers-DwCXEyRu.js.map} +1 -1
- package/dist/adapters/nitro-preview.d.ts.map +1 -1
- package/dist/adapters/nitro.js +11 -2
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/analyze/crawl-entry.js +3 -3
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cli.js +3 -3
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/internal.js +22 -7
- package/dist/client/internal.js.map +1 -1
- package/dist/client/router-effects.d.ts +70 -12
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/routing/index.js +2 -2
- package/dist/server/canonicalize.d.ts +22 -0
- package/dist/server/canonicalize.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.js +54 -3
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +31 -0
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/nitro-preview.ts +10 -1
- package/src/client/router-effects.ts +99 -14
- package/src/client/router.ts +29 -13
- package/src/server/canonicalize.ts +29 -0
- package/src/server/pipeline-helpers.ts +61 -0
- package/src/server/pipeline.ts +13 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as canonicalize } from "./canonicalize-BAWkWiKK.js";
|
|
2
2
|
import { a as revalidationAls, i as requestContextAls, r as reactCacheScopeAls } from "./als-registry-BZqHCtq-.js";
|
|
3
3
|
import { C as isRedirectSignal, F as withSpan, S as isDenySignal, T as isDevMode, b as REDIRECT_BRAND, w as isDebug, y as DENY_BRAND } from "./logger-CbLdcy-W.js";
|
|
4
4
|
import { n as _setGetSegmentParamsFn, t as _setGetSearchParamsFn } from "./als-slots-BEEIPKYm.js";
|
|
@@ -1233,4 +1233,4 @@ function revalidationPathMatches(revalidatedPath, requestPathname, requestSearch
|
|
|
1233
1233
|
//#endregion
|
|
1234
1234
|
export { setMutableCookieContext as C, setMatchedSegmentPath as S, runOutsideReactCacheScope as T, getHeaders as _, parseFormData as a, markResponseFlushed as b, deny as c, waitUntil as d, getCookie as f, getHeader as g, applyRequestHeaderOverlay as h, coerce as i, redirect as l, getSetCookieHeaders as m, revalidatePath as n, DenySignal as o, getCookieJar as p, revalidateTag as r, RedirectSignal as s, executeAction as t, redirectExternal as u, getSearchParams as v, setSegmentParams as w, runWithRequestContext as x, getSegmentParams as y };
|
|
1235
1235
|
|
|
1236
|
-
//# sourceMappingURL=actions-
|
|
1236
|
+
//# sourceMappingURL=actions-CUh3cClk.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions-CEootpB1.js","names":[],"sources":["../../src/server/react-cache-scope.ts","../../src/server/request-context.ts","../../src/server/cookie-parsing.ts","../../src/server/cookie-context.ts","../../src/server/primitives.ts","../../src/server/form-data.ts","../../src/server/actions.ts"],"sourcesContent":["/**\n * React.cache scopes — opening, resuming, and leaving the per-request\n * `React.cache` scope that `react-cache-bridge.ts` makes Flight read.\n *\n * Kept apart from the bridge so request-context and cookie code can use\n * it without importing `react`. See design/spike-TIM-1529-react-cache-bridge.md.\n */\n\nimport { reactCacheScopeAls, type ReactCacheScope } from './als-registry.ts';\nimport { isDevMode } from './debug.ts';\n\nexport type { ReactCacheScope };\n\n/** A new, empty `React.cache` scope. */\nexport function createReactCacheScope(): ReactCacheScope {\n return new Map();\n}\n\n/**\n * Run `fn` in a `React.cache` scope — a fresh one unless `scope` is given.\n * Every `React.cache` call made from `fn` — synchronously, in its async\n * continuations, and in any Flight render it starts — shares one memo table.\n *\n * Opened by `runWithRequestContext`, so each request context gets its own\n * scope. Pass `scope` to resume one across two ALS runs: the post-action\n * revalidation opens a fresh scope for the target route's middleware and\n * renders the revalidated tree in that same scope later.\n */\nexport function runWithReactCacheScope<T>(\n fn: () => T,\n scope: ReactCacheScope = createReactCacheScope()\n): T {\n return reactCacheScopeAls.run(scope, fn);\n}\n\n/**\n * Run `fn` with no timber scope, so `React.cache` behaves as stock React:\n * no dedupe outside a render, Flight's own per-render cache inside one.\n *\n * Used for server action bodies — an action reads, writes cookies, and\n * mutates, so memoized reads would go stale mid-action (the same choice\n * Next.js makes) — and for renders whose output outlives the request\n * (`cache.component` capture), which must not bake request-memoized values\n * into a cross-request cache entry.\n */\nexport function runOutsideReactCacheScope<T>(fn: () => T): T {\n return reactCacheScopeAls.exit(fn);\n}\n\n// ─── Dev diagnostic: cookie written after a cached read ──────────────────\n//\n// A value React.cache stored was computed from the request inputs it read,\n// and does not see a later write — a cached getUser() that read the session\n// cookie still returns the old user after middleware deletes the cookie.\n// The scope is deliberately NOT cleared on a write: that would silently\n// discard every value middleware warmed (spike doc §Decisions D3). Instead,\n// dev mode flags the one case that can go stale: a cookie read while the\n// scope held values, then written later in the same request. Tracking is\n// per scope, so it ends with the request; production does none of it.\n\n/** Stands for \"every cookie\" — a getAll()/size read, or a clear() write. */\nexport const ANY_COOKIE = '*';\n\nconst cookieReads = new WeakMap<ReactCacheScope, Set<string>>();\nconst warnedScopes = new WeakSet<ReactCacheScope>();\n\n/**\n * Dev-only: record that cookie `name` (or {@link ANY_COOKIE}) was read\n * while `React.cache` held values in this request — the read may have\n * happened inside a cached function.\n */\nexport function noteCookieReadForReactCache(name: string): void {\n if (!isDevMode()) return;\n const scope = reactCacheScopeAls.getStore();\n if (!scope || scope.size === 0) return;\n let reads = cookieReads.get(scope);\n if (!reads) {\n reads = new Set();\n cookieReads.set(scope, reads);\n }\n reads.add(name);\n}\n\n/**\n * Dev-only: warn (once per request) when a cookie recorded by\n * {@link noteCookieReadForReactCache} is written. `name` is the written\n * cookie, or {@link ANY_COOKIE} for `clear()`. Dev-only because reads are\n * only recorded in dev.\n */\nexport function warnIfCachedCookieIsWritten(name: string): void {\n const scope = reactCacheScopeAls.getStore();\n if (!scope || warnedScopes.has(scope)) return;\n const reads = cookieReads.get(scope);\n if (!reads) return;\n const stale = name === ANY_COOKIE ? reads.size > 0 : reads.has(name) || reads.has(ANY_COOKIE);\n if (!stale) return;\n warnedScopes.add(scope);\n const which = name === ANY_COOKIE ? 'Cookies were cleared' : `Cookie \"${name}\" was written`;\n console.warn(\n `[timber] ${which} after being read while React.cache held values for this request. ` +\n `A cached function that read it keeps returning the old result — pass the cookie ` +\n `value in as an argument (e.g. getUser(sessionToken)) so a change is a new cache key.`\n );\n}\n","/**\n * Request Context — per-request ALS store for headers, search params,\n * segment params, and request scope lifecycle.\n *\n * Follows the same pattern as tracing.ts: a module-level AsyncLocalStorage\n * instance, public accessor functions that throw outside request scope,\n * and a framework-internal `runWithRequestContext()` to establish scope.\n *\n * Cookie state lives in `cookie-context.ts` (split out in TIM-853). The\n * scope set up here owns the cookie jar / parsedCookies fields on the\n * store, but the cookie API and helpers are in the cookie module.\n *\n * See design/04-authorization.md §\"AccessContext does not include cookies or headers\"\n * and design/11-platform.md §\"AsyncLocalStorage\".\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport {\n requestContextAls,\n type ActionErrorForRender,\n type RequestContextStore,\n} from './als-registry.ts';\nimport { _setGetSearchParamsFn, _setGetSegmentParamsFn } from '../shared/als-slots.ts';\nimport { appVisibleSearch } from '../shared/rsc-cache-key.ts';\nimport {\n mergeSlotParams,\n resolveSegmentParams,\n type SlotParamsRecord,\n} from '../shared/slot-params.ts';\nimport { isDebug } from './debug.ts';\nimport { runWithReactCacheScope, type ReactCacheScope } from './react-cache-scope.ts';\nimport type { ReactFormState } from 'react-dom/client';\n\n// Re-export the ALS for framework-internal consumers that need direct access.\nexport { requestContextAls };\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns a read-only view of the current request's headers.\n *\n * Available in middleware, access checks, server components, and server actions.\n * Throws if called outside a request context (security principle #2: no global fallback).\n */\nexport function getHeaders(): ReadonlyHeaders {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getHeaders() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n\n return store.headers;\n}\n\n/**\n * Returns the value of a single request header, or undefined if absent.\n *\n * Thin wrapper over `getHeaders().get(name)` for the common case where\n * you need exactly one header.\n *\n * @internal — not part of the public API. Use `getHeaders().get(name)` instead.\n */\nexport function getHeader(name: string): string | undefined {\n const headers = getHeaders();\n return headers.get(name) ?? undefined;\n}\n\n/**\n * Returns the current request's raw URLSearchParams.\n *\n * @internal — not part of the public API. Use `defineSearchParams().get()` instead.\n */\nexport function getSearchParams(): URLSearchParams {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getSearchParams() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n\n return store.searchParams;\n}\n\n// Eagerly register getSearchParams with the search-params module so\n// searchParams.get() can call it without a dynamic import.\n// Dynamic imports lose ALS context in React's RSC Flight renderer,\n// breaking getSearchParams() in parallel slot pages. See TIM-523.\n_setGetSearchParamsFn(getSearchParams);\n\n// Eagerly register getSegmentParams with the segment-params module so\n// segmentParams.get() can call it without a dynamic import.\n// Same pattern as search params — dynamic imports lose ALS context. See TIM-523.\n_setGetSegmentParamsFn(getSegmentParams);\n\n/**\n * Returns the current request's coerced segment params.\n *\n * The optional `segmentPath` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value. Pass a segment path\n * (e.g. `'/(browse)/[artistSlug]/[year]'`) to narrow the return type to the\n * exact params shape for that segment.\n *\n * Without an argument, returns all coerced params with optional types.\n *\n * See design/41-global-params.md §Function Signatures\n */\nexport function getSegmentParams(segmentPath?: string): CoercedParams {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getSegmentParams() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n if (!store.segmentParams) {\n throw new Error(\n '[timber] getSegmentParams() called before route matching completed. ' +\n 'Segment params are not available until after the route is matched.'\n );\n }\n\n // Parallel slots may derive a param the main route never had, or the same\n // name with a different type (catch-all [...year] → string[] vs dynamic\n // [year] → string). `resolveSegmentParams` is the shared definition the\n // client's useSegmentParams() also reads, so the two halves of one\n // documented API cannot drift (TIM-1285).\n const slotResolved = resolveSegmentParams(store.segmentParams, store.slotParams, segmentPath);\n if (slotResolved !== store.segmentParams) return slotResolved;\n\n // Dev-mode validation: warn when the segment path expects dynamic\n // segments that don't exist on the actual matched segment. This catches\n // bugs like passing a child segment's $segment to a parent layout.\n // Skip for slot paths — they're validated against their own chain.\n // See design/41-global-params.md §Runtime Validation\n if (segmentPath && isDebug() && store.matchedSegmentPath) {\n const expected = extractDynamicSegments(segmentPath);\n const actual = extractDynamicSegments(store.matchedSegmentPath);\n const missing = expected.filter((s) => !actual.includes(s));\n\n if (missing.length > 0) {\n console.warn(\n `[timber] getSegmentParams('${segmentPath}') called but current segment is '${store.matchedSegmentPath}'\\n` +\n ` Missing params: ${missing.join(', ')}\\n` +\n ` These will be undefined at runtime despite the type annotation.`\n );\n }\n }\n\n return store.segmentParams;\n}\n\n/**\n * Set the segment params on the current request context.\n * Called by the pipeline after route matching and param coercion.\n *\n * @internal — framework use only\n */\nexport function setSegmentParams(params: CoercedParams): void {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] setSegmentParams() called outside of a request context.');\n }\n store.segmentParams = params;\n}\n\n/**\n * Store per-slot coerced segment params in the current request context.\n * Called by the route element builder when resolving parallel slots.\n *\n * @param segmentPath — The slot's full tree path (e.g. '/(browse)/@shows/[artistSlug]/[...year]')\n * @param params — The slot's coerced segment params\n * @internal — framework use only\n */\nexport function setSlotParams(segmentPath: string, params: CoercedParams): void {\n const store = requestContextAls.getStore();\n if (!store) return; // non-throwing — optional diagnostic\n if (!store.slotParams) store.slotParams = Object.create(null) as SlotParamsRecord;\n store.slotParams[segmentPath] = params;\n}\n\n/**\n * Run `fn` with the segment params a parallel slot's own chain produced, so\n * a no-arg `getSegmentParams()` inside that slot returns the slot's params\n * rather than the main route's.\n *\n * ALS is the only channel available: the slot's page and layouts are plain\n * components that receive no params prop and cannot know their own tree\n * path, so they have no way to ask for the keyed overload. The slot resolver\n * invokes each of them itself (page through `SafeSlotPage`, layouts and\n * access functions through their wrappers), and this scopes those calls.\n *\n * The store is wrapped in a Proxy rather than cloned. Everything a slot\n * renders can write to the *live* request store — `setDenyStatus` from an\n * access gate, cookie writes, the flush flag — and a clone would silently\n * strand those writes in a copy the pipeline never reads. Only the two\n * params fields are shadowed; every other read and every write passes\n * straight through to the real store.\n *\n * Returns `fn()`'s result unchanged, and no-ops outside a request context or\n * before route matching has set the main params. See TIM-1283.\n *\n * @internal — framework use only\n */\nexport function runWithSlotSegmentParams<T>(\n slotSegmentPath: string | undefined,\n slotParams: CoercedParams,\n fn: () => T\n): T {\n const store = requestContextAls.getStore();\n if (!store?.segmentParams) return fn();\n\n const merged = mergeSlotParams(store.segmentParams, slotParams);\n const matchedSegmentPath = slotSegmentPath ?? store.matchedSegmentPath;\n const scoped = new Proxy(store, {\n get(target, prop, receiver) {\n if (prop === 'segmentParams') return merged;\n if (prop === 'matchedSegmentPath') return matchedSegmentPath;\n return Reflect.get(target, prop, receiver);\n },\n });\n return requestContextAls.run(scoped, fn);\n}\n\n/**\n * The main route's coerced params, or an empty record before route matching.\n *\n * Non-throwing, unlike `getSegmentParams()`: the deny payload path publishes\n * params from wherever the pipeline happened to fail, which may be before\n * matching completed.\n *\n * @internal — framework use only. See TIM-1285.\n */\nexport function getSegmentParamsForClient(): CoercedParams {\n return requestContextAls.getStore()?.segmentParams ?? {};\n}\n\n/**\n * The per-slot params to publish to the client, or `undefined` when the\n * request rendered no slot with params of its own.\n *\n * The client cannot derive these: a slot matches the URL through its own\n * sub-tree, and the browser only ever sees the main route's record. Reading\n * the map here — rather than threading it back out of the element builder —\n * keeps one producer, since `setSlotParams` already writes it during slot\n * resolution and every serialization site runs after that completes.\n *\n * @internal — framework use only. See TIM-1285.\n */\nexport function getSlotParamsForClient(): SlotParamsRecord | undefined {\n const store = requestContextAls.getStore();\n if (!store?.slotParams) return undefined;\n return Object.keys(store.slotParams).length > 0 ? store.slotParams : undefined;\n}\n\n/**\n * Store the matched segment path (e.g. '/(browse)/[artistSlug]/[year]') for\n * dev-mode validation in getSegmentParams().\n *\n * @internal — framework use only\n */\nexport function setMatchedSegmentPath(segmentPath: string): void {\n const store = requestContextAls.getStore();\n if (!store) return; // non-throwing — optional diagnostic\n store.matchedSegmentPath = segmentPath;\n}\n\n/**\n * Extract dynamic segment names from a route pattern.\n * '/[artistSlug]/[year]' → ['artistSlug', 'year']\n * '/docs/[...slug]' → ['slug']\n */\nfunction extractDynamicSegments(route: string): string[] {\n const segments: string[] = [];\n // Match [paramName], [...paramName], [[...paramName]]\n const re = /\\[{1,2}\\.{0,3}(\\w+)\\]{1,2}/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(route)) !== null) {\n segments.push(m[1]);\n }\n return segments;\n}\n\n/**\n * Returns the raw search string from the current request URL (e.g. \"?foo=bar\").\n * Synchronous — safe for use in `redirect()` which throws synchronously.\n *\n * Returns empty string if called outside a request context (non-throwing for\n * use in redirect's optional preserveSearchParams path).\n *\n * @internal — used by redirect() for preserveSearchParams support.\n */\nexport function getRequestSearchString(): string {\n const store = requestContextAls.getStore();\n return store?.searchString ?? '';\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\n/**\n * Read-only Headers interface. The standard Headers class is mutable;\n * this type narrows it to read-only methods. The underlying object is\n * still a Headers instance, but user code should not mutate it.\n */\nexport type ReadonlyHeaders = Pick<\n Headers,\n 'get' | 'has' | 'entries' | 'keys' | 'values' | 'forEach' | typeof Symbol.iterator\n>;\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/** Options for `runWithRequestContext()`. */\nexport interface RequestContextOptions {\n /**\n * Resume this `React.cache` scope instead of opening a fresh one. Only the\n * post-action revalidation passes it.\n */\n reactCacheScope?: ReactCacheScope;\n /**\n * The request's cookies, already parsed. Re-entry callers (the no-JS form\n * rerender and the revalidation middleware) pass the action's\n * post-mutation cookie state here instead of round-tripping it through a\n * `Cookie:` header — the same parse the H-3 smuggling primitive abused\n * (TIM-868).\n */\n cookies?: Map<string, string>;\n /**\n * The form state of the no-JS action this render answers. Only the\n * action dispatcher's re-entry passes it; see `RequestContextStore.formState`.\n */\n formState?: ReactFormState;\n /**\n * The error of the no-JS action this render answers. Only the action\n * dispatcher's re-entry passes it; see `RequestContextStore.actionError`.\n */\n actionError?: ActionErrorForRender;\n}\n\n/**\n * Run a callback within a request context. Used by the pipeline to establish\n * per-request ALS scope so that `getHeaders()` and `getCookies()` work.\n *\n * When `options.cookies` is given, the context's `parsedCookies` map is a\n * copy of it and the raw `cookieHeader` is left empty, so\n * `parseCookieHeader` never runs for that request. Re-entry callers pass the\n * action's post-mutation cookie state this way (TIM-868).\n *\n * Each request context also opens its own `React.cache` scope, shared by\n * everything that runs inside it — proxy.ts, middleware.ts, route handlers,\n * and the render (design/02-rendering-pipeline.md §\"Cache Scoping Model\").\n *\n * @param req - The incoming Request object.\n * @param fn - The function to run within the request context.\n */\nexport function runWithRequestContext<T>(\n req: Request,\n fn: () => T,\n { reactCacheScope, cookies, formState, actionError }: RequestContextOptions = {}\n): T {\n const originalCopy = new Headers(req.headers);\n const appVisible = appVisibleSearch(new URL(req.url));\n // A copy: the caller keeps its map, and this request's writes stay here.\n const seed = cookies ? new Map(cookies) : undefined;\n const store: RequestContextStore = {\n request: req,\n headers: freezeHeaders(req.headers),\n originalHeaders: originalCopy,\n // Given parsed cookies, leave the raw header empty — parseCookieHeader is the\n // exact code path the smuggling primitive abused, and lazy parsing is\n // gated on `parsedCookies` being undefined.\n cookieHeader: seed ? '' : (req.headers.get('cookie') ?? ''),\n parsedCookies: seed,\n // Not `parsedUrl.searchParams` / `parsedUrl.search`: this store is what\n // `getSearchParams()` and `redirect({ preserveSearchParams })` read, and\n // the RSC payload URL carries the framework's `_rsc` cache key that no\n // other request has (TIM-1272).\n searchParams: appVisible.params,\n searchString: appVisible.search,\n cookieJar: new Map(),\n flushed: false,\n mutableContext: false,\n formState,\n actionError,\n };\n return requestContextAls.run(store, () => runWithReactCacheScope(fn, reactCacheScope));\n}\n\n/**\n * Enable cookie mutation for the current context. Called by the framework\n * when entering middleware.ts, server actions, or route.ts handlers.\n *\n * See design/29-cookies.md §\"Context Tracking\"\n */\nexport function setMutableCookieContext(mutable: boolean): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.mutableContext = mutable;\n }\n}\n\n/**\n * Mark the response as flushed (headers committed). After this point,\n * cookie mutations log a warning instead of throwing.\n *\n * See design/29-cookies.md §\"Streaming Constraint: Post-Flush Cookie Warning\"\n */\nexport function markResponseFlushed(): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.flushed = true;\n }\n}\n\n/**\n * Apply middleware-injected request headers to the current request context.\n *\n * Called by the pipeline after middleware.ts runs. Merges overlay headers\n * on top of the original request headers so downstream code (access.ts,\n * server components, server actions) sees them via `getHeaders()`.\n *\n * The original request headers are never mutated — a new frozen Headers\n * object is created with the overlay applied on top.\n *\n * See design/07-routing.md §\"Request Header Injection\"\n */\nexport function applyRequestHeaderOverlay(overlay: Headers): void {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] applyRequestHeaderOverlay() called outside of a request context.');\n }\n\n // Check if the overlay has any headers — skip if empty\n let hasOverlay = false;\n overlay.forEach(() => {\n hasOverlay = true;\n });\n if (!hasOverlay) return;\n\n // Merge: start with original headers, overlay on top\n const merged = new Headers(store.originalHeaders);\n overlay.forEach((value, key) => {\n merged.set(key, value);\n });\n store.headers = freezeHeaders(merged);\n}\n\n// ─── Read-Only Headers ────────────────────────────────────────────────────\n\nconst MUTATING_METHODS = new Set(['set', 'append', 'delete']);\n\n/**\n * Wrap a Headers object in a Proxy that throws on mutating methods.\n * Object.freeze doesn't work on Headers (native internal slots), so we\n * intercept property access and reject set/append/delete at runtime.\n *\n * Read methods (get, has, entries, etc.) must be bound to the underlying\n * Headers instance because they access private #headersList slots.\n */\nfunction freezeHeaders(source: Headers): Headers {\n const copy = new Headers(source);\n return new Proxy(copy, {\n get(target, prop) {\n if (typeof prop === 'string' && MUTATING_METHODS.has(prop)) {\n return () => {\n throw new Error(\n `[timber] getHeaders() returns a read-only Headers object. ` +\n `Calling .${prop}() is not allowed. ` +\n `Use ctx.requestHeaders in middleware to inject headers for downstream components.`\n );\n };\n }\n const value = Reflect.get(target, prop);\n // Bind methods to the real Headers instance so private slot access works\n if (typeof value === 'function') {\n return value.bind(target);\n }\n return value;\n },\n });\n}\n\n/**\n * The form state of the no-JS action this render answers, or `null`. Read by\n * the SSR renderer only (`rsc-entry/ssr-renderer.ts`), which passes it to\n * Fizz and embeds it for `hydrateRoot`. Framework-internal: not exported\n * from `@timber-js/app/server`.\n */\nexport function getFormStateForSsr(): ReactFormState | null {\n return requestContextAls.getStore()?.formState ?? null;\n}\n\n/**\n * The error of the no-JS action this render answers, or `null`. Read by the\n * route element builder, which renders it in place of the page, and by the\n * Flight `onError`, which gives it the ID the action logged it with.\n * Framework-internal: not exported from `@timber-js/app/server`.\n */\nexport function getActionErrorForRender(): ActionErrorForRender | null {\n return requestContextAls.getStore()?.actionError ?? null;\n}\n","/**\n * Cookie parsing and serialization helpers — pure string ↔ structure\n * functions with no ALS dependency. Split out of `cookie-context.ts`\n * (TIM-853) so the API surface and the wire-format codecs can each be\n * read on their own.\n *\n * The functions in this module are total over arbitrary input. They\n * never throw and never call `assertValid*` (the security validators\n * live in the API surface — `cookie-context.ts` invokes them at every\n * jar entry point so the smuggling-primitive invariant from TIM-868\n * is enforced regardless of which path produced the bytes).\n *\n * Delegates to the `cookie` package (RFC 6265, dependency-free,\n * browser-safe) for wire codecs. Adapts at the boundary to preserve\n * timber's Map-based API and never-throw contract.\n */\n\nimport { parseCookie, parseSetCookie as upstreamParseSetCookie, stringifySetCookie } from 'cookie';\n\nimport type { CookieEntry } from './als-registry.ts';\nimport type { CookieOptions } from './cookie-context.ts';\n\n/**\n * Parse a Cookie header string into a Map of name → value pairs.\n * Follows RFC 6265 §4.2.1: cookies are semicolon-separated key=value pairs.\n *\n * Values are auto-decoded with `decodeURIComponent` so they round-trip\n * losslessly with `getCookies().set()` (which auto-encodes). Malformed\n * `%`-escapes from third-party cookies fall back to the raw byte sequence\n * — the parser must be total over arbitrary inbound headers, including\n * non-conforming values from other servers, browser extensions, etc.\n */\nexport function parseCookieHeader(header: string): Map<string, string> {\n const map = new Map<string, string>();\n if (!header) return map;\n\n // cookie.parseCookie returns a plain object with first-wins semantics\n // and uses safeDecodeURIComponent by default (try/catch around\n // decodeURIComponent, falls back to raw). This matches our contract.\n const parsed = parseCookie(header);\n for (const name in parsed) {\n const value = parsed[name];\n if (value !== undefined) map.set(name, value);\n }\n\n return map;\n}\n\n/**\n * Decode a single cookie value with `decodeURIComponent`, falling back to\n * the raw byte sequence if the input contains a malformed `%`-escape.\n *\n * Used by both `parseCookieHeader` (incoming Cookie: header) and the\n * `setRaw` forwarding path (outgoing Set-Cookie from upstream services).\n * Total — never throws.\n *\n * cookie's internal decode has identical semantics but is not exported,\n * so we keep this standalone helper.\n */\nexport function safeDecodeCookieValue(raw: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Serialize a CookieEntry into a Set-Cookie header value.\n *\n * Total — never throws. If the cookie package's serializer rejects\n * the input (e.g. non-integer maxAge, forbidden chars in name/value),\n * falls back to a minimal hand-rolled serialization so the response\n * path is never interrupted by a codec error.\n */\nexport function serializeCookieEntry(entry: CookieEntry): string {\n try {\n return stringifySetCookie(\n {\n name: entry.name,\n value: entry.value,\n domain: entry.options.domain,\n path: entry.options.path,\n expires: entry.options.expires,\n maxAge: entry.options.maxAge,\n httpOnly: entry.options.httpOnly,\n secure: entry.options.secure,\n sameSite: entry.options.sameSite,\n partitioned: entry.options.partitioned,\n },\n // timber pre-encodes values at the API surface (set() calls\n // encodeURIComponent), so pass identity to avoid double-encoding.\n { encode: (v: string) => v }\n );\n } catch {\n // Fallback: hand-roll a minimal Set-Cookie so we never drop the\n // header entirely. The entry-point validators in cookie-context.ts\n // already reject truly dangerous input — this path only fires for\n // edge-case formatting the upstream serializer is strict about.\n return serializeCookieEntryFallback(entry);\n }\n}\n\nfunction serializeCookieEntryFallback(entry: CookieEntry): string {\n const parts = [`${entry.name}=${entry.value}`];\n const opts = entry.options;\n\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.path) parts.push(`Path=${opts.path}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.httpOnly) parts.push('HttpOnly');\n if (opts.secure) parts.push('Secure');\n if (opts.sameSite) {\n parts.push(`SameSite=${opts.sameSite.charAt(0).toUpperCase()}${opts.sameSite.slice(1)}`);\n }\n if (opts.partitioned) parts.push('Partitioned');\n\n return parts.join('; ');\n}\n\n/**\n * Parse a raw `Set-Cookie` header string into name, value, and options.\n * Handles all standard attributes: Path, Domain, Max-Age, Expires,\n * SameSite, Secure, HttpOnly, Partitioned.\n *\n * Does NOT apply DEFAULT_COOKIE_OPTIONS — the caller decides whether\n * to merge defaults (e.g. `set()` does, but `setRaw()` should preserve\n * the original header's intent).\n */\nexport function parseSetCookie(\n header: string\n): { name: string; value: string; options: CookieOptions } | null {\n const parsed = upstreamParseSetCookie(header, {\n // Don't decode — setRaw expects the wire-form value so it can\n // re-emit verbatim and validate against cookie-octet.\n decode: (v: string) => v,\n });\n\n // cookie.parseSetCookie always returns a SetCookie object, but\n // with name/value as empty strings when the header has no `=`.\n if (!parsed.name) return null;\n\n const options: CookieOptions = {};\n\n if (parsed.path !== undefined) options.path = parsed.path || '/';\n if (parsed.domain !== undefined) options.domain = parsed.domain;\n if (parsed.maxAge !== undefined && Number.isFinite(parsed.maxAge)) {\n options.maxAge = parsed.maxAge;\n }\n if (parsed.expires !== undefined) options.expires = parsed.expires;\n if (parsed.sameSite !== undefined && parsed.sameSite !== true) {\n const sameSite = (parsed.sameSite as string).toLowerCase();\n if (sameSite === 'strict' || sameSite === 'lax' || sameSite === 'none') {\n options.sameSite = sameSite;\n }\n }\n if (parsed.secure) options.secure = true;\n if (parsed.httpOnly) options.httpOnly = true;\n if (parsed.partitioned) options.partitioned = true;\n\n return { name: parsed.name, value: parsed.value ?? '', options };\n}\n","/**\n * Cookie Context — per-request cookie API and on-the-wire helpers.\n *\n * Split out of `request-context.ts` (TIM-853) so the cookie subsystem\n * — encoding contract, options grammar, parser, serializer, and RYW map\n * — lives in one file. The headers/scope/params APIs\n * stay in `request-context.ts` and call into this module via the\n * exported helpers.\n *\n * See design/29-cookies.md for the encoding contract and read-your-own-\n * writes semantics. See ONGOING_SECURITY.md H-3 (TIM-868) for the\n * smuggling primitive that the encoding contract closes.\n */\n\nimport { requestContextAls, type RequestContextStore } from './als-registry.ts';\nimport { isDebug } from './debug.ts';\nimport {\n ANY_COOKIE,\n noteCookieReadForReactCache,\n warnIfCachedCookieIsWritten,\n} from './react-cache-scope.ts';\nimport {\n assertValidCookieName,\n assertValidCookieValue,\n assertValidCookieOptions,\n} from '../cookies/validation.ts';\nimport {\n parseCookieHeader,\n parseSetCookie,\n safeDecodeCookieValue,\n serializeCookieEntry,\n} from './cookie-parsing.ts';\n\n// Re-export the validators so framework-internal consumers and tests can\n// import the canonical implementation from the same module that hosts\n// `getCookies()`. The pure shared module lives in `cookies/validation.ts`\n// so the client `useCookie` hook can use the same checks without pulling\n// in the server ALS code.\nexport { assertValidCookieName, assertValidCookieValue, assertValidCookieOptions };\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns a cookie accessor for the current request.\n *\n * Available in middleware, access checks, server components, and server actions.\n * Throws if called outside a request context (security principle #2: no global fallback).\n *\n * Read methods (.get, .has, .getAll) are always available and reflect\n * read-your-own-writes from .set() calls in the same request.\n *\n * Mutation methods (.set, .delete, .clear) are only available in mutable\n * contexts (middleware.ts, server actions, route.ts handlers). Calling them\n * in read-only contexts (access.ts, server components) throws.\n *\n * This is the escape hatch for direct cookie jar operations. For typed\n * cookie access, use `defineCookie()` instead.\n *\n * See design/29-cookies.md\n */\nexport function getCookieJar(): RequestCookies {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getCookieJar() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n\n // Parse cookies lazily on first access\n if (!store.parsedCookies) {\n store.parsedCookies = parseCookieHeader(store.cookieHeader);\n }\n\n const map = store.parsedCookies;\n return {\n // Reads are noted (dev only) so a later write of the same cookie can\n // warn if React.cache may have memoized the old value (TIM-1529 D3).\n get(name: string): string | undefined {\n noteCookieReadForReactCache(name);\n return map.get(name);\n },\n has(name: string): boolean {\n noteCookieReadForReactCache(name);\n return map.has(name);\n },\n getAll(): Array<{ name: string; value: string }> {\n noteCookieReadForReactCache(ANY_COOKIE);\n return Array.from(map.entries()).map(([name, value]) => ({ name, value }));\n },\n get size(): number {\n noteCookieReadForReactCache(ANY_COOKIE);\n return map.size;\n },\n\n set(name: string, value: string, options?: SetCookieOptions): void {\n assertMutable(store, 'set');\n // Validate the name first — names cannot be URL-encoded (RFC 7230\n // token grammar is strict), so a bad name is always a bug.\n assertValidCookieName(name);\n // Type guard with a guiding error. The single most common mistake\n // is passing a non-string (object, number, Date) and expecting the\n // framework to JSON-encode. Point developers at jsonCookieCodec.\n if (typeof value !== 'string') {\n throw new Error(\n `[timber] getCookieJar().set(${JSON.stringify(name)}, …): value must be a string, got ${typeof value}.\\n` +\n ` To store a JSON-serializable value, use defineCookie + jsonCookieCodec:\\n` +\n `\\n` +\n ` import { defineCookie, jsonCookieCodec } from '@timber-js/app/cookies';\\n` +\n `\\n` +\n ` export const ${name}Cookie = defineCookie(${JSON.stringify(name)}, {\\n` +\n ` codec: jsonCookieCodec(),\\n` +\n ` });\\n` +\n `\\n` +\n ` ${name}Cookie.set(value);\\n` +\n `\\n` +\n ` See design/29-cookies.md §\"Typed Cookies with Schema Validation\".`\n );\n }\n // Encode the value so the on-the-wire bytes always satisfy\n // RFC 6265 §4.1.1 cookie-octet. encodeURIComponent's output is a\n // strict subset of cookie-octet (only `A-Z a-z 0-9 ! ' ( ) * - . _\n // ~ %`), so the encoded form can never carry the H-3 smuggling\n // primitive — `;` becomes `%3B`, CR/LF become `%0D`/`%0A`, etc.\n // The round-trip is lossless: parseCookieHeader auto-decodes on\n // read, so `cookies().get(name)` returns exactly `value`. See\n // ONGOING_SECURITY.md H-3 (TIM-868) and design/29-cookies.md\n // §\"Encoding Contract\".\n //\n // The `{ raw: true }` opt-out skips the encoder for callers who\n // need exact byte control (e.g. forwarding pre-encoded cookies\n // from an upstream service). The opt-out goes through the strict\n // cookie-octet validator instead — the smuggling primitive cannot\n // sneak in via the escape hatch.\n const raw = options?.raw === true;\n const wireValue = raw ? value : encodeURIComponent(value);\n if (raw) {\n assertValidCookieValue(name, wireValue);\n }\n if (store.flushed) {\n if (isDebug()) {\n console.warn(\n `[timber] warn: getCookieJar().set('${name}') called after response headers were committed.\\n` +\n ` The cookie will NOT be sent. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n }\n return;\n }\n // Strip the framework-only `raw` flag before persisting — it is\n // not an HTTP cookie attribute and must not leak into the jar.\n const { raw: _raw, ...attributeOptions } = options ?? {};\n void _raw;\n const opts = { ...DEFAULT_COOKIE_OPTIONS, ...attributeOptions };\n assertValidCookieOptions(opts);\n store.cookieJar.set(name, { name, value: wireValue, options: opts });\n // Read-your-own-writes: store the DECODED logical value so that\n // subsequent `cookies().get(name)` in the same request returns\n // exactly what the developer wrote — never the encoded form.\n // For `{ raw: true }`, the wire form IS the logical form.\n map.set(name, raw ? wireValue : value);\n warnIfCachedCookieIsWritten(name);\n },\n\n setFromHeaders(headers: Headers): void {\n assertMutable(store, 'setFromHeaders');\n if (store.flushed) {\n console.warn(\n `[timber] warn: getCookieJar().setFromHeaders() called after response headers were committed.\\n` +\n ` The cookies will NOT be sent. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n return;\n }\n // Headers.getSetCookie() returns individual Set-Cookie strings,\n // avoiding the fragile comma-splitting that raw .get() requires.\n for (const raw of headers.getSetCookie()) {\n const parsed = parseSetCookie(raw);\n if (parsed) {\n // Use setRaw to preserve the original header's attributes without\n // merging DEFAULT_COOKIE_OPTIONS (parseSetCookie intentionally\n // does not apply defaults — see its doc comment).\n setRaw(store, map, parsed.name, parsed.value, parsed.options);\n }\n }\n },\n\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void {\n assertMutable(store, 'delete');\n // Validate the name even though delete writes an empty value — a\n // smuggled name (`;` or CR/LF) would corrupt the Set-Cookie header\n // we emit. See ONGOING_SECURITY.md H-3 (TIM-868).\n assertValidCookieName(name);\n if (store.flushed) {\n if (isDebug()) {\n console.warn(\n `[timber] warn: getCookieJar().delete('${name}') called after response headers were committed.\\n` +\n ` The cookie will NOT be deleted. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n }\n return;\n }\n const opts: CookieOptions = {\n ...DEFAULT_COOKIE_OPTIONS,\n ...options,\n maxAge: 0,\n expires: new Date(0),\n };\n // delete() is a jar entry point like set() — caller-supplied\n // path/domain are serialized verbatim into the Set-Cookie header,\n // so they must pass the same attribute-injection guard. See\n // design/29-cookies.md §\"Cookie Attribute Validation\" (TIM-1026).\n assertValidCookieOptions(opts);\n store.cookieJar.set(name, { name, value: '', options: opts });\n // Remove from read view\n map.delete(name);\n warnIfCachedCookieIsWritten(name);\n },\n\n clear(): void {\n assertMutable(store, 'clear');\n if (store.flushed) return;\n // Delete every incoming cookie\n for (const name of Array.from(map.keys())) {\n store.cookieJar.set(name, {\n name,\n value: '',\n options: { ...DEFAULT_COOKIE_OPTIONS, maxAge: 0, expires: new Date(0) },\n });\n }\n map.clear();\n warnIfCachedCookieIsWritten(ANY_COOKIE);\n },\n\n toString(): string {\n noteCookieReadForReactCache(ANY_COOKIE);\n // Re-encode values when serializing as a Cookie header — the\n // RYW map holds decoded logical values, but a Cookie header has\n // to satisfy `cookie-octet`. Mirror the auto-encode contract on\n // `set()` so toString() round-trips losslessly with parseCookieHeader.\n return Array.from(map.entries())\n .map(([name, value]) => `${name}=${encodeURIComponent(value)}`)\n .join('; ');\n },\n };\n}\n\n/**\n * Returns the value of a single cookie, or undefined if absent.\n *\n * @internal — not part of the public API. Use `defineCookie().get()` or `getCookieJar().get()` instead.\n */\nexport function getCookie(name: string): string | undefined {\n const jar = getCookieJar();\n return jar.get(name);\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\n/**\n * Per-call options for `getCookies().set()`. Extends the persistent\n * `CookieOptions` (HTTP cookie attributes) with framework-only flags\n * that are NOT serialized into the Set-Cookie header.\n *\n * The `raw` flag is the escape hatch for the auto-encoding contract.\n * See design/29-cookies.md §\"Encoding Contract\".\n */\nexport interface SetCookieOptions extends CookieOptions {\n /**\n * Skip the framework's `encodeURIComponent` pass and store the value\n * verbatim. The value is then validated against the strict RFC 6265\n * §4.1.1 `cookie-octet` grammar — the H-3 smuggling primitive cannot\n * sneak in via this opt-out.\n *\n * Use this when forwarding a cookie value that is already in its\n * intended on-the-wire form (e.g. mirroring an upstream service's\n * Set-Cookie). Default: `false` (auto-encode).\n */\n raw?: boolean;\n}\n\n/** Options for setting a cookie. See design/29-cookies.md. */\nexport interface CookieOptions {\n /** Domain scope. Default: omitted (current domain only). */\n domain?: string;\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Expiration date. Mutually exclusive with maxAge. */\n expires?: Date;\n /** Max age in seconds. Mutually exclusive with expires. */\n maxAge?: number;\n /** Prevent client-side JS access. Default: true. */\n httpOnly?: boolean;\n /** Only send over HTTPS. Default: true. */\n secure?: boolean;\n /** Cross-site request policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Partitioned (CHIPS) — isolate cookie per top-level site. Default: false. */\n partitioned?: boolean;\n}\n\n/**\n * Cookie accessor returned by `getCookies()`.\n *\n * Read methods are always available. Mutation methods throw in read-only\n * contexts (access.ts, server components).\n */\nexport interface RequestCookies {\n /** Get a cookie value by name. Returns undefined if not present. */\n get(name: string): string | undefined;\n /** Check if a cookie exists. */\n has(name: string): boolean;\n /** Get all cookies as an array of { name, value } pairs. */\n getAll(): Array<{ name: string; value: string }>;\n /** Number of cookies. */\n readonly size: number;\n /**\n * Set a cookie. Only available in mutable contexts (middleware, actions,\n * route handlers).\n *\n * The value is auto-encoded with `encodeURIComponent` so the on-the-wire\n * bytes always satisfy RFC 6265 §4.1.1 cookie-octet — `cookies().get()`\n * returns the same logical value the developer wrote. Pass `{ raw: true }`\n * to skip the encoder; the raw path validates against the strict\n * cookie-octet grammar instead. See design/29-cookies.md §\"Encoding\n * Contract\" and ONGOING_SECURITY.md H-3 (TIM-868).\n */\n set(name: string, value: string, options?: SetCookieOptions): void;\n /**\n * Copy all `Set-Cookie` headers from a `Headers` object.\n * Parses each header and forwards name, value, and all attributes\n * (path, domain, max-age, expires, sameSite, secure, httpOnly, partitioned).\n *\n * Useful when forwarding cookies from an internal `fetch()` or auth handler:\n * ```ts\n * const response = await auth.handler(req);\n * getCookies().then(c => c.setFromHeaders(response.headers));\n * ```\n */\n setFromHeaders(headers: Headers): void;\n /** Delete a cookie. Only available in mutable contexts. */\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void;\n /** Delete all cookies. Only available in mutable contexts. */\n clear(): void;\n /** Serialize cookies as a Cookie header string. */\n toString(): string;\n}\n\nconst DEFAULT_COOKIE_OPTIONS: CookieOptions = {\n path: '/',\n httpOnly: true,\n secure: true,\n sameSite: 'lax',\n};\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Build a Map of cookie name → value reflecting the current request's\n * read-your-own-writes state. Includes incoming cookies plus any\n * mutations from getCookies().set() / getCookies().delete() in the same request.\n *\n * Used by SSR renderers to populate NavContext.cookies so that\n * useCookie()'s server snapshot matches the actual response state.\n *\n * See design/29-cookies.md §\"Read-Your-Own-Writes\"\n * See design/triage/TIM-441-cookie-api-triage.md §4\n */\nexport function getCookiesForSsr(): Map<string, string> {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] getCookiesForSsr() called outside of a request context.');\n }\n\n // Trigger lazy parsing if not yet done\n if (!store.parsedCookies) {\n store.parsedCookies = parseCookieHeader(store.cookieHeader);\n }\n\n // The parsedCookies map already reflects read-your-own-writes:\n // - getCookies().set() updates the map via map.set(name, value)\n // - getCookies().delete() removes from the map via map.delete(name)\n // Return a copy so callers can't mutate the internal map.\n return new Map(store.parsedCookies);\n}\n\n/**\n * Collect all Set-Cookie headers from the cookie jar.\n * Called by the framework at flush time to apply cookies to the response.\n *\n * Returns an array of serialized Set-Cookie header values.\n */\nexport function getSetCookieHeaders(): string[] {\n const store = requestContextAls.getStore();\n if (!store) return [];\n return Array.from(store.cookieJar.values()).map(serializeCookieEntry);\n}\n\n// ─── Cookie Helpers ───────────────────────────────────────────────────────\n\n/** Throw if cookie mutation is attempted in a read-only context. */\nfunction assertMutable(store: RequestContextStore, method: string): void {\n if (!store.mutableContext) {\n throw new Error(\n `[timber] getCookieJar().${method}() cannot be called in this context.\\n` +\n ` Set cookies in middleware.ts, server actions, or route.ts handlers.`\n );\n }\n}\n\n/**\n * Write a cookie to the jar WITHOUT merging DEFAULT_COOKIE_OPTIONS.\n * Used by setFromHeaders to preserve the original header's attributes exactly.\n *\n * For deletion cookies (maxAge=0), the jar entry is still created so the\n * Set-Cookie header is emitted, but the cookie is NOT added to the read map\n * (it would be misleading — the cookie is being deleted).\n */\nfunction setRaw(\n store: RequestContextStore,\n readMap: Map<string, string>,\n name: string,\n value: string,\n options: CookieOptions\n): void {\n // setRaw is the forwarding path for upstream Set-Cookie headers\n // (`getCookies().setFromHeaders(response.headers)`). The value comes\n // out of `parseSetCookie` in its on-the-wire form — already encoded\n // by whoever produced it — so we re-emit it verbatim into our jar.\n // We DO validate against the strict cookie-octet grammar, both as\n // defense-in-depth against a malicious upstream and to keep the\n // ONGOING_SECURITY.md H-3 invariant intact at every entry point into\n // the cookie jar / RYW map.\n assertValidCookieName(name);\n assertValidCookieValue(name, value);\n // Options get the same entry-point guard as set()/delete(). For the\n // setFromHeaders path this can only fire on attributes parseSetCookie\n // cannot produce (it splits on `;` and normalizes Max-Age/SameSite),\n // but setRaw is the jar entry point — the invariant lives here, not in\n // the caller. See design/29-cookies.md §\"Cookie Attribute Validation\".\n assertValidCookieOptions(options);\n store.cookieJar.set(name, { name, value, options });\n warnIfCachedCookieIsWritten(name);\n // Deletion cookies (Max-Age=0) should not appear in the read map.\n if (options.maxAge === 0) {\n readMap.delete(name);\n } else {\n // Decode for the RYW map so consumers see the same logical bytes\n // they would see if the upstream cookie had arrived on the next\n // request. Mirrors `parseCookieHeader`'s auto-decode.\n readMap.set(name, safeDecodeCookieValue(value));\n }\n}\n","// Server-side primitives: deny, redirect, redirectExternal, waitUntil\n//\n// These are the core runtime signals that components, middleware, and access gates\n// use to control request flow. See design/10-error-handling.md.\n\nimport type { JsonSerializable } from './types.ts';\nimport { getWaitUntil as _getWaitUntil } from './waituntil-bridge.ts';\nimport { isDebug } from './debug.ts';\nimport { getRequestSearchString } from './request-context.ts';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.ts';\nimport { DENY_BRAND, REDIRECT_BRAND } from './signal-identity.ts';\nimport {\n assertRelativeRedirectPath,\n validateExternalRedirectUrl,\n} from '../shared/href-validation.ts';\n\n// ─── Dev-mode validation ────────────────────────────────────────────────────\n\n/**\n * Check if a value is JSON-serializable without data loss.\n * Returns a description of the first non-serializable value found, or null if OK.\n *\n * @internal Exported for testing only.\n */\nexport function findNonSerializable(value: unknown, path = 'data'): string | null {\n if (value === null || value === undefined) return null;\n\n switch (typeof value) {\n case 'string':\n case 'number':\n case 'boolean':\n return null;\n case 'bigint':\n return `${path} contains a BigInt — BigInt throws in JSON.stringify`;\n case 'function':\n return `${path} is a function — functions are not JSON-serializable`;\n case 'symbol':\n return `${path} is a symbol — symbols are not JSON-serializable`;\n case 'object':\n break;\n default:\n return `${path} has unsupported type \"${typeof value}\"`;\n }\n\n if (value instanceof Date) {\n return `${path} is a Date — Dates silently coerce to strings in JSON.stringify`;\n }\n if (value instanceof Map) {\n return `${path} is a Map — Maps serialize as {} in JSON.stringify (data loss)`;\n }\n if (value instanceof Set) {\n return `${path} is a Set — Sets serialize as {} in JSON.stringify (data loss)`;\n }\n if (value instanceof RegExp) {\n return `${path} is a RegExp — RegExps serialize as {} in JSON.stringify`;\n }\n if (value instanceof Error) {\n return `${path} is an Error — Errors serialize as {} in JSON.stringify`;\n }\n\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const result = findNonSerializable(value[i], `${path}[${i}]`);\n if (result) return result;\n }\n return null;\n }\n\n // Plain object — only Object.prototype is safe. Null-prototype objects\n // (Object.create(null)) survive JSON.stringify but React Flight rejects\n // them with \"Classes or null prototypes are not supported\", so the\n // pre-flush deny path (renderDenyPage → renderToReadableStream) would throw.\n const proto = Object.getPrototypeOf(value);\n if (proto === null) {\n return `${path} is a null-prototype object — React Flight rejects null prototypes`;\n }\n if (proto !== Object.prototype) {\n const name = (value as object).constructor?.name ?? 'unknown';\n return `${path} is a ${name} instance — class instances may lose data in JSON.stringify`;\n }\n\n for (const key of Object.keys(value as Record<string, unknown>)) {\n const result = findNonSerializable((value as Record<string, unknown>)[key], `${path}.${key}`);\n if (result) return result;\n }\n return null;\n}\n\n/**\n * Emit a dev-mode warning if data is not JSON-serializable.\n * No-op in production.\n */\nfunction warnIfNotSerializable(data: unknown, callerName: string): void {\n if (!isDebug()) return;\n if (data === undefined) return;\n\n const issue = findNonSerializable(data);\n if (issue) {\n console.warn(\n `[timber] ${callerName}: ${issue}. ` +\n 'Data passed to deny() must be JSON-serializable because ' +\n 'the post-flush path uses JSON.stringify, not React Flight.'\n );\n }\n}\n\n// ─── DenySignal ─────────────────────────────────────────────────────────────\n\n/**\n * Render-phase signal thrown by `deny()`. Caught by the framework to produce\n * the correct HTTP status code (segment context) or graceful degradation (slot context).\n *\n * Detect with `isDenySignal()`, never `instanceof` — see \"Signal branding\" in signal-identity.ts.\n */\nexport class DenySignal extends Error {\n readonly [DENY_BRAND] = true;\n readonly status: number;\n readonly data: JsonSerializable | undefined;\n\n /**\n * Ordered list of segment keys that own matching deny pages for this\n * signal, best match first. Present = addressed (a boundary should\n * render it); absent = unplaced (the re-render fallback serves it).\n *\n * In-tree hoists stamp a single-element list (`[entry.ownerKey]`) —\n * identical to the pre-TIM-1450 `ownerKey`. Late addressing (TIM-1450)\n * stamps the full candidate list so the client can find the first\n * *reachable* owner via its ancestry context, eliminating the\n * owner-below-throw-site escape.\n *\n * See design/04-authorization.md §\"Where a Deny Page Renders\", TIM-1356.\n */\n owners?: string[];\n\n /**\n * When true, the pipeline must respond with `rscErrorEnvelope` so the\n * client hard-navigates instead of attempting to render a deny page\n * from the RSC payload. Set by AccessGate when a skipped segment\n * denies — rendering the deny page in-tree would appear inside the\n * client's cached layout chrome (TIM-1074). See TIM-1363.\n */\n _hardNavigate?: boolean;\n\n constructor(status: number, data?: JsonSerializable) {\n super(`Access denied with status ${status}`);\n this.name = 'DenySignal';\n this.status = status;\n this.data = data;\n }\n\n /**\n * Extract the file that called deny() from the stack trace.\n * Returns a short path (e.g. \"app/auth/access.ts\") or undefined if\n * the stack can't be parsed. Dev-only — used for dev log output.\n */\n get sourceFile(): string | undefined {\n if (!this.stack) return undefined;\n const frames = this.stack.split('\\n');\n // Skip the Error line and the deny() frame — the caller is the 3rd line.\n // Stack format: \" at FnName (file:line:col)\" or \" at file:line:col\"\n for (let i = 2; i < frames.length; i++) {\n const frame = frames[i];\n if (!frame) continue;\n // Skip framework internals\n if (frame.includes('primitives.ts') || frame.includes('node_modules')) continue;\n // Extract file path from the frame\n const match =\n frame.match(/\\(([^)]+?)(?::\\d+:\\d+)\\)/) ?? frame.match(/at\\s+([^\\s]+?)(?::\\d+:\\d+)/);\n if (match?.[1]) {\n // Shorten to app-relative path\n const full = match[1];\n const appIdx = full.indexOf('/app/');\n return appIdx >= 0 ? full.slice(appIdx + 1) : full;\n }\n }\n return undefined;\n }\n}\n\n/** Options for deny(). */\nexport interface DenyOptions {\n /** Human-readable message (logged server-side, not sent to client). */\n message?: string;\n /**\n * JSON-serializable data forwarded as the `data` prop to status-code files\n * and `denied.tsx`. Named `dangerouslyPassData` because this data crosses\n * the RSC → client serialization boundary — do not pass sensitive server\n * state (tokens, internal IDs, database rows).\n */\n dangerouslyPassData?: JsonSerializable;\n}\n\n/**\n * Universal denial/error primitive. Throws a `DenySignal` that the framework catches.\n *\n * - In segment context (outside Suspense): produces HTTP status code\n * - In slot context: graceful degradation → denied.tsx → default.tsx → null\n * - Inside Suspense (hold window): promoted to pre-flush behavior\n * - Inside Suspense (after flush): error boundary + noindex meta\n *\n * ```ts\n * deny() // 403 (default)\n * deny(404) // 404\n * deny(503, { message: 'Maintenance' }) // server-only log message\n * deny(404, { dangerouslyPassData: { resourceId: params.id } }) // explicit client opt-in\n * ```\n *\n * Accepts any 4xx or 5xx status code — it is also how a page renders a 5xx\n * status file with data. An unplanned throw needs no primitive: it renders\n * the error page with a 500.\n *\n * @param status - HTTP status code (4xx or 5xx). Default: 403.\n * @param options - Optional message and/or data to pass to the client.\n */\nexport function deny(status?: number, options?: DenyOptions): never {\n const resolvedStatus = status ?? 403;\n const resolvedData = options?.dangerouslyPassData;\n\n if (resolvedStatus < 400 || resolvedStatus > 599) {\n throw new Error(`deny() requires a 4xx or 5xx status code, got ${resolvedStatus}.`);\n }\n warnIfNotSerializable(resolvedData, 'deny()');\n throw new DenySignal(resolvedStatus, resolvedData);\n}\n\n// Single source of truth shared with the client next/navigation shim —\n// see shared/redirect-type.ts.\nexport { RedirectType } from '../shared/redirect-type.ts';\n\n// ─── RedirectSignal ─────────────────────────────────────────────────────────\n\n/**\n * Render-phase signal thrown by `redirect()` and `redirectExternal()`.\n * Caught by the framework to produce a 3xx response or client-side navigation.\n *\n * Detect with `isRedirectSignal()`, never `instanceof` — see \"Signal branding\" in signal-identity.ts.\n */\nexport class RedirectSignal extends Error {\n readonly [REDIRECT_BRAND] = true;\n readonly location: string;\n readonly status: number;\n\n constructor(location: string, status: number) {\n super(`Redirect to ${location}`);\n this.name = 'RedirectSignal';\n this.location = location;\n this.status = status;\n }\n}\n\n// ─── Signal Detection ───────────────────────────────────────────────────────\n\n/**\n * Options for redirect() — alternative to passing a bare status code.\n */\nexport interface RedirectOptions {\n /** HTTP redirect status code (3xx). Defaults to 302 (or 308 when `permanent: true`). */\n status?: number;\n /**\n * When true, defaults the status to 308 (Permanent Redirect, preserves HTTP method).\n * If `status` is also provided, `status` takes precedence.\n *\n * @example\n * redirect('/new-path', { permanent: true }); // 308\n * redirect('/new-path', { permanent: true, status: 301 }); // 301\n */\n permanent?: boolean;\n /**\n * Preserve search params from the current request URL on the redirect target.\n *\n * - `true` — preserve ALL current search params (target params take precedence)\n * - `string[]` — preserve only the named params (e.g. `['private', 'token']`)\n *\n * Target path's own query params always take precedence over preserved ones.\n */\n preserveSearchParams?: true | string[];\n}\n\n/**\n * Redirect to a relative path. Rejects absolute and protocol-relative URLs.\n * Use `redirectExternal()` for external redirects with an allow-list.\n *\n * @param path - Relative path (e.g. '/login', 'settings', '/login?returnTo=/dash')\n * @param statusOrOptions - HTTP status code (3xx, default 302) or options object.\n *\n * @example\n * // Simple redirect\n * redirect('/login');\n *\n * // With status code\n * redirect('/login', 301);\n *\n * // With preserved search params\n * redirect(`/docs/${version}/${slug}`, { preserveSearchParams: ['foo'] });\n */\nexport function redirect(path: string, statusOrOptions?: number | RedirectOptions): never {\n let status: number;\n let preserveSearchParams: true | string[] | undefined;\n\n if (typeof statusOrOptions === 'number') {\n status = statusOrOptions;\n } else if (statusOrOptions) {\n // Explicit status wins. Otherwise permanent: true → 308, default → 302.\n status = statusOrOptions.status ?? (statusOrOptions.permanent ? 308 : 302);\n preserveSearchParams = statusOrOptions.preserveSearchParams;\n } else {\n status = 302;\n }\n\n if (status < 300 || status > 399) {\n throw new Error(`redirect() requires a 3xx status code, got ${status}.`);\n }\n // Relative-only validation shared with the client next/navigation shim —\n // strips C0 controls and normalizes backslashes before the scheme check.\n assertRelativeRedirectPath(path);\n\n let resolvedPath = path;\n if (preserveSearchParams) {\n const currentSearch = getRequestSearchString();\n resolvedPath = mergePreservedSearchParams(path, currentSearch, preserveSearchParams);\n }\n\n throw new RedirectSignal(resolvedPath, status);\n}\n\n/**\n * Redirect to an external URL. The origin must be in the provided allow-list.\n *\n * Only http: and https: schemes are permitted. The allow-list is matched\n * against full origins (scheme + host + port), not bare hostnames.\n *\n * @param url - Absolute URL to redirect to.\n * @param allowList - Array of allowed origins (e.g. ['https://example.com', 'https://auth.example.com']).\n * @param status - HTTP redirect status code (3xx). Defaults to 302.\n */\nexport function redirectExternal(url: string, allowList: string[], status: number = 302): never {\n if (status < 300 || status > 399) {\n throw new Error(`redirectExternal() requires a 3xx status code, got ${status}.`);\n }\n\n const canonicalUrl = validateExternalRedirectUrl(url, allowList);\n throw new RedirectSignal(canonicalUrl, status);\n}\n\n// ─── waitUntil ──────────────────────────────────────────────────────────────\n\n// Intentional per-app singleton — warn-once flag that persists for the\n// lifetime of the process/isolate. Not per-request; do not migrate to ALS.\nlet _waitUntilWarned = false;\n\n/**\n * Register a promise to be kept alive after the response is sent.\n * Maps to `ctx.waitUntil()` on Cloudflare Workers and similar platforms.\n *\n * The platform adapter installs a per-request waitUntil function via ALS\n * (see waituntil-bridge.ts). If no ALS handler is available, a warning\n * is logged once and the promise is left to resolve (or reject) without\n * being tracked.\n *\n * @param promise - The background work to keep alive.\n */\nexport function waitUntil(promise: Promise<unknown>): void {\n const alsFn = _getWaitUntil();\n if (alsFn) {\n alsFn(promise);\n return;\n }\n\n if (!_waitUntilWarned) {\n _waitUntilWarned = true;\n console.warn(\n '[timber] waitUntil() is not supported by the current adapter. ' +\n 'Background work will not be tracked. This warning is shown once.'\n );\n }\n}\n\n/**\n * Reset the waitUntil warning state. Exported for testing only.\n * @internal\n */\nexport function _resetWaitUntilWarning(): void {\n _waitUntilWarned = false;\n}\n","/**\n * FormData preprocessing — schema-agnostic conversion of FormData to typed objects.\n *\n * FormData is all strings. Schema validation expects typed values. This module\n * bridges the gap with intelligent coercion that runs *before* schema validation.\n *\n * Inspired by zod-form-data, but schema-agnostic — works with any Standard Schema\n * library (Zod, Valibot, ArkType).\n *\n * See design/08-forms-and-actions.md §\"parseFormData() and coerce helpers\"\n */\n\n// ─── parseFormData ───────────────────────────────────────────────────────\n\n/**\n * Convert FormData into a plain object with intelligent coercion.\n *\n * Handles:\n * - **Duplicate keys → arrays**: `tags=js&tags=ts` → `{ tags: [\"js\", \"ts\"] }`\n * - **Nested dot-paths**: `user.name=Alice` → `{ user: { name: \"Alice\" } }`\n * - **Indexed lists → arrays**: `rows.0.name=A&rows.1.name=B` →\n * `{ rows: [{ name: \"A\" }, { name: \"B\" }] }`, when the indexes are exactly\n * `0..n-1`. A list with a gap stays an object keyed by index.\n * - **Empty strings stay `\"\"`**: a blank text input is `\"\"`, so\n * `z.string().min(1, 'Required')` reports its own message. Use\n * `coerce.text` to make a blank optional field `undefined`\n * - **Empty Files → undefined**: File inputs with no selection become `undefined`\n * - **Strips `$ACTION_*` fields**: React's internal hidden fields are excluded\n * - **Drops `__proto__` paths and names deeper than 32 segments**\n */\nexport function parseFormData(formData: FormData): Record<string, unknown> {\n const flat: Record<string, unknown> = {};\n\n for (const key of new Set(formData.keys())) {\n // Skip React internal fields\n if (key.startsWith('$ACTION_')) continue;\n\n // Skip prototype-pollution keys at the top level — assigning to\n // `flat['__proto__']` invokes the prototype setter and can splice\n // attacker-controlled data into `flat`'s prototype chain.\n // The dot-path case is filtered separately in `expandDotPaths`.\n if (DANGEROUS_KEYS.has(key)) continue;\n\n const values = formData.getAll(key);\n const processed = values.map(normalizeValue);\n\n if (processed.length === 1) {\n flat[key] = processed[0];\n } else {\n // Filter out undefined (empty Files) and empty strings from multi-value fields.\n // Multi-value fields (e.g. tags=js&tags=&tags=ts) should not include blanks.\n flat[key] = processed.filter((v) => v !== undefined && v !== '');\n }\n }\n\n // Expand dot-notation paths into nested objects\n return expandDotPaths(flat);\n}\n\n/**\n * Normalize a single FormData entry value.\n * - Empty File objects (no selection) → undefined\n * - Strings pass through as-is (empty strings stay as empty strings so\n * schema validators like `z.string().min(1, 'Required')` can produce\n * their custom error messages instead of a generic type mismatch)\n * - Everything else passes through as-is\n */\nfunction normalizeValue(value: FormDataEntryValue): unknown {\n // File input with no selection: browsers submit a File with name=\"\" and size=0\n if (value instanceof File && value.size === 0 && value.name === '') {\n return undefined;\n }\n\n return value;\n}\n\n/**\n * Expand dot-notation keys into nested objects.\n * `{ \"user.name\": \"Alice\", \"user.age\": \"30\" }` → `{ user: { name: \"Alice\", age: \"30\" } }`\n *\n * Keys without dots are left as-is. Bracket notation (e.g. `items[0]`) is NOT\n * supported — use dot notation (`items.0`) instead.\n */\nfunction expandDotPaths(flat: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n let hasDotPaths = false;\n\n // First pass: check if any keys have dots\n for (const key of Object.keys(flat)) {\n if (key.includes('.')) {\n hasDotPaths = true;\n break;\n }\n }\n\n // Fast path: no dot-notation keys, return as-is.\n // Top-level dangerous keys are already filtered in `parseFormData` above.\n if (!hasDotPaths) return flat;\n\n for (const [key, value] of Object.entries(flat)) {\n // Reject any path that contains __proto__ / constructor / prototype.\n // Without this, `__proto__.x=1` writes to Object.prototype because\n // `result['__proto__']` resolves to the prototype object itself.\n const parts = key.split('.');\n if (parts.some((p) => DANGEROUS_KEYS.has(p))) continue;\n // Bound the nesting depth. Every walk over the parsed value recurses\n // once per level (list conversion here, then file and sensitive-field\n // stripping and schema validation), and a 10 KB key of 5,000 segments\n // fits well inside the body limits. No form nests this deep.\n if (parts.length > MAX_PATH_DEPTH) continue;\n\n if (parts.length === 1) {\n result[parts[0]] = value;\n continue;\n }\n\n let current: Record<string, unknown> = result;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n // Step only into an object this walk built. Anything else under the\n // name — a string, a File, or an array from a duplicate key\n // (`rows=x&rows=y`) — is replaced: the dot-path takes precedence.\n // Stepping into an array would let `rows.4294967294` set its length\n // to 2^32 - 1, and every later walk (file and sensitive-field\n // stripping) maps over that length (TIM-1573).\n if (!isPlainObject(current[part])) {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n\n current[parts[parts.length - 1]] = value;\n }\n\n // The top level is always an object: it is the form, not a list.\n for (const [key, value] of Object.entries(result)) {\n if (isPlainObject(value)) result[key] = indexedListsToArrays(value);\n }\n return result;\n}\n\n/**\n * `node`, with every nested object whose keys are exactly `\"0\"..\"n-1\"`\n * turned into an array, depth first. Only a dense, canonical index set\n * converts: `{ \"0\", \"2\" }` (a gap), `{ \"01\" }` and `{ \"0\", \"name\" }` stay\n * objects. So a forged `rows.99999` stays one key instead of allocating a\n * sparse array, and the array's length is bounded by the field count limit.\n *\n * Object.keys lists integer-like keys first, in ascending order, so a dense\n * set reads as `0, 1, … n-1` and `Object.values` is already in index order.\n */\nfunction indexedListsToArrays(node: Record<string, unknown>): Record<string, unknown> | unknown[] {\n for (const [key, value] of Object.entries(node)) {\n if (isPlainObject(value)) node[key] = indexedListsToArrays(value);\n }\n const keys = Object.keys(node);\n const dense = keys.length > 0 && keys.every((key, i) => key === String(i));\n return dense ? Object.values(node) : node;\n}\n\n/** An object the dot-path walk built — not an array, File or other instance. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype\n );\n}\n\n// `__proto__` is the only key in this parser that escapes the dot-path\n// walk's `typeof !== 'object'` reset: accessing `obj['__proto__']` returns\n// `Object.prototype` (itself an object), so the walk steps into the global\n// prototype and the leaf write mutates it. `constructor` resolves to a\n// function (reset fires) and `prototype` is undefined on fresh objects\n// (reset fires), so neither escapes the walk — they parse as legit nested\n// keys and do not mutate anything global.\nconst DANGEROUS_KEYS = new Set(['__proto__']);\n\n/**\n * The most segments a dot-path field name may have. A deeper name is dropped,\n * like a `__proto__` path, so no walk over the parsed value can exhaust the\n * stack (TIM-1573).\n */\nconst MAX_PATH_DEPTH = 32;\n\n// ─── Coercion Helpers ────────────────────────────────────────────────────\n\n/**\n * Schema-agnostic coercion primitives for common FormData patterns.\n *\n * These are plain transform functions — they compose with any schema library's\n * `transform`/`preprocess` pipeline. In Zod, use `z.preprocess`: it runs on an\n * absent key (an unchecked checkbox), where `z.unknown().transform()` fails\n * with \"expected nonoptional\" before the transform runs.\n *\n * ```ts\n * // Zod\n * z.preprocess(coerce.number, z.number())\n * // Valibot\n * v.pipe(v.unknown(), v.transform(coerce.number), v.number())\n * ```\n */\nexport const coerce = {\n /**\n * Coerce an empty string to undefined for optional text fields.\n * Use with `.optional()` schemas where an empty input means \"not provided\".\n *\n * ```ts\n * // Zod — preprocess, not `z.unknown().transform(…)`: Zod 4 rejects an\n * // absent key before a transform runs (\"expected nonoptional\")\n * z.preprocess(coerce.text, z.string().optional())\n * // Valibot\n * v.pipe(v.unknown(), v.transform(coerce.text), v.optional(v.string()))\n * ```\n */\n text(value: unknown): string | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value === 'string') return value;\n return undefined;\n },\n\n /**\n * Coerce a string to a number.\n * - `\"42\"` → `42`\n * - `\"3.14\"` → `3.14`\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Non-numeric strings → `undefined` (schema validation will catch this)\n */\n number(value: unknown): number | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value === 'number') return value;\n if (typeof value !== 'string') return undefined;\n const num = Number(value);\n if (Number.isNaN(num)) return undefined;\n return num;\n },\n\n /**\n * Coerce a checkbox value to a boolean.\n * HTML checkboxes submit \"on\" when checked and are absent when unchecked.\n * - `\"on\"` / any truthy string → `true`\n * - `undefined` / `null` / `\"\"` → `false`\n */\n checkbox(value: unknown): boolean {\n if (value === undefined || value === null || value === '') return false;\n if (typeof value === 'boolean') return value;\n // Any non-empty string (typically \"on\") is true\n return typeof value === 'string' && value.length > 0;\n },\n\n /**\n * Parse a JSON string into an object.\n * - Valid JSON string → parsed object\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Invalid JSON → `undefined` (schema validation will catch this)\n */\n json(value: unknown): unknown {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch {\n return undefined;\n }\n },\n\n /**\n * Coerce a date string to a Date object.\n * Handles `<input type=\"date\">` (`\"2024-01-15\"`), `<input type=\"datetime-local\">`\n * (`\"2024-01-15T10:30\"`), and full ISO 8601 strings.\n * - Valid date string → `Date`\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Invalid date strings → `undefined` (schema validation will catch this)\n * - Impossible dates that `new Date()` silently normalizes (e.g. Feb 31) → `undefined`\n */\n date(value: unknown): Date | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (value instanceof Date) return value;\n if (typeof value !== 'string') return undefined;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return undefined;\n\n // Overflow detection: extract Y/M/D from the input string and verify\n // they match the parsed Date components. new Date('2024-02-31') silently\n // normalizes to March 2nd — we reject such inputs.\n const ymdMatch = value.match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n if (ymdMatch) {\n const inputYear = Number(ymdMatch[1]);\n const inputMonth = Number(ymdMatch[2]);\n const inputDay = Number(ymdMatch[3]);\n\n // Use UTC methods for date-only and Z-suffixed strings to avoid\n // timezone offset shifting the day. For datetime-local (no Z suffix),\n // the Date constructor parses in local time, so use local methods.\n const isUTC = value.length === 10 || value.endsWith('Z');\n const parsedYear = isUTC ? date.getUTCFullYear() : date.getFullYear();\n const parsedMonth = isUTC ? date.getUTCMonth() + 1 : date.getMonth() + 1;\n const parsedDay = isUTC ? date.getUTCDate() : date.getDate();\n\n if (inputYear !== parsedYear || inputMonth !== parsedMonth || inputDay !== parsedDay) {\n return undefined;\n }\n }\n\n return date;\n },\n\n /**\n * Create a File coercion function with optional size and mime type validation.\n * Returns the File if valid, `undefined` otherwise.\n *\n * ```ts\n * // Basic — just checks it's a real File\n * z.preprocess(coerce.file(), z.instanceof(File))\n *\n * // With constraints\n * z.preprocess(\n * coerce.file({ maxSize: 5 * 1024 * 1024, accept: ['image/png', 'image/jpeg'] }),\n * z.instanceof(File)\n * )\n * ```\n */\n file(options?: { maxSize?: number; accept?: string[] }): (value: unknown) => File | undefined {\n return (value: unknown): File | undefined => {\n if (value === undefined || value === null || value === '') return undefined;\n if (!(value instanceof File)) return undefined;\n\n // Empty file input (no selection): browsers submit File with name=\"\" and size=0\n if (value.size === 0 && value.name === '') return undefined;\n\n if (options?.maxSize !== undefined && value.size > options.maxSize) {\n return undefined;\n }\n\n if (options?.accept !== undefined && !options.accept.includes(value.type)) {\n return undefined;\n }\n\n return value;\n };\n },\n};\n","/**\n * Server action primitives: revalidatePath, revalidateTag, and the action handler.\n *\n * - revalidatePath(path) re-renders the route at that path and returns the RSC\n * flight payload for inline reconciliation. Server actions only.\n * - revalidateTag(tag) invalidates timber.cache entries by tag. Callable from\n * any server code: deferred to the end of the action inside one, immediate\n * anywhere else.\n *\n * The action handler processes incoming action requests, validates CSRF,\n * enforces body limits, executes the action, and returns the response\n * (with piggybacked RSC payload if revalidatePath was called).\n *\n * See design/08-forms-and-actions.md\n */\n\nimport { cache } from '../cache/cache-api.ts';\nimport { canonicalize } from './canonicalize.ts';\nimport { isDenySignal, isRedirectSignal } from './signal-identity.ts';\nimport { withSpan } from './tracing.ts';\nimport { revalidationAls, type RevalidationState } from './als-registry.ts';\nimport type { ReactCacheScope } from './react-cache-scope.ts';\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/** Result of rendering a revalidation — payload root before RSC serialization. */\nexport interface RevalidationResult {\n /**\n * The payload root — tree plus the params it rendered with, exactly as an\n * ordinary route payload. The client splits it in `applyActionResult`.\n */\n payload: unknown;\n /**\n * The `React.cache` scope the target route's middleware ran in. The\n * revalidated tree renders in it, so middleware, access.ts and components\n * share one scope — fresh, never the action body's (TIM-1529).\n */\n reactCacheScope: ReactCacheScope;\n}\n\n/** Renderer function that builds a React element tree for a given path. */\nexport type RevalidateRenderer = (path: string) => Promise<RevalidationResult>;\n\n/**\n * Thrown by a `RevalidateRenderer` that decided not to render: the path\n * was rejected, its params failed coercion, or its middleware answered\n * with a `Response` that cannot become an element tree. `executeAction`\n * drops the revalidation — the action result is still delivered, with no\n * `_tree` piggybacked — and, because the drop was a decision, does not\n * return it as an error to report. Identified by `instanceof`, so the\n * message text never reaches clients.\n */\nexport class RevalidationDropped extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RevalidationDropped';\n }\n}\n\n// Re-export the type from the registry for public API consumers.\nexport type { RevalidationState } from './als-registry.ts';\n\n/** Options for creating the action handler. */\nexport interface ActionHandlerConfig {\n /** Renderer for producing RSC payloads during revalidation. */\n renderer?: RevalidateRenderer;\n /**\n * Pathname of the page the action was submitted from. When set,\n * revalidatePath renders are skipped for non-matching paths — the\n * server cache is already invalidated, so the next navigation fetches\n * fresh data. See TIM-1454.\n */\n requestPathname?: string;\n /** Search string of the action request URL (e.g. `?page=1`). */\n requestSearch?: string;\n}\n\n/** Result of handling a server action request. */\nexport interface ActionHandlerResult {\n /** The action's return value (serialized). */\n actionResult: unknown;\n /** Revalidation result if revalidatePath was called (element tree, not yet serialized). */\n revalidation?: RevalidationResult;\n /**\n * True when `revalidatePath()` was called only for pages other than the\n * current one (TIM-1454). The client then evicts its caches and skips the\n * refresh — nothing on screen was named. Whenever the current page was\n * named this is false, even if other pages were named too: its re-render,\n * or the refresh that replaces a dropped one, re-renders every layout it\n * shares with them. Which paths were named is not reported: the client\n * evicts every cached payload after any revalidation (TIM-1476), so a path\n * list would carry nothing it acts on (TIM-1461).\n */\n onlyOtherPagesNamed: boolean;\n /** Redirect location if a RedirectSignal was thrown during revalidation. */\n redirectTo?: string;\n /** Redirect status code. */\n redirectStatus?: number;\n /**\n * Errors the action survived after its body returned: a `revalidateTag`\n * invalidation that rejected, or a `revalidatePath` render that threw.\n * Each is already logged. They are returned rather than reported here\n * because reporting needs the request, which the caller owns; the caller\n * passes each to `onRequestError` once.\n */\n revalidationErrors: unknown[];\n}\n\n// ─── Revalidation State ──────────────────────────────────────────────────\n\n// Per-request revalidation state stored in AsyncLocalStorage (from als-registry.ts).\n// This ensures concurrent requests never share or overwrite each other's state\n// (the previous module-level global was vulnerable to cross-request pollution).\n\n/**\n * Run `fn` inside a revalidation ALS scope.\n * @internal — for tests that call revalidatePath/Tag directly without executeAction().\n */\nexport function _runWithRevalidationState<T>(state: RevalidationState, fn: () => T): T {\n return revalidationAls.run(state, fn);\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────\n\n/**\n * Re-render the route at `path` and include the RSC flight payload in the\n * action response. The client reconciles inline — no separate fetch needed.\n *\n * Only callable inside a server action: it attaches a render to the action\n * response, and nothing else has one. To refresh cached data from a\n * `route.ts` handler, a webhook, or a job, use `revalidateTag()` or\n * `cache.invalidate()`.\n *\n * @param path - The path to re-render (e.g. '/dashboard', '/todos').\n */\nexport function revalidatePath(path: string): void {\n const state = revalidationAls.getStore();\n if (!state || state.closed) {\n throw new Error(\n 'revalidatePath() can only be called inside a server action: it re-renders ' +\n 'the route into the action response, and there is no action response here. ' +\n 'To refresh cached data from a route handler or other server code, call ' +\n 'revalidateTag() or cache.invalidate() for the tags the route reads.'\n );\n }\n if (!state.paths.includes(path)) {\n state.paths.push(path);\n }\n}\n\n/**\n * Invalidate all timber.cache entries tagged with `tag`, and tombstone the\n * pre-rendered component seeds that carry it.\n * Does not return a payload — the next request for an invalidated entry re-executes.\n *\n * Callable from any server code. Inside a server action the invalidation is\n * deferred until the action body returns, so a `revalidatePath()` render in\n * the same action reads fresh data; the returned promise resolves at once.\n * Anywhere else — a `route.ts` webhook, a job, or background work an action\n * started that runs after the action body returned — it is\n * `cache.invalidate({ tag })`, and the promise settles when the invalidation\n * has. Await it before responding.\n *\n * @param tag - The cache tag to invalidate (e.g. 'products', 'user:123').\n */\nexport function revalidateTag(tag: string): Promise<void> {\n const state = revalidationAls.getStore();\n if (!state || state.closed) return cache.invalidate({ tag });\n if (!state.tags.includes(tag)) {\n state.tags.push(tag);\n }\n return Promise.resolve();\n}\n\n// ─── Action Handler ──────────────────────────────────────────────────────\n\n/**\n * Execute a server action and process revalidation.\n *\n * 1. Sets up revalidation state\n * 2. Calls the action function\n * 3. Processes revalidateTag calls (invalidates cache entries)\n * 4. Processes revalidatePath calls (re-renders and captures RSC payload)\n * 5. Returns the action result + optional RSC payload\n *\n * @param actionFn - The server action function to execute.\n * @param args - Arguments to pass to the action.\n * @param config - Handler configuration (cache handler, renderer).\n */\nexport async function executeAction(\n actionFn: (...args: unknown[]) => Promise<unknown>,\n args: unknown[],\n config: ActionHandlerConfig = {},\n spanMeta?: { actionFile?: string; actionName?: string }\n): Promise<ActionHandlerResult> {\n const state: RevalidationState = { paths: [], tags: [] };\n const revalidationErrors: unknown[] = [];\n let actionResult: unknown;\n let redirectTo: string | undefined;\n let redirectStatus: number | undefined;\n\n // Run the action inside ALS scope so revalidatePath/Tag resolve to this\n // request's state object — concurrent requests each get their own scope.\n await revalidationAls.run(state, async () => {\n try {\n actionResult = await withSpan(\n 'timber.action',\n {\n ...(spanMeta?.actionFile ? { 'timber.action_file': spanMeta.actionFile } : {}),\n ...(spanMeta?.actionName ? { 'timber.action_name': spanMeta.actionName } : {}),\n },\n () => actionFn(...args)\n );\n } catch (error) {\n if (isRedirectSignal(error)) {\n redirectTo = error.location;\n redirectStatus = error.status;\n } else {\n throw error;\n }\n } finally {\n // From here `paths` and `tags` are snapshotted below (or abandoned on\n // a throw); later calls from un-awaited work must not queue into them.\n state.closed = true;\n }\n });\n\n // Process tag invalidation through cache.invalidate so it records the\n // invalidation epoch — an in-flight cached fn must not re-store data this\n // mutation just invalidated (TIM-1028). cache.invalidate resolves the\n // module-level handler singleton: setCacheHandler() is called at boot from\n // rsc-entry when timber.config.ts provides a cacheHandler; otherwise falls\n // back to in-memory LRU (TIM-599).\n if (state.tags.length > 0) {\n // cache.invalidate handles both data-cache clearing AND component-seed\n // tombstone writes (design/45 §ISR mechanics) — no separate tombstone\n // step needed here.\n // Best-effort: the action body already executed and its mutation committed.\n // Use allSettled so all tags settle before proceeding — Promise.all would\n // short-circuit on the first rejection, leaving other invalidations (including\n // component-seed tombstone writes) in-flight during revalidatePath rendering.\n const results = await Promise.allSettled(state.tags.map((tag) => cache.invalidate({ tag })));\n for (const r of results) {\n if (r.status === 'rejected') {\n console.error('[timber] revalidateTag invalidation failed:', r.reason);\n revalidationErrors.push(r.reason);\n }\n }\n }\n\n // Process path revalidation — build element tree (not yet serialized).\n //\n // TIM-1454: Only a path that matches the current page is rendered. When\n // every call names another page, `onlyOtherPagesNamed` tells the client to\n // evict its caches instead. The matching path is looked for across every\n // call, not just the first: an action that calls revalidatePath('/other')\n // and then revalidatePath('/current') must still re-render the current\n // page (TIM-1461).\n let revalidation: RevalidationResult | undefined;\n const { requestPathname, requestSearch } = config;\n const isCurrentPage = (path: string) =>\n !requestPathname || revalidationPathMatches(path, requestPathname, requestSearch);\n const path = state.paths.find(isCurrentPage);\n const onlyOtherPagesNamed = state.paths.length > 0 && path === undefined;\n\n if (path !== undefined && config.renderer) {\n try {\n revalidation = await config.renderer(path);\n } catch (renderError) {\n if (isRedirectSignal(renderError)) {\n redirectTo = renderError.location;\n redirectStatus = renderError.status;\n } else if (isDenySignal(renderError)) {\n console.error(\n `[timber] revalidatePath dropped — target middleware denied (status ${renderError.status})`\n );\n } else if (renderError instanceof RevalidationDropped) {\n console.error(`[timber] ${renderError.message}`);\n } else {\n console.error('[timber] revalidatePath render failed:', renderError);\n revalidationErrors.push(renderError);\n }\n }\n }\n\n return {\n actionResult,\n revalidation,\n onlyOtherPagesNamed,\n revalidationErrors,\n ...(redirectTo ? { redirectTo, redirectStatus } : {}),\n };\n}\n\n/**\n * Check if a `revalidatePath(path)` argument matches the request pathname.\n * Canonicalizes the revalidated path the same way the renderer does so\n * equivalent paths compare equal. Returns true when the paths match or\n * when comparison is inconclusive (validation failure → render to be safe).\n */\nfunction revalidationPathMatches(\n revalidatedPath: string,\n requestPathname: string,\n requestSearch?: string\n): boolean {\n const hashIdx = revalidatedPath.indexOf('#');\n const noFragment = hashIdx >= 0 ? revalidatedPath.slice(0, hashIdx) : revalidatedPath;\n const queryIdx = noFragment.indexOf('?');\n const pathnameOnly = queryIdx >= 0 ? noFragment.slice(0, queryIdx) : noFragment;\n const search = queryIdx >= 0 ? noFragment.slice(queryIdx) : '';\n const canonical = canonicalize(pathnameOnly, true);\n if (!canonical.ok) return true;\n if (canonical.pathname !== requestPathname) return false;\n // revalidatePath('/dashboard') (no search) matches any query variant.\n // revalidatePath('/dashboard?tab=x') only matches that exact search.\n if (search && search !== (requestSearch ?? '')) return false;\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"actions-CUh3cClk.js","names":[],"sources":["../../src/server/react-cache-scope.ts","../../src/server/request-context.ts","../../src/server/cookie-parsing.ts","../../src/server/cookie-context.ts","../../src/server/primitives.ts","../../src/server/form-data.ts","../../src/server/actions.ts"],"sourcesContent":["/**\n * React.cache scopes — opening, resuming, and leaving the per-request\n * `React.cache` scope that `react-cache-bridge.ts` makes Flight read.\n *\n * Kept apart from the bridge so request-context and cookie code can use\n * it without importing `react`. See design/spike-TIM-1529-react-cache-bridge.md.\n */\n\nimport { reactCacheScopeAls, type ReactCacheScope } from './als-registry.ts';\nimport { isDevMode } from './debug.ts';\n\nexport type { ReactCacheScope };\n\n/** A new, empty `React.cache` scope. */\nexport function createReactCacheScope(): ReactCacheScope {\n return new Map();\n}\n\n/**\n * Run `fn` in a `React.cache` scope — a fresh one unless `scope` is given.\n * Every `React.cache` call made from `fn` — synchronously, in its async\n * continuations, and in any Flight render it starts — shares one memo table.\n *\n * Opened by `runWithRequestContext`, so each request context gets its own\n * scope. Pass `scope` to resume one across two ALS runs: the post-action\n * revalidation opens a fresh scope for the target route's middleware and\n * renders the revalidated tree in that same scope later.\n */\nexport function runWithReactCacheScope<T>(\n fn: () => T,\n scope: ReactCacheScope = createReactCacheScope()\n): T {\n return reactCacheScopeAls.run(scope, fn);\n}\n\n/**\n * Run `fn` with no timber scope, so `React.cache` behaves as stock React:\n * no dedupe outside a render, Flight's own per-render cache inside one.\n *\n * Used for server action bodies — an action reads, writes cookies, and\n * mutates, so memoized reads would go stale mid-action (the same choice\n * Next.js makes) — and for renders whose output outlives the request\n * (`cache.component` capture), which must not bake request-memoized values\n * into a cross-request cache entry.\n */\nexport function runOutsideReactCacheScope<T>(fn: () => T): T {\n return reactCacheScopeAls.exit(fn);\n}\n\n// ─── Dev diagnostic: cookie written after a cached read ──────────────────\n//\n// A value React.cache stored was computed from the request inputs it read,\n// and does not see a later write — a cached getUser() that read the session\n// cookie still returns the old user after middleware deletes the cookie.\n// The scope is deliberately NOT cleared on a write: that would silently\n// discard every value middleware warmed (spike doc §Decisions D3). Instead,\n// dev mode flags the one case that can go stale: a cookie read while the\n// scope held values, then written later in the same request. Tracking is\n// per scope, so it ends with the request; production does none of it.\n\n/** Stands for \"every cookie\" — a getAll()/size read, or a clear() write. */\nexport const ANY_COOKIE = '*';\n\nconst cookieReads = new WeakMap<ReactCacheScope, Set<string>>();\nconst warnedScopes = new WeakSet<ReactCacheScope>();\n\n/**\n * Dev-only: record that cookie `name` (or {@link ANY_COOKIE}) was read\n * while `React.cache` held values in this request — the read may have\n * happened inside a cached function.\n */\nexport function noteCookieReadForReactCache(name: string): void {\n if (!isDevMode()) return;\n const scope = reactCacheScopeAls.getStore();\n if (!scope || scope.size === 0) return;\n let reads = cookieReads.get(scope);\n if (!reads) {\n reads = new Set();\n cookieReads.set(scope, reads);\n }\n reads.add(name);\n}\n\n/**\n * Dev-only: warn (once per request) when a cookie recorded by\n * {@link noteCookieReadForReactCache} is written. `name` is the written\n * cookie, or {@link ANY_COOKIE} for `clear()`. Dev-only because reads are\n * only recorded in dev.\n */\nexport function warnIfCachedCookieIsWritten(name: string): void {\n const scope = reactCacheScopeAls.getStore();\n if (!scope || warnedScopes.has(scope)) return;\n const reads = cookieReads.get(scope);\n if (!reads) return;\n const stale = name === ANY_COOKIE ? reads.size > 0 : reads.has(name) || reads.has(ANY_COOKIE);\n if (!stale) return;\n warnedScopes.add(scope);\n const which = name === ANY_COOKIE ? 'Cookies were cleared' : `Cookie \"${name}\" was written`;\n console.warn(\n `[timber] ${which} after being read while React.cache held values for this request. ` +\n `A cached function that read it keeps returning the old result — pass the cookie ` +\n `value in as an argument (e.g. getUser(sessionToken)) so a change is a new cache key.`\n );\n}\n","/**\n * Request Context — per-request ALS store for headers, search params,\n * segment params, and request scope lifecycle.\n *\n * Follows the same pattern as tracing.ts: a module-level AsyncLocalStorage\n * instance, public accessor functions that throw outside request scope,\n * and a framework-internal `runWithRequestContext()` to establish scope.\n *\n * Cookie state lives in `cookie-context.ts` (split out in TIM-853). The\n * scope set up here owns the cookie jar / parsedCookies fields on the\n * store, but the cookie API and helpers are in the cookie module.\n *\n * See design/04-authorization.md §\"AccessContext does not include cookies or headers\"\n * and design/11-platform.md §\"AsyncLocalStorage\".\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport {\n requestContextAls,\n type ActionErrorForRender,\n type RequestContextStore,\n} from './als-registry.ts';\nimport { _setGetSearchParamsFn, _setGetSegmentParamsFn } from '../shared/als-slots.ts';\nimport { appVisibleSearch } from '../shared/rsc-cache-key.ts';\nimport {\n mergeSlotParams,\n resolveSegmentParams,\n type SlotParamsRecord,\n} from '../shared/slot-params.ts';\nimport { isDebug } from './debug.ts';\nimport { runWithReactCacheScope, type ReactCacheScope } from './react-cache-scope.ts';\nimport type { ReactFormState } from 'react-dom/client';\n\n// Re-export the ALS for framework-internal consumers that need direct access.\nexport { requestContextAls };\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns a read-only view of the current request's headers.\n *\n * Available in middleware, access checks, server components, and server actions.\n * Throws if called outside a request context (security principle #2: no global fallback).\n */\nexport function getHeaders(): ReadonlyHeaders {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getHeaders() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n\n return store.headers;\n}\n\n/**\n * Returns the value of a single request header, or undefined if absent.\n *\n * Thin wrapper over `getHeaders().get(name)` for the common case where\n * you need exactly one header.\n *\n * @internal — not part of the public API. Use `getHeaders().get(name)` instead.\n */\nexport function getHeader(name: string): string | undefined {\n const headers = getHeaders();\n return headers.get(name) ?? undefined;\n}\n\n/**\n * Returns the current request's raw URLSearchParams.\n *\n * @internal — not part of the public API. Use `defineSearchParams().get()` instead.\n */\nexport function getSearchParams(): URLSearchParams {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getSearchParams() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n\n return store.searchParams;\n}\n\n// Eagerly register getSearchParams with the search-params module so\n// searchParams.get() can call it without a dynamic import.\n// Dynamic imports lose ALS context in React's RSC Flight renderer,\n// breaking getSearchParams() in parallel slot pages. See TIM-523.\n_setGetSearchParamsFn(getSearchParams);\n\n// Eagerly register getSegmentParams with the segment-params module so\n// segmentParams.get() can call it without a dynamic import.\n// Same pattern as search params — dynamic imports lose ALS context. See TIM-523.\n_setGetSegmentParamsFn(getSegmentParams);\n\n/**\n * Returns the current request's coerced segment params.\n *\n * The optional `segmentPath` argument exists only for TypeScript narrowing —\n * it does not affect the runtime return value. Pass a segment path\n * (e.g. `'/(browse)/[artistSlug]/[year]'`) to narrow the return type to the\n * exact params shape for that segment.\n *\n * Without an argument, returns all coerced params with optional types.\n *\n * See design/41-global-params.md §Function Signatures\n */\nexport function getSegmentParams(segmentPath?: string): CoercedParams {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getSegmentParams() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n if (!store.segmentParams) {\n throw new Error(\n '[timber] getSegmentParams() called before route matching completed. ' +\n 'Segment params are not available until after the route is matched.'\n );\n }\n\n // Parallel slots may derive a param the main route never had, or the same\n // name with a different type (catch-all [...year] → string[] vs dynamic\n // [year] → string). `resolveSegmentParams` is the shared definition the\n // client's useSegmentParams() also reads, so the two halves of one\n // documented API cannot drift (TIM-1285).\n const slotResolved = resolveSegmentParams(store.segmentParams, store.slotParams, segmentPath);\n if (slotResolved !== store.segmentParams) return slotResolved;\n\n // Dev-mode validation: warn when the segment path expects dynamic\n // segments that don't exist on the actual matched segment. This catches\n // bugs like passing a child segment's $segment to a parent layout.\n // Skip for slot paths — they're validated against their own chain.\n // See design/41-global-params.md §Runtime Validation\n if (segmentPath && isDebug() && store.matchedSegmentPath) {\n const expected = extractDynamicSegments(segmentPath);\n const actual = extractDynamicSegments(store.matchedSegmentPath);\n const missing = expected.filter((s) => !actual.includes(s));\n\n if (missing.length > 0) {\n console.warn(\n `[timber] getSegmentParams('${segmentPath}') called but current segment is '${store.matchedSegmentPath}'\\n` +\n ` Missing params: ${missing.join(', ')}\\n` +\n ` These will be undefined at runtime despite the type annotation.`\n );\n }\n }\n\n return store.segmentParams;\n}\n\n/**\n * Set the segment params on the current request context.\n * Called by the pipeline after route matching and param coercion.\n *\n * @internal — framework use only\n */\nexport function setSegmentParams(params: CoercedParams): void {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] setSegmentParams() called outside of a request context.');\n }\n store.segmentParams = params;\n}\n\n/**\n * Store per-slot coerced segment params in the current request context.\n * Called by the route element builder when resolving parallel slots.\n *\n * @param segmentPath — The slot's full tree path (e.g. '/(browse)/@shows/[artistSlug]/[...year]')\n * @param params — The slot's coerced segment params\n * @internal — framework use only\n */\nexport function setSlotParams(segmentPath: string, params: CoercedParams): void {\n const store = requestContextAls.getStore();\n if (!store) return; // non-throwing — optional diagnostic\n if (!store.slotParams) store.slotParams = Object.create(null) as SlotParamsRecord;\n store.slotParams[segmentPath] = params;\n}\n\n/**\n * Run `fn` with the segment params a parallel slot's own chain produced, so\n * a no-arg `getSegmentParams()` inside that slot returns the slot's params\n * rather than the main route's.\n *\n * ALS is the only channel available: the slot's page and layouts are plain\n * components that receive no params prop and cannot know their own tree\n * path, so they have no way to ask for the keyed overload. The slot resolver\n * invokes each of them itself (page through `SafeSlotPage`, layouts and\n * access functions through their wrappers), and this scopes those calls.\n *\n * The store is wrapped in a Proxy rather than cloned. Everything a slot\n * renders can write to the *live* request store — `setDenyStatus` from an\n * access gate, cookie writes, the flush flag — and a clone would silently\n * strand those writes in a copy the pipeline never reads. Only the two\n * params fields are shadowed; every other read and every write passes\n * straight through to the real store.\n *\n * Returns `fn()`'s result unchanged, and no-ops outside a request context or\n * before route matching has set the main params. See TIM-1283.\n *\n * @internal — framework use only\n */\nexport function runWithSlotSegmentParams<T>(\n slotSegmentPath: string | undefined,\n slotParams: CoercedParams,\n fn: () => T\n): T {\n const store = requestContextAls.getStore();\n if (!store?.segmentParams) return fn();\n\n const merged = mergeSlotParams(store.segmentParams, slotParams);\n const matchedSegmentPath = slotSegmentPath ?? store.matchedSegmentPath;\n const scoped = new Proxy(store, {\n get(target, prop, receiver) {\n if (prop === 'segmentParams') return merged;\n if (prop === 'matchedSegmentPath') return matchedSegmentPath;\n return Reflect.get(target, prop, receiver);\n },\n });\n return requestContextAls.run(scoped, fn);\n}\n\n/**\n * The main route's coerced params, or an empty record before route matching.\n *\n * Non-throwing, unlike `getSegmentParams()`: the deny payload path publishes\n * params from wherever the pipeline happened to fail, which may be before\n * matching completed.\n *\n * @internal — framework use only. See TIM-1285.\n */\nexport function getSegmentParamsForClient(): CoercedParams {\n return requestContextAls.getStore()?.segmentParams ?? {};\n}\n\n/**\n * The per-slot params to publish to the client, or `undefined` when the\n * request rendered no slot with params of its own.\n *\n * The client cannot derive these: a slot matches the URL through its own\n * sub-tree, and the browser only ever sees the main route's record. Reading\n * the map here — rather than threading it back out of the element builder —\n * keeps one producer, since `setSlotParams` already writes it during slot\n * resolution and every serialization site runs after that completes.\n *\n * @internal — framework use only. See TIM-1285.\n */\nexport function getSlotParamsForClient(): SlotParamsRecord | undefined {\n const store = requestContextAls.getStore();\n if (!store?.slotParams) return undefined;\n return Object.keys(store.slotParams).length > 0 ? store.slotParams : undefined;\n}\n\n/**\n * Store the matched segment path (e.g. '/(browse)/[artistSlug]/[year]') for\n * dev-mode validation in getSegmentParams().\n *\n * @internal — framework use only\n */\nexport function setMatchedSegmentPath(segmentPath: string): void {\n const store = requestContextAls.getStore();\n if (!store) return; // non-throwing — optional diagnostic\n store.matchedSegmentPath = segmentPath;\n}\n\n/**\n * Extract dynamic segment names from a route pattern.\n * '/[artistSlug]/[year]' → ['artistSlug', 'year']\n * '/docs/[...slug]' → ['slug']\n */\nfunction extractDynamicSegments(route: string): string[] {\n const segments: string[] = [];\n // Match [paramName], [...paramName], [[...paramName]]\n const re = /\\[{1,2}\\.{0,3}(\\w+)\\]{1,2}/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(route)) !== null) {\n segments.push(m[1]);\n }\n return segments;\n}\n\n/**\n * Returns the raw search string from the current request URL (e.g. \"?foo=bar\").\n * Synchronous — safe for use in `redirect()` which throws synchronously.\n *\n * Returns empty string if called outside a request context (non-throwing for\n * use in redirect's optional preserveSearchParams path).\n *\n * @internal — used by redirect() for preserveSearchParams support.\n */\nexport function getRequestSearchString(): string {\n const store = requestContextAls.getStore();\n return store?.searchString ?? '';\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\n/**\n * Read-only Headers interface. The standard Headers class is mutable;\n * this type narrows it to read-only methods. The underlying object is\n * still a Headers instance, but user code should not mutate it.\n */\nexport type ReadonlyHeaders = Pick<\n Headers,\n 'get' | 'has' | 'entries' | 'keys' | 'values' | 'forEach' | typeof Symbol.iterator\n>;\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/** Options for `runWithRequestContext()`. */\nexport interface RequestContextOptions {\n /**\n * Resume this `React.cache` scope instead of opening a fresh one. Only the\n * post-action revalidation passes it.\n */\n reactCacheScope?: ReactCacheScope;\n /**\n * The request's cookies, already parsed. Re-entry callers (the no-JS form\n * rerender and the revalidation middleware) pass the action's\n * post-mutation cookie state here instead of round-tripping it through a\n * `Cookie:` header — the same parse the H-3 smuggling primitive abused\n * (TIM-868).\n */\n cookies?: Map<string, string>;\n /**\n * The form state of the no-JS action this render answers. Only the\n * action dispatcher's re-entry passes it; see `RequestContextStore.formState`.\n */\n formState?: ReactFormState;\n /**\n * The error of the no-JS action this render answers. Only the action\n * dispatcher's re-entry passes it; see `RequestContextStore.actionError`.\n */\n actionError?: ActionErrorForRender;\n}\n\n/**\n * Run a callback within a request context. Used by the pipeline to establish\n * per-request ALS scope so that `getHeaders()` and `getCookies()` work.\n *\n * When `options.cookies` is given, the context's `parsedCookies` map is a\n * copy of it and the raw `cookieHeader` is left empty, so\n * `parseCookieHeader` never runs for that request. Re-entry callers pass the\n * action's post-mutation cookie state this way (TIM-868).\n *\n * Each request context also opens its own `React.cache` scope, shared by\n * everything that runs inside it — proxy.ts, middleware.ts, route handlers,\n * and the render (design/02-rendering-pipeline.md §\"Cache Scoping Model\").\n *\n * @param req - The incoming Request object.\n * @param fn - The function to run within the request context.\n */\nexport function runWithRequestContext<T>(\n req: Request,\n fn: () => T,\n { reactCacheScope, cookies, formState, actionError }: RequestContextOptions = {}\n): T {\n const originalCopy = new Headers(req.headers);\n const appVisible = appVisibleSearch(new URL(req.url));\n // A copy: the caller keeps its map, and this request's writes stay here.\n const seed = cookies ? new Map(cookies) : undefined;\n const store: RequestContextStore = {\n request: req,\n headers: freezeHeaders(req.headers),\n originalHeaders: originalCopy,\n // Given parsed cookies, leave the raw header empty — parseCookieHeader is the\n // exact code path the smuggling primitive abused, and lazy parsing is\n // gated on `parsedCookies` being undefined.\n cookieHeader: seed ? '' : (req.headers.get('cookie') ?? ''),\n parsedCookies: seed,\n // Not `parsedUrl.searchParams` / `parsedUrl.search`: this store is what\n // `getSearchParams()` and `redirect({ preserveSearchParams })` read, and\n // the RSC payload URL carries the framework's `_rsc` cache key that no\n // other request has (TIM-1272).\n searchParams: appVisible.params,\n searchString: appVisible.search,\n cookieJar: new Map(),\n flushed: false,\n mutableContext: false,\n formState,\n actionError,\n };\n return requestContextAls.run(store, () => runWithReactCacheScope(fn, reactCacheScope));\n}\n\n/**\n * Enable cookie mutation for the current context. Called by the framework\n * when entering middleware.ts, server actions, or route.ts handlers.\n *\n * See design/29-cookies.md §\"Context Tracking\"\n */\nexport function setMutableCookieContext(mutable: boolean): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.mutableContext = mutable;\n }\n}\n\n/**\n * Mark the response as flushed (headers committed). After this point,\n * cookie mutations log a warning instead of throwing.\n *\n * See design/29-cookies.md §\"Streaming Constraint: Post-Flush Cookie Warning\"\n */\nexport function markResponseFlushed(): void {\n const store = requestContextAls.getStore();\n if (store) {\n store.flushed = true;\n }\n}\n\n/**\n * Apply middleware-injected request headers to the current request context.\n *\n * Called by the pipeline after middleware.ts runs. Merges overlay headers\n * on top of the original request headers so downstream code (access.ts,\n * server components, server actions) sees them via `getHeaders()`.\n *\n * The original request headers are never mutated — a new frozen Headers\n * object is created with the overlay applied on top.\n *\n * See design/07-routing.md §\"Request Header Injection\"\n */\nexport function applyRequestHeaderOverlay(overlay: Headers): void {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] applyRequestHeaderOverlay() called outside of a request context.');\n }\n\n // Check if the overlay has any headers — skip if empty\n let hasOverlay = false;\n overlay.forEach(() => {\n hasOverlay = true;\n });\n if (!hasOverlay) return;\n\n // Merge: start with original headers, overlay on top\n const merged = new Headers(store.originalHeaders);\n overlay.forEach((value, key) => {\n merged.set(key, value);\n });\n store.headers = freezeHeaders(merged);\n}\n\n// ─── Read-Only Headers ────────────────────────────────────────────────────\n\nconst MUTATING_METHODS = new Set(['set', 'append', 'delete']);\n\n/**\n * Wrap a Headers object in a Proxy that throws on mutating methods.\n * Object.freeze doesn't work on Headers (native internal slots), so we\n * intercept property access and reject set/append/delete at runtime.\n *\n * Read methods (get, has, entries, etc.) must be bound to the underlying\n * Headers instance because they access private #headersList slots.\n */\nfunction freezeHeaders(source: Headers): Headers {\n const copy = new Headers(source);\n return new Proxy(copy, {\n get(target, prop) {\n if (typeof prop === 'string' && MUTATING_METHODS.has(prop)) {\n return () => {\n throw new Error(\n `[timber] getHeaders() returns a read-only Headers object. ` +\n `Calling .${prop}() is not allowed. ` +\n `Use ctx.requestHeaders in middleware to inject headers for downstream components.`\n );\n };\n }\n const value = Reflect.get(target, prop);\n // Bind methods to the real Headers instance so private slot access works\n if (typeof value === 'function') {\n return value.bind(target);\n }\n return value;\n },\n });\n}\n\n/**\n * The form state of the no-JS action this render answers, or `null`. Read by\n * the SSR renderer only (`rsc-entry/ssr-renderer.ts`), which passes it to\n * Fizz and embeds it for `hydrateRoot`. Framework-internal: not exported\n * from `@timber-js/app/server`.\n */\nexport function getFormStateForSsr(): ReactFormState | null {\n return requestContextAls.getStore()?.formState ?? null;\n}\n\n/**\n * The error of the no-JS action this render answers, or `null`. Read by the\n * route element builder, which renders it in place of the page, and by the\n * Flight `onError`, which gives it the ID the action logged it with.\n * Framework-internal: not exported from `@timber-js/app/server`.\n */\nexport function getActionErrorForRender(): ActionErrorForRender | null {\n return requestContextAls.getStore()?.actionError ?? null;\n}\n","/**\n * Cookie parsing and serialization helpers — pure string ↔ structure\n * functions with no ALS dependency. Split out of `cookie-context.ts`\n * (TIM-853) so the API surface and the wire-format codecs can each be\n * read on their own.\n *\n * The functions in this module are total over arbitrary input. They\n * never throw and never call `assertValid*` (the security validators\n * live in the API surface — `cookie-context.ts` invokes them at every\n * jar entry point so the smuggling-primitive invariant from TIM-868\n * is enforced regardless of which path produced the bytes).\n *\n * Delegates to the `cookie` package (RFC 6265, dependency-free,\n * browser-safe) for wire codecs. Adapts at the boundary to preserve\n * timber's Map-based API and never-throw contract.\n */\n\nimport { parseCookie, parseSetCookie as upstreamParseSetCookie, stringifySetCookie } from 'cookie';\n\nimport type { CookieEntry } from './als-registry.ts';\nimport type { CookieOptions } from './cookie-context.ts';\n\n/**\n * Parse a Cookie header string into a Map of name → value pairs.\n * Follows RFC 6265 §4.2.1: cookies are semicolon-separated key=value pairs.\n *\n * Values are auto-decoded with `decodeURIComponent` so they round-trip\n * losslessly with `getCookies().set()` (which auto-encodes). Malformed\n * `%`-escapes from third-party cookies fall back to the raw byte sequence\n * — the parser must be total over arbitrary inbound headers, including\n * non-conforming values from other servers, browser extensions, etc.\n */\nexport function parseCookieHeader(header: string): Map<string, string> {\n const map = new Map<string, string>();\n if (!header) return map;\n\n // cookie.parseCookie returns a plain object with first-wins semantics\n // and uses safeDecodeURIComponent by default (try/catch around\n // decodeURIComponent, falls back to raw). This matches our contract.\n const parsed = parseCookie(header);\n for (const name in parsed) {\n const value = parsed[name];\n if (value !== undefined) map.set(name, value);\n }\n\n return map;\n}\n\n/**\n * Decode a single cookie value with `decodeURIComponent`, falling back to\n * the raw byte sequence if the input contains a malformed `%`-escape.\n *\n * Used by both `parseCookieHeader` (incoming Cookie: header) and the\n * `setRaw` forwarding path (outgoing Set-Cookie from upstream services).\n * Total — never throws.\n *\n * cookie's internal decode has identical semantics but is not exported,\n * so we keep this standalone helper.\n */\nexport function safeDecodeCookieValue(raw: string): string {\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Serialize a CookieEntry into a Set-Cookie header value.\n *\n * Total — never throws. If the cookie package's serializer rejects\n * the input (e.g. non-integer maxAge, forbidden chars in name/value),\n * falls back to a minimal hand-rolled serialization so the response\n * path is never interrupted by a codec error.\n */\nexport function serializeCookieEntry(entry: CookieEntry): string {\n try {\n return stringifySetCookie(\n {\n name: entry.name,\n value: entry.value,\n domain: entry.options.domain,\n path: entry.options.path,\n expires: entry.options.expires,\n maxAge: entry.options.maxAge,\n httpOnly: entry.options.httpOnly,\n secure: entry.options.secure,\n sameSite: entry.options.sameSite,\n partitioned: entry.options.partitioned,\n },\n // timber pre-encodes values at the API surface (set() calls\n // encodeURIComponent), so pass identity to avoid double-encoding.\n { encode: (v: string) => v }\n );\n } catch {\n // Fallback: hand-roll a minimal Set-Cookie so we never drop the\n // header entirely. The entry-point validators in cookie-context.ts\n // already reject truly dangerous input — this path only fires for\n // edge-case formatting the upstream serializer is strict about.\n return serializeCookieEntryFallback(entry);\n }\n}\n\nfunction serializeCookieEntryFallback(entry: CookieEntry): string {\n const parts = [`${entry.name}=${entry.value}`];\n const opts = entry.options;\n\n if (opts.domain) parts.push(`Domain=${opts.domain}`);\n if (opts.path) parts.push(`Path=${opts.path}`);\n if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);\n if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`);\n if (opts.httpOnly) parts.push('HttpOnly');\n if (opts.secure) parts.push('Secure');\n if (opts.sameSite) {\n parts.push(`SameSite=${opts.sameSite.charAt(0).toUpperCase()}${opts.sameSite.slice(1)}`);\n }\n if (opts.partitioned) parts.push('Partitioned');\n\n return parts.join('; ');\n}\n\n/**\n * Parse a raw `Set-Cookie` header string into name, value, and options.\n * Handles all standard attributes: Path, Domain, Max-Age, Expires,\n * SameSite, Secure, HttpOnly, Partitioned.\n *\n * Does NOT apply DEFAULT_COOKIE_OPTIONS — the caller decides whether\n * to merge defaults (e.g. `set()` does, but `setRaw()` should preserve\n * the original header's intent).\n */\nexport function parseSetCookie(\n header: string\n): { name: string; value: string; options: CookieOptions } | null {\n const parsed = upstreamParseSetCookie(header, {\n // Don't decode — setRaw expects the wire-form value so it can\n // re-emit verbatim and validate against cookie-octet.\n decode: (v: string) => v,\n });\n\n // cookie.parseSetCookie always returns a SetCookie object, but\n // with name/value as empty strings when the header has no `=`.\n if (!parsed.name) return null;\n\n const options: CookieOptions = {};\n\n if (parsed.path !== undefined) options.path = parsed.path || '/';\n if (parsed.domain !== undefined) options.domain = parsed.domain;\n if (parsed.maxAge !== undefined && Number.isFinite(parsed.maxAge)) {\n options.maxAge = parsed.maxAge;\n }\n if (parsed.expires !== undefined) options.expires = parsed.expires;\n if (parsed.sameSite !== undefined && parsed.sameSite !== true) {\n const sameSite = (parsed.sameSite as string).toLowerCase();\n if (sameSite === 'strict' || sameSite === 'lax' || sameSite === 'none') {\n options.sameSite = sameSite;\n }\n }\n if (parsed.secure) options.secure = true;\n if (parsed.httpOnly) options.httpOnly = true;\n if (parsed.partitioned) options.partitioned = true;\n\n return { name: parsed.name, value: parsed.value ?? '', options };\n}\n","/**\n * Cookie Context — per-request cookie API and on-the-wire helpers.\n *\n * Split out of `request-context.ts` (TIM-853) so the cookie subsystem\n * — encoding contract, options grammar, parser, serializer, and RYW map\n * — lives in one file. The headers/scope/params APIs\n * stay in `request-context.ts` and call into this module via the\n * exported helpers.\n *\n * See design/29-cookies.md for the encoding contract and read-your-own-\n * writes semantics. See ONGOING_SECURITY.md H-3 (TIM-868) for the\n * smuggling primitive that the encoding contract closes.\n */\n\nimport { requestContextAls, type RequestContextStore } from './als-registry.ts';\nimport { isDebug } from './debug.ts';\nimport {\n ANY_COOKIE,\n noteCookieReadForReactCache,\n warnIfCachedCookieIsWritten,\n} from './react-cache-scope.ts';\nimport {\n assertValidCookieName,\n assertValidCookieValue,\n assertValidCookieOptions,\n} from '../cookies/validation.ts';\nimport {\n parseCookieHeader,\n parseSetCookie,\n safeDecodeCookieValue,\n serializeCookieEntry,\n} from './cookie-parsing.ts';\n\n// Re-export the validators so framework-internal consumers and tests can\n// import the canonical implementation from the same module that hosts\n// `getCookies()`. The pure shared module lives in `cookies/validation.ts`\n// so the client `useCookie` hook can use the same checks without pulling\n// in the server ALS code.\nexport { assertValidCookieName, assertValidCookieValue, assertValidCookieOptions };\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns a cookie accessor for the current request.\n *\n * Available in middleware, access checks, server components, and server actions.\n * Throws if called outside a request context (security principle #2: no global fallback).\n *\n * Read methods (.get, .has, .getAll) are always available and reflect\n * read-your-own-writes from .set() calls in the same request.\n *\n * Mutation methods (.set, .delete, .clear) are only available in mutable\n * contexts (middleware.ts, server actions, route.ts handlers). Calling them\n * in read-only contexts (access.ts, server components) throws.\n *\n * This is the escape hatch for direct cookie jar operations. For typed\n * cookie access, use `defineCookie()` instead.\n *\n * See design/29-cookies.md\n */\nexport function getCookieJar(): RequestCookies {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getCookieJar() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n\n // Parse cookies lazily on first access\n if (!store.parsedCookies) {\n store.parsedCookies = parseCookieHeader(store.cookieHeader);\n }\n\n const map = store.parsedCookies;\n return {\n // Reads are noted (dev only) so a later write of the same cookie can\n // warn if React.cache may have memoized the old value (TIM-1529 D3).\n get(name: string): string | undefined {\n noteCookieReadForReactCache(name);\n return map.get(name);\n },\n has(name: string): boolean {\n noteCookieReadForReactCache(name);\n return map.has(name);\n },\n getAll(): Array<{ name: string; value: string }> {\n noteCookieReadForReactCache(ANY_COOKIE);\n return Array.from(map.entries()).map(([name, value]) => ({ name, value }));\n },\n get size(): number {\n noteCookieReadForReactCache(ANY_COOKIE);\n return map.size;\n },\n\n set(name: string, value: string, options?: SetCookieOptions): void {\n assertMutable(store, 'set');\n // Validate the name first — names cannot be URL-encoded (RFC 7230\n // token grammar is strict), so a bad name is always a bug.\n assertValidCookieName(name);\n // Type guard with a guiding error. The single most common mistake\n // is passing a non-string (object, number, Date) and expecting the\n // framework to JSON-encode. Point developers at jsonCookieCodec.\n if (typeof value !== 'string') {\n throw new Error(\n `[timber] getCookieJar().set(${JSON.stringify(name)}, …): value must be a string, got ${typeof value}.\\n` +\n ` To store a JSON-serializable value, use defineCookie + jsonCookieCodec:\\n` +\n `\\n` +\n ` import { defineCookie, jsonCookieCodec } from '@timber-js/app/cookies';\\n` +\n `\\n` +\n ` export const ${name}Cookie = defineCookie(${JSON.stringify(name)}, {\\n` +\n ` codec: jsonCookieCodec(),\\n` +\n ` });\\n` +\n `\\n` +\n ` ${name}Cookie.set(value);\\n` +\n `\\n` +\n ` See design/29-cookies.md §\"Typed Cookies with Schema Validation\".`\n );\n }\n // Encode the value so the on-the-wire bytes always satisfy\n // RFC 6265 §4.1.1 cookie-octet. encodeURIComponent's output is a\n // strict subset of cookie-octet (only `A-Z a-z 0-9 ! ' ( ) * - . _\n // ~ %`), so the encoded form can never carry the H-3 smuggling\n // primitive — `;` becomes `%3B`, CR/LF become `%0D`/`%0A`, etc.\n // The round-trip is lossless: parseCookieHeader auto-decodes on\n // read, so `cookies().get(name)` returns exactly `value`. See\n // ONGOING_SECURITY.md H-3 (TIM-868) and design/29-cookies.md\n // §\"Encoding Contract\".\n //\n // The `{ raw: true }` opt-out skips the encoder for callers who\n // need exact byte control (e.g. forwarding pre-encoded cookies\n // from an upstream service). The opt-out goes through the strict\n // cookie-octet validator instead — the smuggling primitive cannot\n // sneak in via the escape hatch.\n const raw = options?.raw === true;\n const wireValue = raw ? value : encodeURIComponent(value);\n if (raw) {\n assertValidCookieValue(name, wireValue);\n }\n if (store.flushed) {\n if (isDebug()) {\n console.warn(\n `[timber] warn: getCookieJar().set('${name}') called after response headers were committed.\\n` +\n ` The cookie will NOT be sent. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n }\n return;\n }\n // Strip the framework-only `raw` flag before persisting — it is\n // not an HTTP cookie attribute and must not leak into the jar.\n const { raw: _raw, ...attributeOptions } = options ?? {};\n void _raw;\n const opts = { ...DEFAULT_COOKIE_OPTIONS, ...attributeOptions };\n assertValidCookieOptions(opts);\n store.cookieJar.set(name, { name, value: wireValue, options: opts });\n // Read-your-own-writes: store the DECODED logical value so that\n // subsequent `cookies().get(name)` in the same request returns\n // exactly what the developer wrote — never the encoded form.\n // For `{ raw: true }`, the wire form IS the logical form.\n map.set(name, raw ? wireValue : value);\n warnIfCachedCookieIsWritten(name);\n },\n\n setFromHeaders(headers: Headers): void {\n assertMutable(store, 'setFromHeaders');\n if (store.flushed) {\n console.warn(\n `[timber] warn: getCookieJar().setFromHeaders() called after response headers were committed.\\n` +\n ` The cookies will NOT be sent. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n return;\n }\n // Headers.getSetCookie() returns individual Set-Cookie strings,\n // avoiding the fragile comma-splitting that raw .get() requires.\n for (const raw of headers.getSetCookie()) {\n const parsed = parseSetCookie(raw);\n if (parsed) {\n // Use setRaw to preserve the original header's attributes without\n // merging DEFAULT_COOKIE_OPTIONS (parseSetCookie intentionally\n // does not apply defaults — see its doc comment).\n setRaw(store, map, parsed.name, parsed.value, parsed.options);\n }\n }\n },\n\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void {\n assertMutable(store, 'delete');\n // Validate the name even though delete writes an empty value — a\n // smuggled name (`;` or CR/LF) would corrupt the Set-Cookie header\n // we emit. See ONGOING_SECURITY.md H-3 (TIM-868).\n assertValidCookieName(name);\n if (store.flushed) {\n if (isDebug()) {\n console.warn(\n `[timber] warn: getCookieJar().delete('${name}') called after response headers were committed.\\n` +\n ` The cookie will NOT be deleted. Move cookie mutations to middleware.ts, a server action,\\n` +\n ` or a route.ts handler.`\n );\n }\n return;\n }\n const opts: CookieOptions = {\n ...DEFAULT_COOKIE_OPTIONS,\n ...options,\n maxAge: 0,\n expires: new Date(0),\n };\n // delete() is a jar entry point like set() — caller-supplied\n // path/domain are serialized verbatim into the Set-Cookie header,\n // so they must pass the same attribute-injection guard. See\n // design/29-cookies.md §\"Cookie Attribute Validation\" (TIM-1026).\n assertValidCookieOptions(opts);\n store.cookieJar.set(name, { name, value: '', options: opts });\n // Remove from read view\n map.delete(name);\n warnIfCachedCookieIsWritten(name);\n },\n\n clear(): void {\n assertMutable(store, 'clear');\n if (store.flushed) return;\n // Delete every incoming cookie\n for (const name of Array.from(map.keys())) {\n store.cookieJar.set(name, {\n name,\n value: '',\n options: { ...DEFAULT_COOKIE_OPTIONS, maxAge: 0, expires: new Date(0) },\n });\n }\n map.clear();\n warnIfCachedCookieIsWritten(ANY_COOKIE);\n },\n\n toString(): string {\n noteCookieReadForReactCache(ANY_COOKIE);\n // Re-encode values when serializing as a Cookie header — the\n // RYW map holds decoded logical values, but a Cookie header has\n // to satisfy `cookie-octet`. Mirror the auto-encode contract on\n // `set()` so toString() round-trips losslessly with parseCookieHeader.\n return Array.from(map.entries())\n .map(([name, value]) => `${name}=${encodeURIComponent(value)}`)\n .join('; ');\n },\n };\n}\n\n/**\n * Returns the value of a single cookie, or undefined if absent.\n *\n * @internal — not part of the public API. Use `defineCookie().get()` or `getCookieJar().get()` instead.\n */\nexport function getCookie(name: string): string | undefined {\n const jar = getCookieJar();\n return jar.get(name);\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\n/**\n * Per-call options for `getCookies().set()`. Extends the persistent\n * `CookieOptions` (HTTP cookie attributes) with framework-only flags\n * that are NOT serialized into the Set-Cookie header.\n *\n * The `raw` flag is the escape hatch for the auto-encoding contract.\n * See design/29-cookies.md §\"Encoding Contract\".\n */\nexport interface SetCookieOptions extends CookieOptions {\n /**\n * Skip the framework's `encodeURIComponent` pass and store the value\n * verbatim. The value is then validated against the strict RFC 6265\n * §4.1.1 `cookie-octet` grammar — the H-3 smuggling primitive cannot\n * sneak in via this opt-out.\n *\n * Use this when forwarding a cookie value that is already in its\n * intended on-the-wire form (e.g. mirroring an upstream service's\n * Set-Cookie). Default: `false` (auto-encode).\n */\n raw?: boolean;\n}\n\n/** Options for setting a cookie. See design/29-cookies.md. */\nexport interface CookieOptions {\n /** Domain scope. Default: omitted (current domain only). */\n domain?: string;\n /** URL path scope. Default: '/'. */\n path?: string;\n /** Expiration date. Mutually exclusive with maxAge. */\n expires?: Date;\n /** Max age in seconds. Mutually exclusive with expires. */\n maxAge?: number;\n /** Prevent client-side JS access. Default: true. */\n httpOnly?: boolean;\n /** Only send over HTTPS. Default: true. */\n secure?: boolean;\n /** Cross-site request policy. Default: 'lax'. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Partitioned (CHIPS) — isolate cookie per top-level site. Default: false. */\n partitioned?: boolean;\n}\n\n/**\n * Cookie accessor returned by `getCookies()`.\n *\n * Read methods are always available. Mutation methods throw in read-only\n * contexts (access.ts, server components).\n */\nexport interface RequestCookies {\n /** Get a cookie value by name. Returns undefined if not present. */\n get(name: string): string | undefined;\n /** Check if a cookie exists. */\n has(name: string): boolean;\n /** Get all cookies as an array of { name, value } pairs. */\n getAll(): Array<{ name: string; value: string }>;\n /** Number of cookies. */\n readonly size: number;\n /**\n * Set a cookie. Only available in mutable contexts (middleware, actions,\n * route handlers).\n *\n * The value is auto-encoded with `encodeURIComponent` so the on-the-wire\n * bytes always satisfy RFC 6265 §4.1.1 cookie-octet — `cookies().get()`\n * returns the same logical value the developer wrote. Pass `{ raw: true }`\n * to skip the encoder; the raw path validates against the strict\n * cookie-octet grammar instead. See design/29-cookies.md §\"Encoding\n * Contract\" and ONGOING_SECURITY.md H-3 (TIM-868).\n */\n set(name: string, value: string, options?: SetCookieOptions): void;\n /**\n * Copy all `Set-Cookie` headers from a `Headers` object.\n * Parses each header and forwards name, value, and all attributes\n * (path, domain, max-age, expires, sameSite, secure, httpOnly, partitioned).\n *\n * Useful when forwarding cookies from an internal `fetch()` or auth handler:\n * ```ts\n * const response = await auth.handler(req);\n * getCookies().then(c => c.setFromHeaders(response.headers));\n * ```\n */\n setFromHeaders(headers: Headers): void;\n /** Delete a cookie. Only available in mutable contexts. */\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void;\n /** Delete all cookies. Only available in mutable contexts. */\n clear(): void;\n /** Serialize cookies as a Cookie header string. */\n toString(): string;\n}\n\nconst DEFAULT_COOKIE_OPTIONS: CookieOptions = {\n path: '/',\n httpOnly: true,\n secure: true,\n sameSite: 'lax',\n};\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Build a Map of cookie name → value reflecting the current request's\n * read-your-own-writes state. Includes incoming cookies plus any\n * mutations from getCookies().set() / getCookies().delete() in the same request.\n *\n * Used by SSR renderers to populate NavContext.cookies so that\n * useCookie()'s server snapshot matches the actual response state.\n *\n * See design/29-cookies.md §\"Read-Your-Own-Writes\"\n * See design/triage/TIM-441-cookie-api-triage.md §4\n */\nexport function getCookiesForSsr(): Map<string, string> {\n const store = requestContextAls.getStore();\n if (!store) {\n throw new Error('[timber] getCookiesForSsr() called outside of a request context.');\n }\n\n // Trigger lazy parsing if not yet done\n if (!store.parsedCookies) {\n store.parsedCookies = parseCookieHeader(store.cookieHeader);\n }\n\n // The parsedCookies map already reflects read-your-own-writes:\n // - getCookies().set() updates the map via map.set(name, value)\n // - getCookies().delete() removes from the map via map.delete(name)\n // Return a copy so callers can't mutate the internal map.\n return new Map(store.parsedCookies);\n}\n\n/**\n * Collect all Set-Cookie headers from the cookie jar.\n * Called by the framework at flush time to apply cookies to the response.\n *\n * Returns an array of serialized Set-Cookie header values.\n */\nexport function getSetCookieHeaders(): string[] {\n const store = requestContextAls.getStore();\n if (!store) return [];\n return Array.from(store.cookieJar.values()).map(serializeCookieEntry);\n}\n\n// ─── Cookie Helpers ───────────────────────────────────────────────────────\n\n/** Throw if cookie mutation is attempted in a read-only context. */\nfunction assertMutable(store: RequestContextStore, method: string): void {\n if (!store.mutableContext) {\n throw new Error(\n `[timber] getCookieJar().${method}() cannot be called in this context.\\n` +\n ` Set cookies in middleware.ts, server actions, or route.ts handlers.`\n );\n }\n}\n\n/**\n * Write a cookie to the jar WITHOUT merging DEFAULT_COOKIE_OPTIONS.\n * Used by setFromHeaders to preserve the original header's attributes exactly.\n *\n * For deletion cookies (maxAge=0), the jar entry is still created so the\n * Set-Cookie header is emitted, but the cookie is NOT added to the read map\n * (it would be misleading — the cookie is being deleted).\n */\nfunction setRaw(\n store: RequestContextStore,\n readMap: Map<string, string>,\n name: string,\n value: string,\n options: CookieOptions\n): void {\n // setRaw is the forwarding path for upstream Set-Cookie headers\n // (`getCookies().setFromHeaders(response.headers)`). The value comes\n // out of `parseSetCookie` in its on-the-wire form — already encoded\n // by whoever produced it — so we re-emit it verbatim into our jar.\n // We DO validate against the strict cookie-octet grammar, both as\n // defense-in-depth against a malicious upstream and to keep the\n // ONGOING_SECURITY.md H-3 invariant intact at every entry point into\n // the cookie jar / RYW map.\n assertValidCookieName(name);\n assertValidCookieValue(name, value);\n // Options get the same entry-point guard as set()/delete(). For the\n // setFromHeaders path this can only fire on attributes parseSetCookie\n // cannot produce (it splits on `;` and normalizes Max-Age/SameSite),\n // but setRaw is the jar entry point — the invariant lives here, not in\n // the caller. See design/29-cookies.md §\"Cookie Attribute Validation\".\n assertValidCookieOptions(options);\n store.cookieJar.set(name, { name, value, options });\n warnIfCachedCookieIsWritten(name);\n // Deletion cookies (Max-Age=0) should not appear in the read map.\n if (options.maxAge === 0) {\n readMap.delete(name);\n } else {\n // Decode for the RYW map so consumers see the same logical bytes\n // they would see if the upstream cookie had arrived on the next\n // request. Mirrors `parseCookieHeader`'s auto-decode.\n readMap.set(name, safeDecodeCookieValue(value));\n }\n}\n","// Server-side primitives: deny, redirect, redirectExternal, waitUntil\n//\n// These are the core runtime signals that components, middleware, and access gates\n// use to control request flow. See design/10-error-handling.md.\n\nimport type { JsonSerializable } from './types.ts';\nimport { getWaitUntil as _getWaitUntil } from './waituntil-bridge.ts';\nimport { isDebug } from './debug.ts';\nimport { getRequestSearchString } from './request-context.ts';\nimport { mergePreservedSearchParams } from '../shared/merge-search-params.ts';\nimport { DENY_BRAND, REDIRECT_BRAND } from './signal-identity.ts';\nimport {\n assertRelativeRedirectPath,\n validateExternalRedirectUrl,\n} from '../shared/href-validation.ts';\n\n// ─── Dev-mode validation ────────────────────────────────────────────────────\n\n/**\n * Check if a value is JSON-serializable without data loss.\n * Returns a description of the first non-serializable value found, or null if OK.\n *\n * @internal Exported for testing only.\n */\nexport function findNonSerializable(value: unknown, path = 'data'): string | null {\n if (value === null || value === undefined) return null;\n\n switch (typeof value) {\n case 'string':\n case 'number':\n case 'boolean':\n return null;\n case 'bigint':\n return `${path} contains a BigInt — BigInt throws in JSON.stringify`;\n case 'function':\n return `${path} is a function — functions are not JSON-serializable`;\n case 'symbol':\n return `${path} is a symbol — symbols are not JSON-serializable`;\n case 'object':\n break;\n default:\n return `${path} has unsupported type \"${typeof value}\"`;\n }\n\n if (value instanceof Date) {\n return `${path} is a Date — Dates silently coerce to strings in JSON.stringify`;\n }\n if (value instanceof Map) {\n return `${path} is a Map — Maps serialize as {} in JSON.stringify (data loss)`;\n }\n if (value instanceof Set) {\n return `${path} is a Set — Sets serialize as {} in JSON.stringify (data loss)`;\n }\n if (value instanceof RegExp) {\n return `${path} is a RegExp — RegExps serialize as {} in JSON.stringify`;\n }\n if (value instanceof Error) {\n return `${path} is an Error — Errors serialize as {} in JSON.stringify`;\n }\n\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const result = findNonSerializable(value[i], `${path}[${i}]`);\n if (result) return result;\n }\n return null;\n }\n\n // Plain object — only Object.prototype is safe. Null-prototype objects\n // (Object.create(null)) survive JSON.stringify but React Flight rejects\n // them with \"Classes or null prototypes are not supported\", so the\n // pre-flush deny path (renderDenyPage → renderToReadableStream) would throw.\n const proto = Object.getPrototypeOf(value);\n if (proto === null) {\n return `${path} is a null-prototype object — React Flight rejects null prototypes`;\n }\n if (proto !== Object.prototype) {\n const name = (value as object).constructor?.name ?? 'unknown';\n return `${path} is a ${name} instance — class instances may lose data in JSON.stringify`;\n }\n\n for (const key of Object.keys(value as Record<string, unknown>)) {\n const result = findNonSerializable((value as Record<string, unknown>)[key], `${path}.${key}`);\n if (result) return result;\n }\n return null;\n}\n\n/**\n * Emit a dev-mode warning if data is not JSON-serializable.\n * No-op in production.\n */\nfunction warnIfNotSerializable(data: unknown, callerName: string): void {\n if (!isDebug()) return;\n if (data === undefined) return;\n\n const issue = findNonSerializable(data);\n if (issue) {\n console.warn(\n `[timber] ${callerName}: ${issue}. ` +\n 'Data passed to deny() must be JSON-serializable because ' +\n 'the post-flush path uses JSON.stringify, not React Flight.'\n );\n }\n}\n\n// ─── DenySignal ─────────────────────────────────────────────────────────────\n\n/**\n * Render-phase signal thrown by `deny()`. Caught by the framework to produce\n * the correct HTTP status code (segment context) or graceful degradation (slot context).\n *\n * Detect with `isDenySignal()`, never `instanceof` — see \"Signal branding\" in signal-identity.ts.\n */\nexport class DenySignal extends Error {\n readonly [DENY_BRAND] = true;\n readonly status: number;\n readonly data: JsonSerializable | undefined;\n\n /**\n * Ordered list of segment keys that own matching deny pages for this\n * signal, best match first. Present = addressed (a boundary should\n * render it); absent = unplaced (the re-render fallback serves it).\n *\n * In-tree hoists stamp a single-element list (`[entry.ownerKey]`) —\n * identical to the pre-TIM-1450 `ownerKey`. Late addressing (TIM-1450)\n * stamps the full candidate list so the client can find the first\n * *reachable* owner via its ancestry context, eliminating the\n * owner-below-throw-site escape.\n *\n * See design/04-authorization.md §\"Where a Deny Page Renders\", TIM-1356.\n */\n owners?: string[];\n\n /**\n * When true, the pipeline must respond with `rscErrorEnvelope` so the\n * client hard-navigates instead of attempting to render a deny page\n * from the RSC payload. Set by AccessGate when a skipped segment\n * denies — rendering the deny page in-tree would appear inside the\n * client's cached layout chrome (TIM-1074). See TIM-1363.\n */\n _hardNavigate?: boolean;\n\n constructor(status: number, data?: JsonSerializable) {\n super(`Access denied with status ${status}`);\n this.name = 'DenySignal';\n this.status = status;\n this.data = data;\n }\n\n /**\n * Extract the file that called deny() from the stack trace.\n * Returns a short path (e.g. \"app/auth/access.ts\") or undefined if\n * the stack can't be parsed. Dev-only — used for dev log output.\n */\n get sourceFile(): string | undefined {\n if (!this.stack) return undefined;\n const frames = this.stack.split('\\n');\n // Skip the Error line and the deny() frame — the caller is the 3rd line.\n // Stack format: \" at FnName (file:line:col)\" or \" at file:line:col\"\n for (let i = 2; i < frames.length; i++) {\n const frame = frames[i];\n if (!frame) continue;\n // Skip framework internals\n if (frame.includes('primitives.ts') || frame.includes('node_modules')) continue;\n // Extract file path from the frame\n const match =\n frame.match(/\\(([^)]+?)(?::\\d+:\\d+)\\)/) ?? frame.match(/at\\s+([^\\s]+?)(?::\\d+:\\d+)/);\n if (match?.[1]) {\n // Shorten to app-relative path\n const full = match[1];\n const appIdx = full.indexOf('/app/');\n return appIdx >= 0 ? full.slice(appIdx + 1) : full;\n }\n }\n return undefined;\n }\n}\n\n/** Options for deny(). */\nexport interface DenyOptions {\n /** Human-readable message (logged server-side, not sent to client). */\n message?: string;\n /**\n * JSON-serializable data forwarded as the `data` prop to status-code files\n * and `denied.tsx`. Named `dangerouslyPassData` because this data crosses\n * the RSC → client serialization boundary — do not pass sensitive server\n * state (tokens, internal IDs, database rows).\n */\n dangerouslyPassData?: JsonSerializable;\n}\n\n/**\n * Universal denial/error primitive. Throws a `DenySignal` that the framework catches.\n *\n * - In segment context (outside Suspense): produces HTTP status code\n * - In slot context: graceful degradation → denied.tsx → default.tsx → null\n * - Inside Suspense (hold window): promoted to pre-flush behavior\n * - Inside Suspense (after flush): error boundary + noindex meta\n *\n * ```ts\n * deny() // 403 (default)\n * deny(404) // 404\n * deny(503, { message: 'Maintenance' }) // server-only log message\n * deny(404, { dangerouslyPassData: { resourceId: params.id } }) // explicit client opt-in\n * ```\n *\n * Accepts any 4xx or 5xx status code — it is also how a page renders a 5xx\n * status file with data. An unplanned throw needs no primitive: it renders\n * the error page with a 500.\n *\n * @param status - HTTP status code (4xx or 5xx). Default: 403.\n * @param options - Optional message and/or data to pass to the client.\n */\nexport function deny(status?: number, options?: DenyOptions): never {\n const resolvedStatus = status ?? 403;\n const resolvedData = options?.dangerouslyPassData;\n\n if (resolvedStatus < 400 || resolvedStatus > 599) {\n throw new Error(`deny() requires a 4xx or 5xx status code, got ${resolvedStatus}.`);\n }\n warnIfNotSerializable(resolvedData, 'deny()');\n throw new DenySignal(resolvedStatus, resolvedData);\n}\n\n// Single source of truth shared with the client next/navigation shim —\n// see shared/redirect-type.ts.\nexport { RedirectType } from '../shared/redirect-type.ts';\n\n// ─── RedirectSignal ─────────────────────────────────────────────────────────\n\n/**\n * Render-phase signal thrown by `redirect()` and `redirectExternal()`.\n * Caught by the framework to produce a 3xx response or client-side navigation.\n *\n * Detect with `isRedirectSignal()`, never `instanceof` — see \"Signal branding\" in signal-identity.ts.\n */\nexport class RedirectSignal extends Error {\n readonly [REDIRECT_BRAND] = true;\n readonly location: string;\n readonly status: number;\n\n constructor(location: string, status: number) {\n super(`Redirect to ${location}`);\n this.name = 'RedirectSignal';\n this.location = location;\n this.status = status;\n }\n}\n\n// ─── Signal Detection ───────────────────────────────────────────────────────\n\n/**\n * Options for redirect() — alternative to passing a bare status code.\n */\nexport interface RedirectOptions {\n /** HTTP redirect status code (3xx). Defaults to 302 (or 308 when `permanent: true`). */\n status?: number;\n /**\n * When true, defaults the status to 308 (Permanent Redirect, preserves HTTP method).\n * If `status` is also provided, `status` takes precedence.\n *\n * @example\n * redirect('/new-path', { permanent: true }); // 308\n * redirect('/new-path', { permanent: true, status: 301 }); // 301\n */\n permanent?: boolean;\n /**\n * Preserve search params from the current request URL on the redirect target.\n *\n * - `true` — preserve ALL current search params (target params take precedence)\n * - `string[]` — preserve only the named params (e.g. `['private', 'token']`)\n *\n * Target path's own query params always take precedence over preserved ones.\n */\n preserveSearchParams?: true | string[];\n}\n\n/**\n * Redirect to a relative path. Rejects absolute and protocol-relative URLs.\n * Use `redirectExternal()` for external redirects with an allow-list.\n *\n * @param path - Relative path (e.g. '/login', 'settings', '/login?returnTo=/dash')\n * @param statusOrOptions - HTTP status code (3xx, default 302) or options object.\n *\n * @example\n * // Simple redirect\n * redirect('/login');\n *\n * // With status code\n * redirect('/login', 301);\n *\n * // With preserved search params\n * redirect(`/docs/${version}/${slug}`, { preserveSearchParams: ['foo'] });\n */\nexport function redirect(path: string, statusOrOptions?: number | RedirectOptions): never {\n let status: number;\n let preserveSearchParams: true | string[] | undefined;\n\n if (typeof statusOrOptions === 'number') {\n status = statusOrOptions;\n } else if (statusOrOptions) {\n // Explicit status wins. Otherwise permanent: true → 308, default → 302.\n status = statusOrOptions.status ?? (statusOrOptions.permanent ? 308 : 302);\n preserveSearchParams = statusOrOptions.preserveSearchParams;\n } else {\n status = 302;\n }\n\n if (status < 300 || status > 399) {\n throw new Error(`redirect() requires a 3xx status code, got ${status}.`);\n }\n // Relative-only validation shared with the client next/navigation shim —\n // strips C0 controls and normalizes backslashes before the scheme check.\n assertRelativeRedirectPath(path);\n\n let resolvedPath = path;\n if (preserveSearchParams) {\n const currentSearch = getRequestSearchString();\n resolvedPath = mergePreservedSearchParams(path, currentSearch, preserveSearchParams);\n }\n\n throw new RedirectSignal(resolvedPath, status);\n}\n\n/**\n * Redirect to an external URL. The origin must be in the provided allow-list.\n *\n * Only http: and https: schemes are permitted. The allow-list is matched\n * against full origins (scheme + host + port), not bare hostnames.\n *\n * @param url - Absolute URL to redirect to.\n * @param allowList - Array of allowed origins (e.g. ['https://example.com', 'https://auth.example.com']).\n * @param status - HTTP redirect status code (3xx). Defaults to 302.\n */\nexport function redirectExternal(url: string, allowList: string[], status: number = 302): never {\n if (status < 300 || status > 399) {\n throw new Error(`redirectExternal() requires a 3xx status code, got ${status}.`);\n }\n\n const canonicalUrl = validateExternalRedirectUrl(url, allowList);\n throw new RedirectSignal(canonicalUrl, status);\n}\n\n// ─── waitUntil ──────────────────────────────────────────────────────────────\n\n// Intentional per-app singleton — warn-once flag that persists for the\n// lifetime of the process/isolate. Not per-request; do not migrate to ALS.\nlet _waitUntilWarned = false;\n\n/**\n * Register a promise to be kept alive after the response is sent.\n * Maps to `ctx.waitUntil()` on Cloudflare Workers and similar platforms.\n *\n * The platform adapter installs a per-request waitUntil function via ALS\n * (see waituntil-bridge.ts). If no ALS handler is available, a warning\n * is logged once and the promise is left to resolve (or reject) without\n * being tracked.\n *\n * @param promise - The background work to keep alive.\n */\nexport function waitUntil(promise: Promise<unknown>): void {\n const alsFn = _getWaitUntil();\n if (alsFn) {\n alsFn(promise);\n return;\n }\n\n if (!_waitUntilWarned) {\n _waitUntilWarned = true;\n console.warn(\n '[timber] waitUntil() is not supported by the current adapter. ' +\n 'Background work will not be tracked. This warning is shown once.'\n );\n }\n}\n\n/**\n * Reset the waitUntil warning state. Exported for testing only.\n * @internal\n */\nexport function _resetWaitUntilWarning(): void {\n _waitUntilWarned = false;\n}\n","/**\n * FormData preprocessing — schema-agnostic conversion of FormData to typed objects.\n *\n * FormData is all strings. Schema validation expects typed values. This module\n * bridges the gap with intelligent coercion that runs *before* schema validation.\n *\n * Inspired by zod-form-data, but schema-agnostic — works with any Standard Schema\n * library (Zod, Valibot, ArkType).\n *\n * See design/08-forms-and-actions.md §\"parseFormData() and coerce helpers\"\n */\n\n// ─── parseFormData ───────────────────────────────────────────────────────\n\n/**\n * Convert FormData into a plain object with intelligent coercion.\n *\n * Handles:\n * - **Duplicate keys → arrays**: `tags=js&tags=ts` → `{ tags: [\"js\", \"ts\"] }`\n * - **Nested dot-paths**: `user.name=Alice` → `{ user: { name: \"Alice\" } }`\n * - **Indexed lists → arrays**: `rows.0.name=A&rows.1.name=B` →\n * `{ rows: [{ name: \"A\" }, { name: \"B\" }] }`, when the indexes are exactly\n * `0..n-1`. A list with a gap stays an object keyed by index.\n * - **Empty strings stay `\"\"`**: a blank text input is `\"\"`, so\n * `z.string().min(1, 'Required')` reports its own message. Use\n * `coerce.text` to make a blank optional field `undefined`\n * - **Empty Files → undefined**: File inputs with no selection become `undefined`\n * - **Strips `$ACTION_*` fields**: React's internal hidden fields are excluded\n * - **Drops `__proto__` paths and names deeper than 32 segments**\n */\nexport function parseFormData(formData: FormData): Record<string, unknown> {\n const flat: Record<string, unknown> = {};\n\n for (const key of new Set(formData.keys())) {\n // Skip React internal fields\n if (key.startsWith('$ACTION_')) continue;\n\n // Skip prototype-pollution keys at the top level — assigning to\n // `flat['__proto__']` invokes the prototype setter and can splice\n // attacker-controlled data into `flat`'s prototype chain.\n // The dot-path case is filtered separately in `expandDotPaths`.\n if (DANGEROUS_KEYS.has(key)) continue;\n\n const values = formData.getAll(key);\n const processed = values.map(normalizeValue);\n\n if (processed.length === 1) {\n flat[key] = processed[0];\n } else {\n // Filter out undefined (empty Files) and empty strings from multi-value fields.\n // Multi-value fields (e.g. tags=js&tags=&tags=ts) should not include blanks.\n flat[key] = processed.filter((v) => v !== undefined && v !== '');\n }\n }\n\n // Expand dot-notation paths into nested objects\n return expandDotPaths(flat);\n}\n\n/**\n * Normalize a single FormData entry value.\n * - Empty File objects (no selection) → undefined\n * - Strings pass through as-is (empty strings stay as empty strings so\n * schema validators like `z.string().min(1, 'Required')` can produce\n * their custom error messages instead of a generic type mismatch)\n * - Everything else passes through as-is\n */\nfunction normalizeValue(value: FormDataEntryValue): unknown {\n // File input with no selection: browsers submit a File with name=\"\" and size=0\n if (value instanceof File && value.size === 0 && value.name === '') {\n return undefined;\n }\n\n return value;\n}\n\n/**\n * Expand dot-notation keys into nested objects.\n * `{ \"user.name\": \"Alice\", \"user.age\": \"30\" }` → `{ user: { name: \"Alice\", age: \"30\" } }`\n *\n * Keys without dots are left as-is. Bracket notation (e.g. `items[0]`) is NOT\n * supported — use dot notation (`items.0`) instead.\n */\nfunction expandDotPaths(flat: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n let hasDotPaths = false;\n\n // First pass: check if any keys have dots\n for (const key of Object.keys(flat)) {\n if (key.includes('.')) {\n hasDotPaths = true;\n break;\n }\n }\n\n // Fast path: no dot-notation keys, return as-is.\n // Top-level dangerous keys are already filtered in `parseFormData` above.\n if (!hasDotPaths) return flat;\n\n for (const [key, value] of Object.entries(flat)) {\n // Reject any path that contains __proto__ / constructor / prototype.\n // Without this, `__proto__.x=1` writes to Object.prototype because\n // `result['__proto__']` resolves to the prototype object itself.\n const parts = key.split('.');\n if (parts.some((p) => DANGEROUS_KEYS.has(p))) continue;\n // Bound the nesting depth. Every walk over the parsed value recurses\n // once per level (list conversion here, then file and sensitive-field\n // stripping and schema validation), and a 10 KB key of 5,000 segments\n // fits well inside the body limits. No form nests this deep.\n if (parts.length > MAX_PATH_DEPTH) continue;\n\n if (parts.length === 1) {\n result[parts[0]] = value;\n continue;\n }\n\n let current: Record<string, unknown> = result;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n // Step only into an object this walk built. Anything else under the\n // name — a string, a File, or an array from a duplicate key\n // (`rows=x&rows=y`) — is replaced: the dot-path takes precedence.\n // Stepping into an array would let `rows.4294967294` set its length\n // to 2^32 - 1, and every later walk (file and sensitive-field\n // stripping) maps over that length (TIM-1573).\n if (!isPlainObject(current[part])) {\n current[part] = {};\n }\n current = current[part] as Record<string, unknown>;\n }\n\n current[parts[parts.length - 1]] = value;\n }\n\n // The top level is always an object: it is the form, not a list.\n for (const [key, value] of Object.entries(result)) {\n if (isPlainObject(value)) result[key] = indexedListsToArrays(value);\n }\n return result;\n}\n\n/**\n * `node`, with every nested object whose keys are exactly `\"0\"..\"n-1\"`\n * turned into an array, depth first. Only a dense, canonical index set\n * converts: `{ \"0\", \"2\" }` (a gap), `{ \"01\" }` and `{ \"0\", \"name\" }` stay\n * objects. So a forged `rows.99999` stays one key instead of allocating a\n * sparse array, and the array's length is bounded by the field count limit.\n *\n * Object.keys lists integer-like keys first, in ascending order, so a dense\n * set reads as `0, 1, … n-1` and `Object.values` is already in index order.\n */\nfunction indexedListsToArrays(node: Record<string, unknown>): Record<string, unknown> | unknown[] {\n for (const [key, value] of Object.entries(node)) {\n if (isPlainObject(value)) node[key] = indexedListsToArrays(value);\n }\n const keys = Object.keys(node);\n const dense = keys.length > 0 && keys.every((key, i) => key === String(i));\n return dense ? Object.values(node) : node;\n}\n\n/** An object the dot-path walk built — not an array, File or other instance. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype\n );\n}\n\n// `__proto__` is the only key in this parser that escapes the dot-path\n// walk's `typeof !== 'object'` reset: accessing `obj['__proto__']` returns\n// `Object.prototype` (itself an object), so the walk steps into the global\n// prototype and the leaf write mutates it. `constructor` resolves to a\n// function (reset fires) and `prototype` is undefined on fresh objects\n// (reset fires), so neither escapes the walk — they parse as legit nested\n// keys and do not mutate anything global.\nconst DANGEROUS_KEYS = new Set(['__proto__']);\n\n/**\n * The most segments a dot-path field name may have. A deeper name is dropped,\n * like a `__proto__` path, so no walk over the parsed value can exhaust the\n * stack (TIM-1573).\n */\nconst MAX_PATH_DEPTH = 32;\n\n// ─── Coercion Helpers ────────────────────────────────────────────────────\n\n/**\n * Schema-agnostic coercion primitives for common FormData patterns.\n *\n * These are plain transform functions — they compose with any schema library's\n * `transform`/`preprocess` pipeline. In Zod, use `z.preprocess`: it runs on an\n * absent key (an unchecked checkbox), where `z.unknown().transform()` fails\n * with \"expected nonoptional\" before the transform runs.\n *\n * ```ts\n * // Zod\n * z.preprocess(coerce.number, z.number())\n * // Valibot\n * v.pipe(v.unknown(), v.transform(coerce.number), v.number())\n * ```\n */\nexport const coerce = {\n /**\n * Coerce an empty string to undefined for optional text fields.\n * Use with `.optional()` schemas where an empty input means \"not provided\".\n *\n * ```ts\n * // Zod — preprocess, not `z.unknown().transform(…)`: Zod 4 rejects an\n * // absent key before a transform runs (\"expected nonoptional\")\n * z.preprocess(coerce.text, z.string().optional())\n * // Valibot\n * v.pipe(v.unknown(), v.transform(coerce.text), v.optional(v.string()))\n * ```\n */\n text(value: unknown): string | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value === 'string') return value;\n return undefined;\n },\n\n /**\n * Coerce a string to a number.\n * - `\"42\"` → `42`\n * - `\"3.14\"` → `3.14`\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Non-numeric strings → `undefined` (schema validation will catch this)\n */\n number(value: unknown): number | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value === 'number') return value;\n if (typeof value !== 'string') return undefined;\n const num = Number(value);\n if (Number.isNaN(num)) return undefined;\n return num;\n },\n\n /**\n * Coerce a checkbox value to a boolean.\n * HTML checkboxes submit \"on\" when checked and are absent when unchecked.\n * - `\"on\"` / any truthy string → `true`\n * - `undefined` / `null` / `\"\"` → `false`\n */\n checkbox(value: unknown): boolean {\n if (value === undefined || value === null || value === '') return false;\n if (typeof value === 'boolean') return value;\n // Any non-empty string (typically \"on\") is true\n return typeof value === 'string' && value.length > 0;\n },\n\n /**\n * Parse a JSON string into an object.\n * - Valid JSON string → parsed object\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Invalid JSON → `undefined` (schema validation will catch this)\n */\n json(value: unknown): unknown {\n if (value === undefined || value === null || value === '') return undefined;\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch {\n return undefined;\n }\n },\n\n /**\n * Coerce a date string to a Date object.\n * Handles `<input type=\"date\">` (`\"2024-01-15\"`), `<input type=\"datetime-local\">`\n * (`\"2024-01-15T10:30\"`), and full ISO 8601 strings.\n * - Valid date string → `Date`\n * - `\"\"` / `undefined` / `null` → `undefined`\n * - Invalid date strings → `undefined` (schema validation will catch this)\n * - Impossible dates that `new Date()` silently normalizes (e.g. Feb 31) → `undefined`\n */\n date(value: unknown): Date | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n if (value instanceof Date) return value;\n if (typeof value !== 'string') return undefined;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return undefined;\n\n // Overflow detection: extract Y/M/D from the input string and verify\n // they match the parsed Date components. new Date('2024-02-31') silently\n // normalizes to March 2nd — we reject such inputs.\n const ymdMatch = value.match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n if (ymdMatch) {\n const inputYear = Number(ymdMatch[1]);\n const inputMonth = Number(ymdMatch[2]);\n const inputDay = Number(ymdMatch[3]);\n\n // Use UTC methods for date-only and Z-suffixed strings to avoid\n // timezone offset shifting the day. For datetime-local (no Z suffix),\n // the Date constructor parses in local time, so use local methods.\n const isUTC = value.length === 10 || value.endsWith('Z');\n const parsedYear = isUTC ? date.getUTCFullYear() : date.getFullYear();\n const parsedMonth = isUTC ? date.getUTCMonth() + 1 : date.getMonth() + 1;\n const parsedDay = isUTC ? date.getUTCDate() : date.getDate();\n\n if (inputYear !== parsedYear || inputMonth !== parsedMonth || inputDay !== parsedDay) {\n return undefined;\n }\n }\n\n return date;\n },\n\n /**\n * Create a File coercion function with optional size and mime type validation.\n * Returns the File if valid, `undefined` otherwise.\n *\n * ```ts\n * // Basic — just checks it's a real File\n * z.preprocess(coerce.file(), z.instanceof(File))\n *\n * // With constraints\n * z.preprocess(\n * coerce.file({ maxSize: 5 * 1024 * 1024, accept: ['image/png', 'image/jpeg'] }),\n * z.instanceof(File)\n * )\n * ```\n */\n file(options?: { maxSize?: number; accept?: string[] }): (value: unknown) => File | undefined {\n return (value: unknown): File | undefined => {\n if (value === undefined || value === null || value === '') return undefined;\n if (!(value instanceof File)) return undefined;\n\n // Empty file input (no selection): browsers submit File with name=\"\" and size=0\n if (value.size === 0 && value.name === '') return undefined;\n\n if (options?.maxSize !== undefined && value.size > options.maxSize) {\n return undefined;\n }\n\n if (options?.accept !== undefined && !options.accept.includes(value.type)) {\n return undefined;\n }\n\n return value;\n };\n },\n};\n","/**\n * Server action primitives: revalidatePath, revalidateTag, and the action handler.\n *\n * - revalidatePath(path) re-renders the route at that path and returns the RSC\n * flight payload for inline reconciliation. Server actions only.\n * - revalidateTag(tag) invalidates timber.cache entries by tag. Callable from\n * any server code: deferred to the end of the action inside one, immediate\n * anywhere else.\n *\n * The action handler processes incoming action requests, validates CSRF,\n * enforces body limits, executes the action, and returns the response\n * (with piggybacked RSC payload if revalidatePath was called).\n *\n * See design/08-forms-and-actions.md\n */\n\nimport { cache } from '../cache/cache-api.ts';\nimport { canonicalize } from './canonicalize.ts';\nimport { isDenySignal, isRedirectSignal } from './signal-identity.ts';\nimport { withSpan } from './tracing.ts';\nimport { revalidationAls, type RevalidationState } from './als-registry.ts';\nimport type { ReactCacheScope } from './react-cache-scope.ts';\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/** Result of rendering a revalidation — payload root before RSC serialization. */\nexport interface RevalidationResult {\n /**\n * The payload root — tree plus the params it rendered with, exactly as an\n * ordinary route payload. The client splits it in `applyActionResult`.\n */\n payload: unknown;\n /**\n * The `React.cache` scope the target route's middleware ran in. The\n * revalidated tree renders in it, so middleware, access.ts and components\n * share one scope — fresh, never the action body's (TIM-1529).\n */\n reactCacheScope: ReactCacheScope;\n}\n\n/** Renderer function that builds a React element tree for a given path. */\nexport type RevalidateRenderer = (path: string) => Promise<RevalidationResult>;\n\n/**\n * Thrown by a `RevalidateRenderer` that decided not to render: the path\n * was rejected, its params failed coercion, or its middleware answered\n * with a `Response` that cannot become an element tree. `executeAction`\n * drops the revalidation — the action result is still delivered, with no\n * `_tree` piggybacked — and, because the drop was a decision, does not\n * return it as an error to report. Identified by `instanceof`, so the\n * message text never reaches clients.\n */\nexport class RevalidationDropped extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RevalidationDropped';\n }\n}\n\n// Re-export the type from the registry for public API consumers.\nexport type { RevalidationState } from './als-registry.ts';\n\n/** Options for creating the action handler. */\nexport interface ActionHandlerConfig {\n /** Renderer for producing RSC payloads during revalidation. */\n renderer?: RevalidateRenderer;\n /**\n * Pathname of the page the action was submitted from. When set,\n * revalidatePath renders are skipped for non-matching paths — the\n * server cache is already invalidated, so the next navigation fetches\n * fresh data. See TIM-1454.\n */\n requestPathname?: string;\n /** Search string of the action request URL (e.g. `?page=1`). */\n requestSearch?: string;\n}\n\n/** Result of handling a server action request. */\nexport interface ActionHandlerResult {\n /** The action's return value (serialized). */\n actionResult: unknown;\n /** Revalidation result if revalidatePath was called (element tree, not yet serialized). */\n revalidation?: RevalidationResult;\n /**\n * True when `revalidatePath()` was called only for pages other than the\n * current one (TIM-1454). The client then evicts its caches and skips the\n * refresh — nothing on screen was named. Whenever the current page was\n * named this is false, even if other pages were named too: its re-render,\n * or the refresh that replaces a dropped one, re-renders every layout it\n * shares with them. Which paths were named is not reported: the client\n * evicts every cached payload after any revalidation (TIM-1476), so a path\n * list would carry nothing it acts on (TIM-1461).\n */\n onlyOtherPagesNamed: boolean;\n /** Redirect location if a RedirectSignal was thrown during revalidation. */\n redirectTo?: string;\n /** Redirect status code. */\n redirectStatus?: number;\n /**\n * Errors the action survived after its body returned: a `revalidateTag`\n * invalidation that rejected, or a `revalidatePath` render that threw.\n * Each is already logged. They are returned rather than reported here\n * because reporting needs the request, which the caller owns; the caller\n * passes each to `onRequestError` once.\n */\n revalidationErrors: unknown[];\n}\n\n// ─── Revalidation State ──────────────────────────────────────────────────\n\n// Per-request revalidation state stored in AsyncLocalStorage (from als-registry.ts).\n// This ensures concurrent requests never share or overwrite each other's state\n// (the previous module-level global was vulnerable to cross-request pollution).\n\n/**\n * Run `fn` inside a revalidation ALS scope.\n * @internal — for tests that call revalidatePath/Tag directly without executeAction().\n */\nexport function _runWithRevalidationState<T>(state: RevalidationState, fn: () => T): T {\n return revalidationAls.run(state, fn);\n}\n\n// ─── Public API ──────────────────────────────────────────────────────────\n\n/**\n * Re-render the route at `path` and include the RSC flight payload in the\n * action response. The client reconciles inline — no separate fetch needed.\n *\n * Only callable inside a server action: it attaches a render to the action\n * response, and nothing else has one. To refresh cached data from a\n * `route.ts` handler, a webhook, or a job, use `revalidateTag()` or\n * `cache.invalidate()`.\n *\n * @param path - The path to re-render (e.g. '/dashboard', '/todos').\n */\nexport function revalidatePath(path: string): void {\n const state = revalidationAls.getStore();\n if (!state || state.closed) {\n throw new Error(\n 'revalidatePath() can only be called inside a server action: it re-renders ' +\n 'the route into the action response, and there is no action response here. ' +\n 'To refresh cached data from a route handler or other server code, call ' +\n 'revalidateTag() or cache.invalidate() for the tags the route reads.'\n );\n }\n if (!state.paths.includes(path)) {\n state.paths.push(path);\n }\n}\n\n/**\n * Invalidate all timber.cache entries tagged with `tag`, and tombstone the\n * pre-rendered component seeds that carry it.\n * Does not return a payload — the next request for an invalidated entry re-executes.\n *\n * Callable from any server code. Inside a server action the invalidation is\n * deferred until the action body returns, so a `revalidatePath()` render in\n * the same action reads fresh data; the returned promise resolves at once.\n * Anywhere else — a `route.ts` webhook, a job, or background work an action\n * started that runs after the action body returned — it is\n * `cache.invalidate({ tag })`, and the promise settles when the invalidation\n * has. Await it before responding.\n *\n * @param tag - The cache tag to invalidate (e.g. 'products', 'user:123').\n */\nexport function revalidateTag(tag: string): Promise<void> {\n const state = revalidationAls.getStore();\n if (!state || state.closed) return cache.invalidate({ tag });\n if (!state.tags.includes(tag)) {\n state.tags.push(tag);\n }\n return Promise.resolve();\n}\n\n// ─── Action Handler ──────────────────────────────────────────────────────\n\n/**\n * Execute a server action and process revalidation.\n *\n * 1. Sets up revalidation state\n * 2. Calls the action function\n * 3. Processes revalidateTag calls (invalidates cache entries)\n * 4. Processes revalidatePath calls (re-renders and captures RSC payload)\n * 5. Returns the action result + optional RSC payload\n *\n * @param actionFn - The server action function to execute.\n * @param args - Arguments to pass to the action.\n * @param config - Handler configuration (cache handler, renderer).\n */\nexport async function executeAction(\n actionFn: (...args: unknown[]) => Promise<unknown>,\n args: unknown[],\n config: ActionHandlerConfig = {},\n spanMeta?: { actionFile?: string; actionName?: string }\n): Promise<ActionHandlerResult> {\n const state: RevalidationState = { paths: [], tags: [] };\n const revalidationErrors: unknown[] = [];\n let actionResult: unknown;\n let redirectTo: string | undefined;\n let redirectStatus: number | undefined;\n\n // Run the action inside ALS scope so revalidatePath/Tag resolve to this\n // request's state object — concurrent requests each get their own scope.\n await revalidationAls.run(state, async () => {\n try {\n actionResult = await withSpan(\n 'timber.action',\n {\n ...(spanMeta?.actionFile ? { 'timber.action_file': spanMeta.actionFile } : {}),\n ...(spanMeta?.actionName ? { 'timber.action_name': spanMeta.actionName } : {}),\n },\n () => actionFn(...args)\n );\n } catch (error) {\n if (isRedirectSignal(error)) {\n redirectTo = error.location;\n redirectStatus = error.status;\n } else {\n throw error;\n }\n } finally {\n // From here `paths` and `tags` are snapshotted below (or abandoned on\n // a throw); later calls from un-awaited work must not queue into them.\n state.closed = true;\n }\n });\n\n // Process tag invalidation through cache.invalidate so it records the\n // invalidation epoch — an in-flight cached fn must not re-store data this\n // mutation just invalidated (TIM-1028). cache.invalidate resolves the\n // module-level handler singleton: setCacheHandler() is called at boot from\n // rsc-entry when timber.config.ts provides a cacheHandler; otherwise falls\n // back to in-memory LRU (TIM-599).\n if (state.tags.length > 0) {\n // cache.invalidate handles both data-cache clearing AND component-seed\n // tombstone writes (design/45 §ISR mechanics) — no separate tombstone\n // step needed here.\n // Best-effort: the action body already executed and its mutation committed.\n // Use allSettled so all tags settle before proceeding — Promise.all would\n // short-circuit on the first rejection, leaving other invalidations (including\n // component-seed tombstone writes) in-flight during revalidatePath rendering.\n const results = await Promise.allSettled(state.tags.map((tag) => cache.invalidate({ tag })));\n for (const r of results) {\n if (r.status === 'rejected') {\n console.error('[timber] revalidateTag invalidation failed:', r.reason);\n revalidationErrors.push(r.reason);\n }\n }\n }\n\n // Process path revalidation — build element tree (not yet serialized).\n //\n // TIM-1454: Only a path that matches the current page is rendered. When\n // every call names another page, `onlyOtherPagesNamed` tells the client to\n // evict its caches instead. The matching path is looked for across every\n // call, not just the first: an action that calls revalidatePath('/other')\n // and then revalidatePath('/current') must still re-render the current\n // page (TIM-1461).\n let revalidation: RevalidationResult | undefined;\n const { requestPathname, requestSearch } = config;\n const isCurrentPage = (path: string) =>\n !requestPathname || revalidationPathMatches(path, requestPathname, requestSearch);\n const path = state.paths.find(isCurrentPage);\n const onlyOtherPagesNamed = state.paths.length > 0 && path === undefined;\n\n if (path !== undefined && config.renderer) {\n try {\n revalidation = await config.renderer(path);\n } catch (renderError) {\n if (isRedirectSignal(renderError)) {\n redirectTo = renderError.location;\n redirectStatus = renderError.status;\n } else if (isDenySignal(renderError)) {\n console.error(\n `[timber] revalidatePath dropped — target middleware denied (status ${renderError.status})`\n );\n } else if (renderError instanceof RevalidationDropped) {\n console.error(`[timber] ${renderError.message}`);\n } else {\n console.error('[timber] revalidatePath render failed:', renderError);\n revalidationErrors.push(renderError);\n }\n }\n }\n\n return {\n actionResult,\n revalidation,\n onlyOtherPagesNamed,\n revalidationErrors,\n ...(redirectTo ? { redirectTo, redirectStatus } : {}),\n };\n}\n\n/**\n * Check if a `revalidatePath(path)` argument matches the request pathname.\n * Canonicalizes the revalidated path the same way the renderer does so\n * equivalent paths compare equal. Returns true when the paths match or\n * when comparison is inconclusive (validation failure → render to be safe).\n */\nfunction revalidationPathMatches(\n revalidatedPath: string,\n requestPathname: string,\n requestSearch?: string\n): boolean {\n const hashIdx = revalidatedPath.indexOf('#');\n const noFragment = hashIdx >= 0 ? revalidatedPath.slice(0, hashIdx) : revalidatedPath;\n const queryIdx = noFragment.indexOf('?');\n const pathnameOnly = queryIdx >= 0 ? noFragment.slice(0, queryIdx) : noFragment;\n const search = queryIdx >= 0 ? noFragment.slice(queryIdx) : '';\n const canonical = canonicalize(pathnameOnly, true);\n if (!canonical.ok) return true;\n if (canonical.pathname !== requestPathname) return false;\n // revalidatePath('/dashboard') (no search) matches any query variant.\n // revalidatePath('/dashboard?tab=x') only matches that exact search.\n if (search && search !== (requestSearch ?? '')) return false;\n return true;\n}\n"],"mappings":"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"}
|
|
@@ -60,7 +60,33 @@ function canonicalize(rawPathname, stripTrailingSlash = true) {
|
|
|
60
60
|
pathname
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The path a safe request for `rawPathname` should be redirected to — or
|
|
65
|
+
* `rawPathname` itself when it is already canonical.
|
|
66
|
+
*
|
|
67
|
+
* Applies the structural steps of `canonicalize()` — collapse `//` (step 3)
|
|
68
|
+
* and strip the trailing slash (step 5) — to the **encoded** path, without
|
|
69
|
+
* decoding. That is what the browser will send back, so the redirect is
|
|
70
|
+
* idempotent: `/%61dmin` differs from its canonical form `/admin` only by
|
|
71
|
+
* percent-encoding, and redirecting it to `/admin` would compare against the
|
|
72
|
+
* decoded form forever. Decoding cannot introduce a `/` (`%2f` is rejected),
|
|
73
|
+
* so the structure is the same before and after decoding.
|
|
74
|
+
*
|
|
75
|
+
* Dot segments are not handled here: the request URL is parsed by the WHATWG
|
|
76
|
+
* URL parser, which has already resolved `.`, `..` and their encoded forms.
|
|
77
|
+
*
|
|
78
|
+
* Only meaningful for a path `canonicalize()` accepted. The collapse also
|
|
79
|
+
* means the result can never start with `//`, so it is never a
|
|
80
|
+
* protocol-relative redirect target.
|
|
81
|
+
*
|
|
82
|
+
* See design/07-routing.md §"Non-Canonical URLs Redirect"
|
|
83
|
+
*/
|
|
84
|
+
function canonicalRequestPath(rawPathname, stripTrailingSlash = true) {
|
|
85
|
+
let pathname = rawPathname.replace(/\/\/+/g, "/");
|
|
86
|
+
if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
|
|
87
|
+
return pathname;
|
|
88
|
+
}
|
|
63
89
|
//#endregion
|
|
64
|
-
export { NULL_BYTE_RE as n,
|
|
90
|
+
export { canonicalize as i, NULL_BYTE_RE as n, canonicalRequestPath as r, ENCODED_SEPARATOR_RE as t };
|
|
65
91
|
|
|
66
|
-
//# sourceMappingURL=canonicalize-
|
|
92
|
+
//# sourceMappingURL=canonicalize-BAWkWiKK.js.map
|