@timber-js/app 0.2.0-alpha.166 → 0.2.0-alpha.168
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-CDPfMp_I.js → actions-O_LsyCE4.js} +3 -3
- package/dist/_chunks/{actions-CDPfMp_I.js.map → actions-O_LsyCE4.js.map} +1 -1
- package/dist/_chunks/{cache-api-DygSeKCB.js → cache-api-B-lhk9p4.js} +107 -58
- package/dist/_chunks/cache-api-B-lhk9p4.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-EXGYPhI2.js → cli-schema-sync-B73L6pMq.js} +5 -3
- package/dist/_chunks/cli-schema-sync-B73L6pMq.js.map +1 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js +1188 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js.map +1 -0
- package/dist/_chunks/json-lossy-check-ClNvBM_3.js +63 -0
- package/dist/_chunks/json-lossy-check-ClNvBM_3.js.map +1 -0
- package/dist/_chunks/{logger-t3uxAmbX.js → logger-AWfuX-KJ.js} +2 -19
- package/dist/_chunks/logger-AWfuX-KJ.js.map +1 -0
- package/dist/_chunks/{walkers-DBVzXuWc.js → walkers-CoOC8Hga.js} +2 -2
- package/dist/_chunks/{walkers-DBVzXuWc.js.map → walkers-CoOC8Hga.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +10 -3
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +12 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +2 -461
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/cache-api.d.ts +33 -11
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cache/json-lossy-check.d.ts +11 -0
- package/dist/cache/json-lossy-check.d.ts.map +1 -0
- package/dist/cache/redis-handler.d.ts +42 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -1
- package/dist/cache/stores/memory.d.ts +1 -1
- package/dist/cache/stores/memory.d.ts.map +1 -1
- package/dist/cache/stores/redis.d.ts +1 -1
- package/dist/cache/stores/redis.d.ts.map +1 -1
- package/dist/cache/stores/vercel.d.ts +1 -1
- package/dist/cache/stores/vercel.d.ts.map +1 -1
- package/dist/cache/tag-aware-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.d.ts.map +1 -1
- package/dist/cdn/fastly-purge.d.ts.map +1 -1
- package/dist/cdn/workers-cache-purge.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/child-segment-context.d.ts +2 -2
- package/dist/client/child-segment-context.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -0
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/internal.js +141 -116
- package/dist/client/internal.js.map +1 -1
- package/dist/client/router.d.ts +6 -0
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +3 -3
- package/dist/client/segment-update-context.d.ts.map +1 -1
- package/dist/client/slot-outlet.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +2 -1
- package/dist/codec.js.map +1 -1
- package/dist/config-types.d.ts +16 -37
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/dev-tools/instrumentation.d.ts.map +1 -1
- package/dist/fonts/pipeline.d.ts +19 -0
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +378 -109
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +8 -8
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +27 -3
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/action-client.d.ts +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +7 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/body-limits.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +3 -1
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/index.js +20 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +28 -41
- package/dist/server/internal.js.map +1 -1
- package/dist/server/middleware-runner.d.ts +0 -17
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +6 -3
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +18 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/prebuilt/capture-state.d.ts.map +1 -1
- package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -1
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +9 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +14 -14
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +1 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +8 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +44 -74
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
- package/dist/server/safe-load.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/shared/redirect-type.d.ts +2 -2
- package/dist/shared/redirect-type.d.ts.map +1 -1
- package/dist/shims/font-google.d.ts.map +1 -1
- package/dist/shims/image.d.ts +120 -120
- package/dist/shims/image.d.ts.map +1 -1
- package/docs/api/32-api-cache.mdx +116 -4
- package/docs/api/34-api-config.mdx +0 -26
- package/docs/learn/09-caching.mdx +126 -10
- package/docs/learn/12-client-navigation.mdx +9 -1
- package/docs/learn/13-configuration.mdx +6 -8
- package/docs/learn/14-deploying.mdx +18 -18
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/docs/more/50-ai-agent-instructions.mdx +5 -5
- package/package.json +6 -5
- package/src/adapters/cloudflare-kv-cache.ts +27 -6
- package/src/adapters/cloudflare.ts +63 -25
- package/src/adapters/types.ts +2 -0
- package/src/cache/cache-api.ts +84 -84
- package/src/cache/index.ts +1 -1
- package/src/cache/json-lossy-check.ts +75 -0
- package/src/cache/redis-handler.ts +65 -14
- package/src/cache/timber-cache.ts +10 -2
- package/src/client/browser-entry/index.ts +2 -0
- package/src/client/browser-entry/post-hydration.ts +16 -9
- package/src/client/browser-entry/router-init.ts +2 -0
- package/src/client/history.ts +11 -0
- package/src/client/router.ts +224 -208
- package/src/codec.ts +3 -1
- package/src/config-types.ts +16 -37
- package/src/config-validation.ts +7 -5
- package/src/fonts/pipeline.ts +32 -0
- package/src/fonts/transform.ts +30 -24
- package/src/plugins/adapter-build.ts +132 -13
- package/src/plugins/cache.ts +45 -30
- package/src/plugins/entries.ts +40 -43
- package/src/plugins/fonts.ts +30 -0
- package/src/plugins/server-bundle.ts +7 -15
- package/src/routing/scanner.ts +7 -0
- package/src/server/action-client.ts +1 -1
- package/src/server/als-registry.ts +7 -0
- package/src/server/deny-boundary.ts +6 -3
- package/src/server/middleware-runner.ts +0 -44
- package/src/server/param-coercion.ts +1 -0
- package/src/server/pipeline-phases.ts +11 -13
- package/src/server/pipeline.ts +51 -3
- package/src/server/route-element-builder.ts +59 -9
- package/src/server/rsc-entry/action-middleware-runner.ts +14 -14
- package/src/server/rsc-entry/index.ts +30 -40
- package/src/server/rsc-entry/render-route.ts +6 -2
- package/src/server/rsc-entry/rsc-payload.ts +21 -4
- package/src/server/rsc-entry/rsc-stream.ts +8 -0
- package/src/server/rsc-entry/wrap-action-dispatch.ts +109 -372
- package/dist/_chunks/cache-api-DygSeKCB.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-EXGYPhI2.js.map +0 -1
- package/dist/_chunks/logger-t3uxAmbX.js.map +0 -1
- package/dist/_chunks/tree-match-D2l830j2.js +0 -102
- package/dist/_chunks/tree-match-D2l830j2.js.map +0 -1
- package/dist/adapters/cloudflare.js.map +0 -1
package/src/plugins/fonts.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* Design doc: 24-fonts.md
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { parseAst } from 'vite';
|
|
21
22
|
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
|
|
22
23
|
import type { PluginContext } from '../plugin-context.js';
|
|
23
24
|
import {
|
|
@@ -224,6 +225,35 @@ export function timberFonts(ctx: PluginContext): Plugin {
|
|
|
224
225
|
*/
|
|
225
226
|
buildStart() {},
|
|
226
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Prune stale font entries when a module that previously imported fonts
|
|
230
|
+
* no longer contains any font import. The transform hook's `filter.code`
|
|
231
|
+
* skips such modules, so without this hook the old entries would persist
|
|
232
|
+
* until dev restart. See TIM-1087.
|
|
233
|
+
*/
|
|
234
|
+
async hotUpdate({ type, file, read }) {
|
|
235
|
+
if (!pipeline.hasImporter(file)) return;
|
|
236
|
+
if (type === 'delete') {
|
|
237
|
+
pipeline.pruneImporter(file);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const code = await read();
|
|
241
|
+
const hasFontImport =
|
|
242
|
+
code.includes('@timber/fonts/') ||
|
|
243
|
+
code.includes('@timber-js/app/fonts/') ||
|
|
244
|
+
code.includes('next/font/');
|
|
245
|
+
if (hasFontImport) return;
|
|
246
|
+
// Only prune if the new source parses successfully. A syntax error
|
|
247
|
+
// means the HMR update will fail and the old module stays active —
|
|
248
|
+
// pruning would leave it without font CSS until the next valid save.
|
|
249
|
+
try {
|
|
250
|
+
parseAst(code, { lang: 'tsx' });
|
|
251
|
+
} catch {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
pipeline.pruneImporter(file);
|
|
255
|
+
},
|
|
256
|
+
|
|
227
257
|
/**
|
|
228
258
|
* Scan source files for font function calls and extract static config.
|
|
229
259
|
*
|
|
@@ -19,23 +19,15 @@ import type { Plugin } from 'vite';
|
|
|
19
19
|
// Listing explicitly silences Vite's "Automatically externalized" warnings.
|
|
20
20
|
const NODE_BUILTINS: string[] = builtinModules.flatMap((m) => [m, `node:${m}`]);
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// `virtual:timber-cache-handler` dynamic import) into adapter code, dragging
|
|
26
|
-
// build tooling into the prod server bundle. The most visible symptom is
|
|
27
|
-
// rolldown trying to parse `fsevents.node` as JS and failing with
|
|
28
|
-
// `[UNLOADABLE_DEPENDENCY] … stream did not contain valid UTF-8`.
|
|
22
|
+
// Packages that must never be bundled into the runtime server environments.
|
|
23
|
+
// With `noExternal: true`, rolldown follows all transitive imports. These
|
|
24
|
+
// are build-time-only tools that have no runtime use in a server bundle.
|
|
29
25
|
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// - `fsevents`, `chokidar` — native/optional deps of
|
|
35
|
-
// rollup/vite. They are macOS-only file watchers with no runtime use
|
|
36
|
-
// in a server bundle.
|
|
26
|
+
// The primary leak path (TIM-988) — `virtual:timber-cache-handler` importing
|
|
27
|
+
// `timber.config.ts` and pulling in adapter/MDX deps — was eliminated by
|
|
28
|
+
// TIM-1211: the cache handler virtual module now imports `timber.cache.ts`
|
|
29
|
+
// instead. These externals are retained as defense-in-depth.
|
|
37
30
|
//
|
|
38
|
-
// Mirrored on both rsc and ssr environments. See TIM-988.
|
|
39
31
|
// Cloudflare runtime modules (cloudflare:workers, cloudflare:sockets, etc.)
|
|
40
32
|
// available at runtime on Workers but must not be bundled.
|
|
41
33
|
const CLOUDFLARE_BUILTINS = [
|
package/src/routing/scanner.ts
CHANGED
|
@@ -232,6 +232,13 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
|
|
|
232
232
|
// See design/16-metadata.md §"Metadata Routes"
|
|
233
233
|
const metaInfo = classifyMetadataRoute(entry);
|
|
234
234
|
if (metaInfo) {
|
|
235
|
+
if (!metaInfo.nestable && node.segmentName !== '') {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Build error: '${name}' is a root-only metadata convention and must be in the app root directory.\n` +
|
|
238
|
+
` File: ${fullPath}\n` +
|
|
239
|
+
` Move this file to the app root (not a route group or nested segment).`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
235
242
|
if (!node.metadataRoutes) {
|
|
236
243
|
node.metadataRoutes = {};
|
|
237
244
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 1. Direct call: action(input) → Promise<ActionResult>
|
|
9
9
|
* 2. React useActionState: (prevState, formData) => Promise<ActionResult>
|
|
10
10
|
*
|
|
11
|
-
* See design/08-forms-and-actions.md §"Middleware
|
|
11
|
+
* See design/08-forms-and-actions.md §"Middleware and Server Actions"
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
// ─── ActionError ─────────────────────────────────────────────────────────
|
|
@@ -127,6 +127,13 @@ export interface RequestContextStore {
|
|
|
127
127
|
* See TIM-666.
|
|
128
128
|
*/
|
|
129
129
|
denyStatus?: number;
|
|
130
|
+
/**
|
|
131
|
+
* Callback fired when setDenyStatus() is called. Used by shellSettled
|
|
132
|
+
* to short-circuit when any component catches a DenySignal in-tree,
|
|
133
|
+
* regardless of which component caught it (AccessGate, TracedLayout,
|
|
134
|
+
* or PageDenyBoundary). See TIM-1208.
|
|
135
|
+
*/
|
|
136
|
+
onDenyStatus?: () => void;
|
|
130
137
|
/**
|
|
131
138
|
* Dev-only: getter for the current request's RSC debug components.
|
|
132
139
|
* Set by renderRoute() so onPipelineError can include component tree
|
|
@@ -262,6 +262,7 @@ export async function PageDenyBoundary({
|
|
|
262
262
|
Page,
|
|
263
263
|
route,
|
|
264
264
|
denyPages,
|
|
265
|
+
onSettled,
|
|
265
266
|
}: {
|
|
266
267
|
/** The page server component function. */
|
|
267
268
|
Page: (...args: unknown[]) => unknown;
|
|
@@ -269,10 +270,10 @@ export async function PageDenyBoundary({
|
|
|
269
270
|
route: string;
|
|
270
271
|
/** Deny page fallback chain from the segment chain. */
|
|
271
272
|
denyPages: DenyPageEntry[];
|
|
273
|
+
/** Called when the page component finishes (success or deny). See TIM-1045. */
|
|
274
|
+
onSettled?: () => void;
|
|
272
275
|
}): Promise<React.ReactElement> {
|
|
273
276
|
try {
|
|
274
|
-
// Call the page as an async function — same as React Flight does.
|
|
275
|
-
// Wrap in OTEL span for tracing (replaces the TracedPage wrapper).
|
|
276
277
|
const result = await withSpan('timber.page', { 'timber.route': route }, () => Page({}));
|
|
277
278
|
return result as React.ReactElement;
|
|
278
279
|
} catch (error: unknown) {
|
|
@@ -283,8 +284,9 @@ export async function PageDenyBoundary({
|
|
|
283
284
|
return denyElement;
|
|
284
285
|
}
|
|
285
286
|
}
|
|
286
|
-
// Non-deny errors (RedirectSignal, runtime errors) propagate normally.
|
|
287
287
|
throw error;
|
|
288
|
+
} finally {
|
|
289
|
+
onSettled?.();
|
|
288
290
|
}
|
|
289
291
|
}
|
|
290
292
|
|
|
@@ -299,6 +301,7 @@ export function setDenyStatus(status: number): void {
|
|
|
299
301
|
const store = requestContextAls.getStore();
|
|
300
302
|
if (store) {
|
|
301
303
|
store.denyStatus = status;
|
|
304
|
+
store.onDenyStatus?.();
|
|
302
305
|
}
|
|
303
306
|
}
|
|
304
307
|
|
|
@@ -57,47 +57,3 @@ export async function runMiddlewareChain(
|
|
|
57
57
|
}
|
|
58
58
|
return undefined;
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
// ─── Per-Request Middleware Bypass ─────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Per-request marker for synthetic re-render requests that should NOT
|
|
65
|
-
* re-execute `middleware.ts`. The action-dispatch wrapper runs middleware
|
|
66
|
-
* once on the inbound action POST; when validation fails on the no-JS
|
|
67
|
-
* path, it builds a synthetic GET that flows through the normal pipeline
|
|
68
|
-
* to render the page with `getFormFlash()` data. Without this marker, the
|
|
69
|
-
* pipeline would run middleware a second time on that synthetic GET.
|
|
70
|
-
*
|
|
71
|
-
* The set is keyed by the synthetic Request object itself, so the entry
|
|
72
|
-
* lives exactly as long as the request and is garbage-collected with it.
|
|
73
|
-
* Cannot be set or detected by user code — there is no header, no URL
|
|
74
|
-
* parameter, nothing on the wire that an attacker could spoof.
|
|
75
|
-
*
|
|
76
|
-
* See TIM-871.
|
|
77
|
-
*
|
|
78
|
-
* @internal — framework use only.
|
|
79
|
-
*/
|
|
80
|
-
const middlewareBypassRequests = new WeakSet<Request>();
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Mark a request so the pipeline skips its middleware phase.
|
|
84
|
-
*
|
|
85
|
-
* Used by `wrap-action-dispatch.ts` for the no-JS form-rerender path.
|
|
86
|
-
*
|
|
87
|
-
* @internal
|
|
88
|
-
*/
|
|
89
|
-
export function markRequestBypassMiddleware(req: Request): void {
|
|
90
|
-
middlewareBypassRequests.add(req);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Check whether a request was marked to bypass middleware.
|
|
95
|
-
*
|
|
96
|
-
* Called by `handleRequest` in pipeline-phases.ts before invoking the
|
|
97
|
-
* middleware phase. Returns false for any request not explicitly marked.
|
|
98
|
-
*
|
|
99
|
-
* @internal
|
|
100
|
-
*/
|
|
101
|
-
export function shouldBypassMiddleware(req: Request): boolean {
|
|
102
|
-
return middlewareBypassRequests.has(req);
|
|
103
|
-
}
|
|
@@ -132,6 +132,7 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
|
|
|
132
132
|
if (!codec) continue; // no codec in schema → keep raw string
|
|
133
133
|
|
|
134
134
|
const key = segment.paramName;
|
|
135
|
+
if (!(key in mergeTarget)) continue;
|
|
135
136
|
try {
|
|
136
137
|
mergeTarget[key] = sanitizeParamValue(codec.parse(mergeTarget[key] as string | string[]));
|
|
137
138
|
} catch (err) {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { canonicalize } from './canonicalize.js';
|
|
16
16
|
import { runProxy } from './proxy.js';
|
|
17
|
-
import { runMiddlewareChain
|
|
17
|
+
import { runMiddlewareChain } from './middleware-runner.js';
|
|
18
18
|
import { withTiming } from './server-timing.js';
|
|
19
19
|
import {
|
|
20
20
|
applyRequestHeaderOverlay,
|
|
@@ -66,8 +66,11 @@ function validateInterceptionHeader(raw: string, stripTrailingSlash: boolean): s
|
|
|
66
66
|
// ─── Phase: Proxy ──────────────────────────────────────────────────────────
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Run the proxy.ts phase. Calls user proxy code
|
|
70
|
-
*
|
|
69
|
+
* Run the proxy.ts phase. Calls user proxy code with a `next()` continuation.
|
|
70
|
+
*
|
|
71
|
+
* When `next` is provided, it replaces the default `handleRequest` as the
|
|
72
|
+
* continuation — this lets the pipeline inject action dispatch between proxy
|
|
73
|
+
* and route matching (TIM-1213). The proxy resolver was picked at pipeline
|
|
71
74
|
* construction time so the hot path sees no per-request branching on the
|
|
72
75
|
* `ProxyConfig` discriminant.
|
|
73
76
|
*/
|
|
@@ -76,13 +79,14 @@ export async function runProxyPhase(
|
|
|
76
79
|
getProxy: ProxyResolver,
|
|
77
80
|
req: Request,
|
|
78
81
|
method: string,
|
|
79
|
-
path: string
|
|
82
|
+
path: string,
|
|
83
|
+
next?: () => Promise<Response>
|
|
80
84
|
): Promise<PhaseOutcome> {
|
|
81
85
|
const detailed = config.serverTiming === 'detailed';
|
|
82
86
|
try {
|
|
83
87
|
const proxyExport = await getProxy();
|
|
84
|
-
const
|
|
85
|
-
|
|
88
|
+
const continuation = next ?? (() => handleRequest(config, req, method, path, true));
|
|
89
|
+
const proxyFn = () => runProxy(proxyExport, req, continuation);
|
|
86
90
|
const response = await withSpan('timber.proxy', {}, () =>
|
|
87
91
|
detailed ? withTiming('proxy', 'proxy.ts', proxyFn) : proxyFn()
|
|
88
92
|
);
|
|
@@ -514,14 +518,8 @@ export async function handleRequest(
|
|
|
514
518
|
.join('/') || '/';
|
|
515
519
|
setMatchedSegmentPath(segmentPath.startsWith('/') ? segmentPath : `/${segmentPath}`);
|
|
516
520
|
|
|
517
|
-
// Bypass middleware for synthetic re-render requests built by the
|
|
518
|
-
// action-dispatch wrapper after a no-JS form validation failure.
|
|
519
|
-
// The wrapper has already executed middleware once on the inbound POST;
|
|
520
|
-
// running it again on the rerender GET would double-execute auth, rate
|
|
521
|
-
// limiting, and request-header injection. See TIM-871.
|
|
522
|
-
const skipMiddleware = shouldBypassMiddleware(req);
|
|
523
521
|
const outcome =
|
|
524
|
-
|
|
522
|
+
match.middlewareChain.length > 0
|
|
525
523
|
? await runMiddlewarePhase(config, req, match, responseHeaders, requestHeaderOverlay, {
|
|
526
524
|
canonicalPathname,
|
|
527
525
|
interception,
|
package/src/server/pipeline.ts
CHANGED
|
@@ -238,6 +238,29 @@ export interface PipelineConfig {
|
|
|
238
238
|
*/
|
|
239
239
|
match?: RouteMatch
|
|
240
240
|
) => Response | Promise<Response>;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Server action dispatch — called after proxy.ts runs for POST requests
|
|
244
|
+
* that look like server actions. Returns a Response if the request was
|
|
245
|
+
* handled as an action, or null to fall through to normal routing.
|
|
246
|
+
*
|
|
247
|
+
* `canonicalPath` is the already-decoded canonical pathname — the same
|
|
248
|
+
* value that `handleRequest` and `matchRoute` use. Do NOT re-derive it
|
|
249
|
+
* from `req.url` (which re-encodes).
|
|
250
|
+
*
|
|
251
|
+
* The `reenter` parameter is the full pipeline function (including proxy,
|
|
252
|
+
* ALS, tracing) for the no-JS validation rerender path — the synthetic
|
|
253
|
+
* GET flows through the complete pipeline.
|
|
254
|
+
*
|
|
255
|
+
* Moved into the pipeline (TIM-1213) so proxy.ts runs on action POSTs,
|
|
256
|
+
* matching the design doc contract: "proxy.ts runs on every request,
|
|
257
|
+
* no exclusions."
|
|
258
|
+
*/
|
|
259
|
+
dispatchAction?: (
|
|
260
|
+
req: Request,
|
|
261
|
+
canonicalPath: string,
|
|
262
|
+
reenter: (req: Request) => Promise<Response>
|
|
263
|
+
) => Promise<Response | null>;
|
|
241
264
|
}
|
|
242
265
|
|
|
243
266
|
// ─── Pipeline ──────────────────────────────────────────────────────────────
|
|
@@ -265,7 +288,11 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
|
|
|
265
288
|
// Logged with each request for diagnosing resource contention.
|
|
266
289
|
let activeRequests = 0;
|
|
267
290
|
|
|
268
|
-
|
|
291
|
+
// Self-reference for re-entry — the no-JS validation rerender path
|
|
292
|
+
// passes a synthetic GET back through the full pipeline (including
|
|
293
|
+
// proxy, ALS, tracing). The inner closure captures `pipelineFn` by
|
|
294
|
+
// name; by the time it's invoked, the const binding is initialized.
|
|
295
|
+
const pipelineFn = async (req: Request): Promise<Response> => {
|
|
269
296
|
const url = new URL(req.url);
|
|
270
297
|
const method = req.method;
|
|
271
298
|
const path = url.pathname;
|
|
@@ -325,6 +352,24 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
|
|
|
325
352
|
canonicalReq = new Request(canonicalUrl.toString(), req);
|
|
326
353
|
}
|
|
327
354
|
|
|
355
|
+
// Build the inner handler — runs after proxy, before route
|
|
356
|
+
// matching. Checks for server action POSTs and dispatches them
|
|
357
|
+
// without entering the route match → middleware → render chain.
|
|
358
|
+
// Falls through to handleRequest for normal route handling.
|
|
359
|
+
// TIM-1213: action dispatch is now INSIDE the pipeline so
|
|
360
|
+
// proxy.ts runs on every request, including action POSTs.
|
|
361
|
+
const innerHandler = async (): Promise<Response> => {
|
|
362
|
+
if (config.dispatchAction) {
|
|
363
|
+
const actionResult = await config.dispatchAction(
|
|
364
|
+
canonicalReq,
|
|
365
|
+
canonicalPath,
|
|
366
|
+
pipelineFn
|
|
367
|
+
);
|
|
368
|
+
if (actionResult) return actionResult;
|
|
369
|
+
}
|
|
370
|
+
return handleRequest(config, canonicalReq, method, canonicalPath, true);
|
|
371
|
+
};
|
|
372
|
+
|
|
328
373
|
let result: Response;
|
|
329
374
|
if (proxyResolver) {
|
|
330
375
|
const outcome = await runProxyPhase(
|
|
@@ -332,7 +377,8 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
|
|
|
332
377
|
proxyResolver,
|
|
333
378
|
canonicalReq,
|
|
334
379
|
method,
|
|
335
|
-
canonicalPath
|
|
380
|
+
canonicalPath,
|
|
381
|
+
innerHandler
|
|
336
382
|
);
|
|
337
383
|
result = await outcomeToResponse(config, outcome, {
|
|
338
384
|
req: canonicalReq,
|
|
@@ -340,7 +386,7 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
|
|
|
340
386
|
path: canonicalPath,
|
|
341
387
|
});
|
|
342
388
|
} else {
|
|
343
|
-
result = await
|
|
389
|
+
result = await innerHandler();
|
|
344
390
|
}
|
|
345
391
|
|
|
346
392
|
// Set response status on the root span before it ends —
|
|
@@ -427,4 +473,6 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
|
|
|
427
473
|
});
|
|
428
474
|
});
|
|
429
475
|
};
|
|
476
|
+
|
|
477
|
+
return pipelineFn;
|
|
430
478
|
}
|
|
@@ -34,6 +34,7 @@ function getDevNonce(): string {
|
|
|
34
34
|
}
|
|
35
35
|
import { DenySignal, RedirectSignal } from './primitives.js';
|
|
36
36
|
import { AccessGate } from './access-gate.js';
|
|
37
|
+
import { requestContextAls } from './als-registry.js';
|
|
37
38
|
import {
|
|
38
39
|
PageDenyBoundary,
|
|
39
40
|
buildDenyPageChain,
|
|
@@ -177,6 +178,15 @@ export interface RouteElementResult {
|
|
|
177
178
|
* See design/19-client-navigation.md §"X-Timber-State-Tree Header"
|
|
178
179
|
*/
|
|
179
180
|
skippedSegments: string[];
|
|
181
|
+
/**
|
|
182
|
+
* Resolves when every deny-capable shell component (PageDenyBoundary and
|
|
183
|
+
* each TracedLayout with a deny page chain) has finished executing, OR
|
|
184
|
+
* immediately when any of them catches a DenySignal in-tree (the HTTP
|
|
185
|
+
* status is known at that point; inner components may never run).
|
|
186
|
+
* Undefined when the route has no deny-capable server components.
|
|
187
|
+
* See TIM-1045 (pages), TIM-1208 (layouts).
|
|
188
|
+
*/
|
|
189
|
+
shellSettled?: Promise<void>;
|
|
180
190
|
}
|
|
181
191
|
|
|
182
192
|
// ─── Module Processing Helpers ─────────────────────────────────────────────
|
|
@@ -398,6 +408,7 @@ export async function buildRouteElement(
|
|
|
398
408
|
// Skip OG auto-linking when the user already declared images in metadata.
|
|
399
409
|
// See design/16-metadata.md §"Auto-Linking"
|
|
400
410
|
const hasUserOgImage = Boolean(resolvedMetadata.openGraph?.images);
|
|
411
|
+
const hasUserTwitterImage = Boolean(resolvedMetadata.twitter?.images);
|
|
401
412
|
const requestUrl = new URL(req.url);
|
|
402
413
|
const requestPathname = requestUrl.pathname;
|
|
403
414
|
// In dev mode, use the request origin so OG URLs resolve to localhost.
|
|
@@ -417,7 +428,7 @@ export async function buildRouteElement(
|
|
|
417
428
|
if (!convention) continue;
|
|
418
429
|
// Non-nestable routes only auto-link from root
|
|
419
430
|
if (!convention.nestable && segment.urlPath !== '/') continue;
|
|
420
|
-
// Skip auto-linking if user
|
|
431
|
+
// Skip auto-linking entirely if user declared openGraph.images (covers both og + twitter)
|
|
421
432
|
if (convention.type === 'opengraph-image' && hasUserOgImage) continue;
|
|
422
433
|
// Build the href using the actual request path (not the pattern with [param]).
|
|
423
434
|
// For nestable routes, the metadata route sits under the same resolved path
|
|
@@ -440,6 +451,14 @@ export async function buildRouteElement(
|
|
|
440
451
|
href = new URL(href, ogBase).toString();
|
|
441
452
|
}
|
|
442
453
|
for (const autoLink of getMetadataRouteAutoLink(convention.type, href)) {
|
|
454
|
+
// Skip auto-linked twitter:image when user declared twitter.images
|
|
455
|
+
if (
|
|
456
|
+
hasUserTwitterImage &&
|
|
457
|
+
autoLink.tag === 'meta' &&
|
|
458
|
+
'name' in autoLink &&
|
|
459
|
+
autoLink.name === 'twitter:image'
|
|
460
|
+
)
|
|
461
|
+
continue;
|
|
443
462
|
if (autoLink.tag === 'link') {
|
|
444
463
|
const attrs: Record<string, string> = { rel: autoLink.rel, href: autoLink.href };
|
|
445
464
|
if (autoLink.type) attrs.type = autoLink.type;
|
|
@@ -462,12 +481,24 @@ export async function buildRouteElement(
|
|
|
462
481
|
// Build element tree: page wrapped in layouts (innermost to outermost)
|
|
463
482
|
const h = createElement as (...args: unknown[]) => React.ReactElement;
|
|
464
483
|
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
484
|
+
// Shell settlement tracking (TIM-1045 pages, TIM-1208 layouts).
|
|
485
|
+
// ChildSegmentProvider is 'use client', so React Flight eagerly
|
|
486
|
+
// serializes both the layout and child content as independent subtrees —
|
|
487
|
+
// the page can settle before an outer layout. We need all deny-capable
|
|
488
|
+
// components to settle before committing the HTTP status.
|
|
489
|
+
//
|
|
490
|
+
// Each tracked component gets its own deferred. shellSettled resolves
|
|
491
|
+
// when ALL settle (Promise.all) OR when any in-tree deny fires
|
|
492
|
+
// (short-circuit via onDenyStatus — covers AccessGate, TracedLayout,
|
|
493
|
+
// and PageDenyBoundary without threading callbacks into each).
|
|
494
|
+
const shellPromises: Promise<void>[] = [];
|
|
495
|
+
let resolveShortCircuit: (() => void) | undefined;
|
|
496
|
+
const trackShellComponent = (): (() => void) => {
|
|
497
|
+
let resolve!: () => void;
|
|
498
|
+
shellPromises.push(new Promise<void>((r) => (resolve = r)));
|
|
499
|
+
return resolve;
|
|
500
|
+
};
|
|
501
|
+
|
|
471
502
|
// Build the page element.
|
|
472
503
|
// Server component pages are wrapped in PageDenyBoundary which calls
|
|
473
504
|
// them as async functions and catches DenySignal — rendering the deny
|
|
@@ -482,11 +513,12 @@ export async function buildRouteElement(
|
|
|
482
513
|
if (isClientReference(PageComponent)) {
|
|
483
514
|
element = h(PageComponent, {});
|
|
484
515
|
} else if (leafDenyPages && leafDenyPages.length > 0) {
|
|
485
|
-
|
|
516
|
+
const onSettled = trackShellComponent();
|
|
486
517
|
element = h(PageDenyBoundary, {
|
|
487
518
|
Page: PageComponent,
|
|
488
519
|
route: match.segments[leafIndex]?.urlPath ?? '/',
|
|
489
520
|
denyPages: leafDenyPages,
|
|
521
|
+
onSettled,
|
|
490
522
|
});
|
|
491
523
|
} else {
|
|
492
524
|
// Server component page WITHOUT deny page chain — trace only
|
|
@@ -694,6 +726,8 @@ export async function buildRouteElement(
|
|
|
694
726
|
// fallback path. See TIM-668, design/04-authorization.md.
|
|
695
727
|
const layoutComponentRef = layoutComponent;
|
|
696
728
|
const layoutDenyPages = denyPageChains.get(i);
|
|
729
|
+
const hasLayoutDenyChain = !!layoutDenyPages && layoutDenyPages.length > 0;
|
|
730
|
+
const onLayoutSettled = hasLayoutDenyChain ? trackShellComponent() : undefined;
|
|
697
731
|
const TracedLayout = async (props: Record<string, unknown>) => {
|
|
698
732
|
try {
|
|
699
733
|
return await withSpan('timber.layout', { 'timber.segment': segmentId }, () =>
|
|
@@ -707,8 +741,11 @@ export async function buildRouteElement(
|
|
|
707
741
|
return denyElement;
|
|
708
742
|
}
|
|
709
743
|
}
|
|
710
|
-
// Non-deny errors (RedirectSignal, runtime errors) propagate normally
|
|
744
|
+
// Non-deny errors (RedirectSignal, runtime errors) propagate normally
|
|
745
|
+
// to onError — signalDetected / renderError handling covers those.
|
|
711
746
|
throw error;
|
|
747
|
+
} finally {
|
|
748
|
+
onLayoutSettled?.();
|
|
712
749
|
}
|
|
713
750
|
};
|
|
714
751
|
layoutElement = h(TracedLayout, {
|
|
@@ -776,11 +813,24 @@ export async function buildRouteElement(
|
|
|
776
813
|
element = replaceOutermostSegmentOutlet(element, outermostSegmentProvider);
|
|
777
814
|
}
|
|
778
815
|
|
|
816
|
+
// Compose shellSettled: resolves when ALL deny-capable components settle
|
|
817
|
+
// (Promise.all) OR immediately when any component calls setDenyStatus()
|
|
818
|
+
// (short-circuit via onDenyStatus — covers AccessGate, TracedLayout, and
|
|
819
|
+
// PageDenyBoundary). See TIM-1045, TIM-1208.
|
|
820
|
+
let shellSettled: Promise<void> | undefined;
|
|
821
|
+
if (shellPromises.length > 0) {
|
|
822
|
+
const shortCircuit = new Promise<void>((r) => (resolveShortCircuit = r));
|
|
823
|
+
const store = requestContextAls.getStore();
|
|
824
|
+
if (store) store.onDenyStatus = () => resolveShortCircuit?.();
|
|
825
|
+
shellSettled = Promise.race([Promise.all(shellPromises).then(() => {}), shortCircuit]);
|
|
826
|
+
}
|
|
827
|
+
|
|
779
828
|
return {
|
|
780
829
|
element,
|
|
781
830
|
layoutComponents,
|
|
782
831
|
segments,
|
|
783
832
|
deferSuspenseFor,
|
|
784
833
|
skippedSegments,
|
|
834
|
+
shellSettled,
|
|
785
835
|
};
|
|
786
836
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Middleware
|
|
3
|
-
* chain
|
|
4
|
-
*
|
|
2
|
+
* Middleware runner for revalidation — runs a route's `middleware.ts`
|
|
3
|
+
* chain inside its own `runWithRequestContext` scope and returns a
|
|
4
|
+
* `MiddlewareOutcome` value the caller can translate.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* so it can be unit-tested in isolation. It does not import virtual
|
|
10
|
-
* modules and does not call into the action handler — it owns exactly
|
|
11
|
-
* the "run middleware once" step and nothing else.
|
|
6
|
+
* Used by the revalidation renderer (TIM-996) to run the TARGET route's
|
|
7
|
+
* middleware before building the revalidated element tree, ensuring that
|
|
8
|
+
* `revalidatePath('/admin')` from a public action honors admin's auth.
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* NOT used for action dispatch itself — per-segment middleware does not
|
|
11
|
+
* run on action POSTs (TIM-1134). Actions are dispatched by action ID,
|
|
12
|
+
* not route segment, so `proxy.ts` and `createActionClient({ middleware })`
|
|
13
|
+
* are the correct auth mechanisms.
|
|
16
14
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* The function is intentionally a free function (not a closure over deps)
|
|
16
|
+
* so it can be unit-tested in isolation. Failure modes are encoded as
|
|
17
|
+
* outcome variants rather than thrown so the caller never has to wrap
|
|
18
|
+
* this in try/catch.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { getCookiesForSsr, getSetCookieHeaders } from '../cookie-context.js';
|
|
@@ -43,7 +43,7 @@ import { loadCdnPurgeHandler } from 'virtual:timber-cache-handler';
|
|
|
43
43
|
// @ts-expect-error — virtual module provided by timber-routing plugin
|
|
44
44
|
import globalSchemaCodecs from 'virtual:timber-schema';
|
|
45
45
|
|
|
46
|
-
import { wrapPipelineWithActionDispatch } from './wrap-action-dispatch.js';
|
|
46
|
+
import { wrapPipelineWithActionDispatch, buildActionDispatcher } from './wrap-action-dispatch.js';
|
|
47
47
|
import type { BodyLimitsConfig } from '../body-limits.js';
|
|
48
48
|
import type { BuildManifest } from '../build-manifest.js';
|
|
49
49
|
import { resolveLogMode } from '../../dev-tools/logger.js';
|
|
@@ -199,6 +199,8 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
199
199
|
| { disabled: boolean; enableHMRInDev: boolean }
|
|
200
200
|
| undefined;
|
|
201
201
|
const clientJsDisabled = clientJsConfig?.disabled ?? false;
|
|
202
|
+
const clientSegmentCache =
|
|
203
|
+
((runtimeConfig as Record<string, unknown>).clientSegmentCache as boolean) ?? false;
|
|
202
204
|
const clientBootstrap = buildClientScripts({
|
|
203
205
|
...runtimeConfig,
|
|
204
206
|
clientJavascript: clientJsConfig ?? { disabled: false, enableHMRInDev: false },
|
|
@@ -295,6 +297,7 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
295
297
|
{
|
|
296
298
|
clientBootstrap,
|
|
297
299
|
clientJsDisabled,
|
|
300
|
+
clientSegmentCache,
|
|
298
301
|
rootSegment: manifest.root,
|
|
299
302
|
buildManifest: typedBuildManifest,
|
|
300
303
|
globalError: manifest.globalError,
|
|
@@ -386,19 +389,36 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
386
389
|
// and no user-authored sitemap exists at the app root.
|
|
387
390
|
// See design/16-metadata.md §"Auto-generated Sitemap"
|
|
388
391
|
autoSitemapHandler: createAutoSitemapHandler(manifest.root, runtimeConfig),
|
|
392
|
+
|
|
393
|
+
// Server action dispatch — runs after proxy.ts, before route matching
|
|
394
|
+
// (TIM-1213). Moved into the pipeline so proxy.ts runs on action POSTs,
|
|
395
|
+
// matching the design doc: "proxy.ts runs on every request, no exclusions."
|
|
396
|
+
// Per-segment `middleware.ts` does NOT run on action POSTs (TIM-1134).
|
|
397
|
+
dispatchAction: buildActionDispatcher({
|
|
398
|
+
csrfConfig: {
|
|
399
|
+
csrf: runtimeConfig.csrf,
|
|
400
|
+
allowedOrigins: (runtimeConfig as Record<string, unknown>).allowedOrigins as
|
|
401
|
+
| string[]
|
|
402
|
+
| undefined,
|
|
403
|
+
},
|
|
404
|
+
bodyLimits: (runtimeConfig as Record<string, unknown>).limits as BodyLimitsConfig['limits'],
|
|
405
|
+
sensitiveFields: formsConfig?.stripSensitiveFields,
|
|
406
|
+
matchRoute,
|
|
407
|
+
buildRevalidateRenderer: createRevalidateRendererFactory({
|
|
408
|
+
matchRoute,
|
|
409
|
+
coerceSegmentParams,
|
|
410
|
+
buildManifest: buildManifest as BuildManifest,
|
|
411
|
+
}),
|
|
412
|
+
}),
|
|
389
413
|
};
|
|
390
414
|
|
|
391
415
|
const pipeline = createPipeline(pipelineConfig);
|
|
392
416
|
|
|
393
|
-
// Wrap the pipeline to enforce CSRF at the request boundary
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
// design/08-forms-and-actions.md §"Middleware for Server Actions".
|
|
399
|
-
//
|
|
400
|
-
// CSRF validation lives in the wrapper (not inside the action handler) so
|
|
401
|
-
// it covers route.ts API handlers as well as server actions. See LOCAL-773.
|
|
417
|
+
// Wrap the pipeline to enforce CSRF at the request boundary.
|
|
418
|
+
// CSRF validation lives in the wrapper (not inside the pipeline) so it
|
|
419
|
+
// runs before proxy.ts and covers every unsafe-method request. Server
|
|
420
|
+
// action dispatch has moved into the pipeline (TIM-1213) so proxy.ts
|
|
421
|
+
// runs on action POSTs. See LOCAL-773.
|
|
402
422
|
const csrfConfig = {
|
|
403
423
|
csrf: runtimeConfig.csrf,
|
|
404
424
|
allowedOrigins: (runtimeConfig as Record<string, unknown>).allowedOrigins as
|
|
@@ -406,38 +426,8 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
|
|
|
406
426
|
| undefined,
|
|
407
427
|
};
|
|
408
428
|
|
|
409
|
-
const actionsConfig = (runtimeConfig as Record<string, unknown>).actions as
|
|
410
|
-
| { runMiddleware?: boolean }
|
|
411
|
-
| undefined;
|
|
412
|
-
|
|
413
429
|
const handler = wrapPipelineWithActionDispatch(pipeline, {
|
|
414
430
|
csrfConfig,
|
|
415
|
-
bodyLimits: (runtimeConfig as Record<string, unknown>).limits as BodyLimitsConfig['limits'],
|
|
416
|
-
sensitiveFields: formsConfig?.stripSensitiveFields,
|
|
417
|
-
matchRoute,
|
|
418
|
-
coerceSegmentParams,
|
|
419
|
-
renderDenyFallback: pipelineConfig.renderDenyFallback,
|
|
420
|
-
runMiddleware: actionsConfig?.runMiddleware,
|
|
421
|
-
// Thread the pipeline's `stripTrailingSlash` setting into the wrapper
|
|
422
|
-
// so its canonicalization step (applied before `matchRoute` for
|
|
423
|
-
// action POSTs) matches the page pipeline exactly. Defaults to `true`
|
|
424
|
-
// both here and inside the wrapper.
|
|
425
|
-
stripTrailingSlash: pipelineConfig.stripTrailingSlash,
|
|
426
|
-
// Revalidation renderer — runs the target route's `middleware.ts`
|
|
427
|
-
// chain BEFORE building the element tree so a server action that
|
|
428
|
-
// revalidates a protected path honors that path's auth, tenant
|
|
429
|
-
// isolation, and request-header injection. Closes the silent-bypass
|
|
430
|
-
// bug where `revalidatePath('/admin')` from a public action would
|
|
431
|
-
// serialize the admin RSC tree without ever consulting admin's
|
|
432
|
-
// middleware. Also validates the user-supplied `path` as a local
|
|
433
|
-
// root-relative URL to reject `revalidatePath('//evil.com/admin')`
|
|
434
|
-
// and similar scheme/authority injections. See TIM-996 and
|
|
435
|
-
// design/08-forms-and-actions.md §"revalidatePath()".
|
|
436
|
-
buildRevalidateRenderer: createRevalidateRendererFactory({
|
|
437
|
-
matchRoute,
|
|
438
|
-
coerceSegmentParams,
|
|
439
|
-
buildManifest: buildManifest as BuildManifest,
|
|
440
|
-
}),
|
|
441
431
|
});
|
|
442
432
|
|
|
443
433
|
return { handler, pipelineConfig };
|