@timber-js/app 0.2.0-alpha.212 → 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-CCdnVtWm.js → actions-CUh3cClk.js} +43 -8
- package/dist/_chunks/{actions-CCdnVtWm.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-DsNScGRM.js → error-boundary-xxxLtXt6.js} +38 -5
- package/dist/_chunks/{error-boundary-DsNScGRM.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/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +1 -0
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
- package/dist/client/browser-entry/form-state.d.ts +22 -0
- package/dist/client/browser-entry/form-state.d.ts.map +1 -0
- package/dist/client/browser-entry/hydrate.d.ts +9 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +2 -0
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +90 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +26 -10
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-transition.d.ts +11 -2
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +70 -8
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +3 -1
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +12 -1
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/use-form-field.d.ts +39 -0
- package/dist/client/use-form-field.d.ts.map +1 -0
- package/dist/config-types.d.ts +2 -1
- package/dist/config-types.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/action-client.d.ts +11 -2
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/canonicalize.d.ts +22 -0
- package/dist/server/canonicalize.d.ts.map +1 -1
- package/dist/server/flight-scripts.d.ts +9 -0
- package/dist/server/flight-scripts.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +13 -4
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/form-state-flight.d.ts +32 -0
- package/dist/server/form-state-flight.d.ts.map +1 -0
- package/dist/server/index.js +37 -23
- package/dist/server/index.js.map +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/dist/server/rsc-entry/action-dispatcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +2 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +6 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +7 -5
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-form-state.d.ts +30 -0
- package/dist/server/ssr-form-state.d.ts.map +1 -0
- package/dist/shared/form-state-flight.d.ts +36 -0
- package/dist/shared/form-state-flight.d.ts.map +1 -0
- package/docs/api/31-api-client.mdx +22 -0
- package/docs/api/34-api-config.mdx +1 -1
- package/docs/learn/08-forms-and-actions.mdx +109 -22
- package/package.json +1 -1
- package/src/adapters/nitro-preview.ts +10 -1
- package/src/client/browser-entry/action-dispatch.ts +34 -10
- package/src/client/browser-entry/action-queue.ts +1 -1
- package/src/client/browser-entry/form-state.ts +48 -0
- package/src/client/browser-entry/hydrate.ts +9 -18
- package/src/client/browser-entry/index.ts +25 -7
- package/src/client/browser-entry/router-init.ts +3 -2
- package/src/client/index.ts +1 -0
- package/src/client/navigation-transition.ts +13 -2
- package/src/client/router-effects.ts +99 -10
- package/src/client/router-pipeline.ts +7 -3
- package/src/client/router-types.ts +12 -1
- package/src/client/router.ts +35 -6
- package/src/client/use-form-field.ts +132 -0
- package/src/config-types.ts +2 -1
- package/src/server/action-client.ts +68 -53
- package/src/server/canonicalize.ts +29 -0
- package/src/server/flight-scripts.ts +13 -0
- package/src/server/form-data.ts +62 -10
- package/src/server/form-state-flight.ts +67 -0
- package/src/server/pipeline-helpers.ts +61 -0
- package/src/server/pipeline.ts +13 -1
- package/src/server/rsc-entry/action-dispatcher.ts +3 -0
- package/src/server/rsc-entry/index.ts +1 -0
- package/src/server/rsc-entry/render-route.ts +16 -0
- package/src/server/rsc-entry/ssr-renderer.ts +12 -14
- package/src/server/ssr-bridge-types.ts +7 -6
- package/src/server/ssr-entry.ts +16 -3
- package/src/server/ssr-form-state.ts +58 -0
- package/src/shared/form-state-flight.ts +74 -0
- package/dist/server/form-state-embed.d.ts +0 -32
- package/dist/server/form-state-embed.d.ts.map +0 -1
- package/src/server/form-state-embed.ts +0 -63
package/dist/routing/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as INTERCEPTION_MARKERS, i as DEFAULT_PAGE_EXTENSIONS, n as classifyUrlSegment, t as classifySegment } from "../_chunks/segment-classify-C539Pa2O.js";
|
|
2
|
-
import { d as collectInterceptionRewrites, n as scanRoutes } from "../_chunks/scanner-
|
|
2
|
+
import { d as collectInterceptionRewrites, n as scanRoutes } from "../_chunks/scanner-CKAT5gRx.js";
|
|
3
3
|
import { t as generateRouteMap } from "../_chunks/codegen-Bps1sLKJ.js";
|
|
4
|
-
import { n as collectDynamicSegmentsFromTree, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "../_chunks/walkers-
|
|
4
|
+
import { n as collectDynamicSegmentsFromTree, r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "../_chunks/walkers-DwCXEyRu.js";
|
|
5
5
|
export { DEFAULT_PAGE_EXTENSIONS, INTERCEPTION_MARKERS, classifySegment, classifyUrlSegment, collectDynamicSegmentsFromTree, collectInterceptionRewrites, collectLeafRoutes, generateRouteMap, scanRoutes, validateSchemaAgainstRoutes };
|
|
@@ -105,7 +105,11 @@ export type ActionResult<TData = unknown> = {
|
|
|
105
105
|
code: string;
|
|
106
106
|
data?: Record<string, unknown>;
|
|
107
107
|
};
|
|
108
|
-
|
|
108
|
+
/**
|
|
109
|
+
* The submitted form, when the input was FormData — for repopulating
|
|
110
|
+
* form fields, which React resets after the action (TIM-1573).
|
|
111
|
+
*/
|
|
112
|
+
submittedValues?: Record<string, unknown>;
|
|
109
113
|
};
|
|
110
114
|
/** Context passed to the action body. */
|
|
111
115
|
export interface ActionContext<TCtx, TInput> {
|
|
@@ -184,7 +188,12 @@ import { type SensitiveFieldsOption } from './sensitive-fields.ts';
|
|
|
184
188
|
* `createActionClient` actions only. A raw `'use server'` function that
|
|
185
189
|
* throws is rejected on the client instead (action-handler.ts).
|
|
186
190
|
*/
|
|
187
|
-
export declare function handleActionError(error: unknown):
|
|
191
|
+
export declare function handleActionError(error: unknown): {
|
|
192
|
+
serverError: {
|
|
193
|
+
code: string;
|
|
194
|
+
data?: Record<string, unknown>;
|
|
195
|
+
};
|
|
196
|
+
};
|
|
188
197
|
/**
|
|
189
198
|
* Create a typed action client with middleware and schema validation.
|
|
190
199
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-client.d.ts","sourceRoot":"","sources":["../../src/server/action-client.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"action-client.d.ts","sourceRoot":"","sources":["../../src/server/action-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -37,4 +37,26 @@ export declare const NULL_BYTE_RE: RegExp;
|
|
|
37
37
|
* @param stripTrailingSlash - Whether to strip trailing slashes. Default: true.
|
|
38
38
|
*/
|
|
39
39
|
export declare function canonicalize(rawPathname: string, stripTrailingSlash?: boolean): CanonicalizeResult;
|
|
40
|
+
/**
|
|
41
|
+
* The path a safe request for `rawPathname` should be redirected to — or
|
|
42
|
+
* `rawPathname` itself when it is already canonical.
|
|
43
|
+
*
|
|
44
|
+
* Applies the structural steps of `canonicalize()` — collapse `//` (step 3)
|
|
45
|
+
* and strip the trailing slash (step 5) — to the **encoded** path, without
|
|
46
|
+
* decoding. That is what the browser will send back, so the redirect is
|
|
47
|
+
* idempotent: `/%61dmin` differs from its canonical form `/admin` only by
|
|
48
|
+
* percent-encoding, and redirecting it to `/admin` would compare against the
|
|
49
|
+
* decoded form forever. Decoding cannot introduce a `/` (`%2f` is rejected),
|
|
50
|
+
* so the structure is the same before and after decoding.
|
|
51
|
+
*
|
|
52
|
+
* Dot segments are not handled here: the request URL is parsed by the WHATWG
|
|
53
|
+
* URL parser, which has already resolved `.`, `..` and their encoded forms.
|
|
54
|
+
*
|
|
55
|
+
* Only meaningful for a path `canonicalize()` accepted. The collapse also
|
|
56
|
+
* means the result can never start with `//`, so it is never a
|
|
57
|
+
* protocol-relative redirect target.
|
|
58
|
+
*
|
|
59
|
+
* See design/07-routing.md §"Non-Canonical URLs Redirect"
|
|
60
|
+
*/
|
|
61
|
+
export declare function canonicalRequestPath(rawPathname: string, stripTrailingSlash?: boolean): string;
|
|
40
62
|
//# sourceMappingURL=canonicalize.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canonicalize.d.ts","sourceRoot":"","sources":["../../src/server/canonicalize.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAE7F;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,QAAa,CAAC;AAE/C,2DAA2D;AAC3D,eAAO,MAAM,YAAY,QAAS,CAAC;AAEnC;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,kBAAkB,UAAO,GAAG,kBAAkB,CAwD/F"}
|
|
1
|
+
{"version":3,"file":"canonicalize.d.ts","sourceRoot":"","sources":["../../src/server/canonicalize.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAE7F;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,QAAa,CAAC;AAE/C,2DAA2D;AAC3D,eAAO,MAAM,YAAY,QAAS,CAAC;AAEnC;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,kBAAkB,UAAO,GAAG,kBAAkB,CAwD/F;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,kBAAkB,UAAO,GAAG,MAAM,CAM3F"}
|
|
@@ -42,4 +42,13 @@ export declare function flightInitScript(nonce?: string): string;
|
|
|
42
42
|
* push if needed, preventing a TypeError. See TIM-559.
|
|
43
43
|
*/
|
|
44
44
|
export declare function flightChunkScript(data: string, nonce?: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Generate the script that embeds a no-JS action's form state for
|
|
47
|
+
* hydration: its Flight bytes (server/form-state-flight.ts), as base64. Only
|
|
48
|
+
* a page answering a no-JS action has one. Base64 carries Flight's binary
|
|
49
|
+
* rows, which a text chunk would corrupt, and has no character that can end
|
|
50
|
+
* the script element or the string. See design/08-forms-and-actions.md
|
|
51
|
+
* §"No-JS Result Round-Trip".
|
|
52
|
+
*/
|
|
53
|
+
export declare function formStateScript(bytes: Uint8Array, nonce?: string): string;
|
|
45
54
|
//# sourceMappingURL=flight-scripts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flight-scripts.d.ts","sourceRoot":"","sources":["../../src/server/flight-scripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"flight-scripts.d.ts","sourceRoot":"","sources":["../../src/server/flight-scripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMxD;AAOD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAGtE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzE"}
|
|
@@ -15,16 +15,24 @@
|
|
|
15
15
|
* Handles:
|
|
16
16
|
* - **Duplicate keys → arrays**: `tags=js&tags=ts` → `{ tags: ["js", "ts"] }`
|
|
17
17
|
* - **Nested dot-paths**: `user.name=Alice` → `{ user: { name: "Alice" } }`
|
|
18
|
-
* - **
|
|
18
|
+
* - **Indexed lists → arrays**: `rows.0.name=A&rows.1.name=B` →
|
|
19
|
+
* `{ rows: [{ name: "A" }, { name: "B" }] }`, when the indexes are exactly
|
|
20
|
+
* `0..n-1`. A list with a gap stays an object keyed by index.
|
|
21
|
+
* - **Empty strings stay `""`**: a blank text input is `""`, so
|
|
22
|
+
* `z.string().min(1, 'Required')` reports its own message. Use
|
|
23
|
+
* `coerce.text` to make a blank optional field `undefined`
|
|
19
24
|
* - **Empty Files → undefined**: File inputs with no selection become `undefined`
|
|
20
25
|
* - **Strips `$ACTION_*` fields**: React's internal hidden fields are excluded
|
|
26
|
+
* - **Drops `__proto__` paths and names deeper than 32 segments**
|
|
21
27
|
*/
|
|
22
28
|
export declare function parseFormData(formData: FormData): Record<string, unknown>;
|
|
23
29
|
/**
|
|
24
30
|
* Schema-agnostic coercion primitives for common FormData patterns.
|
|
25
31
|
*
|
|
26
32
|
* These are plain transform functions — they compose with any schema library's
|
|
27
|
-
* `transform`/`preprocess` pipeline
|
|
33
|
+
* `transform`/`preprocess` pipeline. In Zod, use `z.preprocess`: it runs on an
|
|
34
|
+
* absent key (an unchecked checkbox), where `z.unknown().transform()` fails
|
|
35
|
+
* with "expected nonoptional" before the transform runs.
|
|
28
36
|
*
|
|
29
37
|
* ```ts
|
|
30
38
|
* // Zod
|
|
@@ -39,8 +47,9 @@ export declare const coerce: {
|
|
|
39
47
|
* Use with `.optional()` schemas where an empty input means "not provided".
|
|
40
48
|
*
|
|
41
49
|
* ```ts
|
|
42
|
-
* // Zod
|
|
43
|
-
*
|
|
50
|
+
* // Zod — preprocess, not `z.unknown().transform(…)`: Zod 4 rejects an
|
|
51
|
+
* // absent key before a transform runs ("expected nonoptional")
|
|
52
|
+
* z.preprocess(coerce.text, z.string().optional())
|
|
44
53
|
* // Valibot
|
|
45
54
|
* v.pipe(v.unknown(), v.transform(coerce.text), v.optional(v.string()))
|
|
46
55
|
* ```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-data.d.ts","sourceRoot":"","sources":["../../src/server/form-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH
|
|
1
|
+
{"version":3,"file":"form-data.d.ts","sourceRoot":"","sources":["../../src/server/form-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA2BzE;AAgID;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,MAAM;IACjB;;;;;;;;;;;OAWG;IACH,IAAI,QAAQ,OAAO,GAAG,MAAM,GAAG,SAAS;IAMxC;;;;;;OAMG;IACH,MAAM,QAAQ,OAAO,GAAG,MAAM,GAAG,SAAS;IAS1C;;;;;OAKG;IACH,QAAQ,QAAQ,OAAO,GAAG,OAAO;IAOjC;;;;;OAKG;IACH,IAAI,QAAQ,OAAO,GAAG,OAAO;IAU7B;;;;;;;;OAQG;IACH,IAAI,QAAQ,OAAO,GAAG,IAAI,GAAG,SAAS;IAgCtC;;;;;;;;;;;;;;OAcG;IACH,IAAI,WAAW;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,SAAS;CAmB9F,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The no-JS action form state a page render hands React (TIM-1570), encoded
|
|
3
|
+
* with Flight (TIM-1572).
|
|
4
|
+
*
|
|
5
|
+
* Fizz renders the `useActionState` hook that submitted with the action's
|
|
6
|
+
* result, and the browser must hydrate with the same value, or the hook
|
|
7
|
+
* resets. Both get it from the Flight bytes encoded here: SSR decodes them
|
|
8
|
+
* for Fizz (`NavContext.formState`) and, once they decode, embeds them for
|
|
9
|
+
* `hydrateRoot` (server/ssr-form-state.ts; decoded by
|
|
10
|
+
* client/browser-entry/form-state.ts). Flight is what carries a
|
|
11
|
+
* `useActionState` result with JS, so a result has the same types with and
|
|
12
|
+
* without JS. See shared/form-state-flight.ts for the decode.
|
|
13
|
+
*
|
|
14
|
+
* A state Flight cannot encode must not turn the page into a 500 after the
|
|
15
|
+
* action's mutation committed, so it is dropped from both renders: the page
|
|
16
|
+
* renders with no result, as for a form without `useActionState`. That is a
|
|
17
|
+
* value Flight cannot serialize (a plain function, a class instance), which
|
|
18
|
+
* fails the action with JS too, or a nested promise that rejects or does not
|
|
19
|
+
* settle in time, which with JS reaches the hook as that rejected or pending
|
|
20
|
+
* promise. The no-JS page cannot carry a pending promise, and does not embed
|
|
21
|
+
* an error row.
|
|
22
|
+
*
|
|
23
|
+
* See design/08-forms-and-actions.md §"No-JS Result Round-Trip".
|
|
24
|
+
*/
|
|
25
|
+
import type { ReactFormState } from 'react-dom/client';
|
|
26
|
+
/**
|
|
27
|
+
* Serialize the form state with Flight, or return `null` when it cannot be,
|
|
28
|
+
* with a warning. The whole payload is read under one `timeoutMs` deadline,
|
|
29
|
+
* and the render stops when `signal` (the request's) aborts.
|
|
30
|
+
*/
|
|
31
|
+
export declare function encodeFormState(formState: ReactFormState, timeoutMs: number, signal?: AbortSignal): Promise<Uint8Array | null>;
|
|
32
|
+
//# sourceMappingURL=form-state-flight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-state-flight.d.ts","sourceRoot":"","sources":["../../src/server/form-state-flight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAKvD;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,cAAc,EACzB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA2B5B"}
|
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { A as getSpanId, C as isRedirectSignal, E as addSpanEvent, F as withSpan, S as isDenySignal, T as isDevMode, j as getTraceId, w as isDebug } from "../_chunks/logger-CbLdcy-W.js";
|
|
2
2
|
import { t as formatSize } from "../_chunks/format-BKclgVbk.js";
|
|
3
|
-
import { _ as getHeaders, a as parseFormData, c as deny, d as waitUntil, i as coerce, l as redirect, n as revalidatePath, p as getCookieJar, r as revalidateTag, u as redirectExternal, y as getSegmentParams } from "../_chunks/actions-
|
|
3
|
+
import { _ as getHeaders, a as parseFormData, c as deny, d as waitUntil, i as coerce, l as redirect, n as revalidatePath, p as getCookieJar, r as revalidateTag, u as redirectExternal, y as getSegmentParams } from "../_chunks/actions-CUh3cClk.js";
|
|
4
4
|
//#region src/shared/redirect-type.ts
|
|
5
5
|
/**
|
|
6
6
|
* Next.js redirect type discriminator.
|
|
@@ -288,18 +288,21 @@ function handleActionError(error) {
|
|
|
288
288
|
function createActionClient(config = {}) {
|
|
289
289
|
function buildAction(schema, fn) {
|
|
290
290
|
async function actionHandler(...args) {
|
|
291
|
+
const last = args.at(-1);
|
|
292
|
+
const form = last instanceof FormData ? parseFormData(last) : void 0;
|
|
293
|
+
const sensitivePredicate = resolveSensitivePredicate(config.stripSensitiveFields, void 0);
|
|
294
|
+
const echo = (value) => {
|
|
295
|
+
const withoutFiles = stripFiles(value);
|
|
296
|
+
if (withoutFiles === void 0) return void 0;
|
|
297
|
+
return stripSensitiveFields(withoutFiles, sensitivePredicate);
|
|
298
|
+
};
|
|
291
299
|
try {
|
|
292
300
|
const ctx = await runActionMiddleware(config.middleware);
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
if (withoutFiles === void 0) return void 0;
|
|
299
|
-
return stripSensitiveFields(withoutFiles, sensitivePredicate);
|
|
300
|
-
};
|
|
301
|
-
if (config.fileSizeLimit !== void 0 && rawInput && typeof rawInput === "object") {
|
|
302
|
-
const fileSizeErrors = validateFileSizes(rawInput, config.fileSizeLimit);
|
|
301
|
+
const rawInput = schema && form ? form : last;
|
|
302
|
+
const submitted = form ?? rawInput;
|
|
303
|
+
const buildSubmittedValues = () => echo(submitted);
|
|
304
|
+
if (config.fileSizeLimit !== void 0 && submitted && typeof submitted === "object") {
|
|
305
|
+
const fileSizeErrors = validateFileSizes(submitted, config.fileSizeLimit);
|
|
303
306
|
if (fileSizeErrors) return {
|
|
304
307
|
validationErrors: fileSizeErrors,
|
|
305
308
|
submittedValues: buildSubmittedValues()
|
|
@@ -348,7 +351,12 @@ function createActionClient(config = {}) {
|
|
|
348
351
|
}) };
|
|
349
352
|
} catch (error) {
|
|
350
353
|
if (isRedirectSignal(error) || isDenySignal(error)) throw error;
|
|
351
|
-
|
|
354
|
+
const result = handleActionError(error);
|
|
355
|
+
const submittedValues = form && echo(form);
|
|
356
|
+
return submittedValues ? {
|
|
357
|
+
...result,
|
|
358
|
+
submittedValues
|
|
359
|
+
} : result;
|
|
352
360
|
}
|
|
353
361
|
}
|
|
354
362
|
return actionHandler;
|
|
@@ -397,6 +405,8 @@ function logValidationFailure(errors) {
|
|
|
397
405
|
/**
|
|
398
406
|
* Validate that all File objects in the input are within the size limit.
|
|
399
407
|
* Returns validation errors keyed by field name, or null if all files are ok.
|
|
408
|
+
* Keys are dot paths (`rows.0.file`), the form a field name and a Standard
|
|
409
|
+
* Schema error use, so `getFieldError` finds a file in a list.
|
|
400
410
|
*/
|
|
401
411
|
function validateFileSizes(input, limit) {
|
|
402
412
|
const limitKb = Math.round(limit / 1024);
|
|
@@ -408,7 +418,7 @@ function validateFileSizes(input, limit) {
|
|
|
408
418
|
if (value instanceof File && value.size > limit) errors[path] = [`File "${value.name}" (${formatSize(value.size)}) exceeds the ${limitLabel} limit`];
|
|
409
419
|
else if (Array.isArray(value)) for (let i = 0; i < value.length; i++) {
|
|
410
420
|
const item = value[i];
|
|
411
|
-
const itemPath = `${path}
|
|
421
|
+
const itemPath = `${path}.${i}`;
|
|
412
422
|
if (item instanceof File && item.size > limit) (errors[itemPath] ??= []).push(`File "${item.name}" (${formatSize(item.size)}) exceeds the ${limitLabel} limit`);
|
|
413
423
|
else if (typeof item === "object" && item !== null && !(item instanceof File)) walk(item, itemPath);
|
|
414
424
|
}
|
|
@@ -419,19 +429,23 @@ function validateFileSizes(input, limit) {
|
|
|
419
429
|
return Object.keys(errors).length > 0 ? errors : null;
|
|
420
430
|
}
|
|
421
431
|
/**
|
|
422
|
-
* Strip File objects from a value, returning a
|
|
423
|
-
*
|
|
432
|
+
* Strip File objects from a value, returning a copy safe for serialization.
|
|
433
|
+
* File objects can't be serialized and shouldn't be echoed back.
|
|
434
|
+
*
|
|
435
|
+
* Mirrors `parseFormData`'s output: objects and arrays at any depth, arrays
|
|
436
|
+
* of arrays included. A File in an array becomes `undefined` rather than
|
|
437
|
+
* being removed, so `rows.2` still names the third element (TIM-1573).
|
|
424
438
|
*/
|
|
425
439
|
function stripFiles(value) {
|
|
426
|
-
if (value === null || value
|
|
427
|
-
|
|
440
|
+
if (value === null || typeof value !== "object" || value instanceof File) return void 0;
|
|
441
|
+
return stripFilesFrom(value);
|
|
442
|
+
}
|
|
443
|
+
function stripFilesFrom(value) {
|
|
444
|
+
if (value instanceof File) return void 0;
|
|
445
|
+
if (Array.isArray(value)) return value.map(stripFilesFrom);
|
|
446
|
+
if (typeof value !== "object" || value === null) return value;
|
|
428
447
|
const result = {};
|
|
429
|
-
for (const [k, v] of Object.entries(value))
|
|
430
|
-
if (v instanceof File) continue;
|
|
431
|
-
if (Array.isArray(v)) result[k] = v.filter((item) => !(item instanceof File)).map((item) => typeof item === "object" && item !== null && !(item instanceof File) ? stripFiles(item) ?? {} : item);
|
|
432
|
-
else if (typeof v === "object" && v !== null && !(v instanceof File)) result[k] = stripFiles(v) ?? {};
|
|
433
|
-
else result[k] = v;
|
|
434
|
-
}
|
|
448
|
+
for (const [k, v] of Object.entries(value)) if (!(v instanceof File)) result[k] = stripFilesFrom(v);
|
|
435
449
|
return result;
|
|
436
450
|
}
|
|
437
451
|
//#endregion
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/shared/redirect-type.ts","../../src/server/sensitive-fields.ts","../../src/server/action-client.ts"],"sourcesContent":["/**\n * Next.js redirect type discriminator.\n *\n * Provided for API compatibility with libraries that import `RedirectType`\n * from `next/navigation`. In timber, `redirect()` always uses `replace`\n * semantics (no history entry for the redirect itself).\n *\n * Lives in shared/ (isomorphic) so both the server primitives and the\n * client-only next/navigation shim export the same definition without the\n * client shim pulling in server code.\n */\nexport const RedirectType = {\n push: 'push',\n replace: 'replace',\n} as const;\n\nexport type RedirectTypeValue = (typeof RedirectType)[keyof typeof RedirectType];\n","/**\n * Sensitive field stripping — removes password/token/CVV-style fields\n * from form values before they are echoed back to the client as\n * `submittedValues` for form repopulation.\n *\n * Applied to both action paths:\n * - With-JS action path: `createActionClient()` in `action-client.ts`\n * - No-JS form POST path: `handleFormAction()` in `action-handler.ts`\n *\n * Why: on a validation failure, timber echoes submitted form values back so\n * the user doesn't have to re-type everything. Without filtering, plaintext\n * passwords / credit-card numbers / TOTP codes would travel through the RSC\n * stream (with-JS) or land in the HTML as `defaultValue` attributes (no-JS)\n * — ending up in browser history, proxy logs, disk caches, and the\n * back-forward cache.\n *\n * Safe by default: the built-in deny-list is applied unconditionally unless\n * the user explicitly opts out via `forms.stripSensitiveFields: false` in\n * `timber.config.ts` or per-action via `createActionClient({ stripSensitiveFields: false })`.\n *\n * See design/08-forms-and-actions.md §\"Validation errors\"\n * See design/13-security.md §\"Sensitive field stripping\"\n * See TIM-816\n */\n\nimport { isDebug } from './debug.ts';\n\n// ─── Public types ────────────────────────────────────────────────────────\n\n/**\n * How to strip sensitive fields from `submittedValues`.\n *\n * - `true` / `undefined` — use the built-in deny-list (default, safe).\n * - `false` — do not strip anything (dev convenience; never do this in prod).\n * - `string[]` — additional field names to strip, merged with the built-in list.\n * - `(name) => boolean` — custom predicate, fully replaces the built-in list.\n * Return `true` to strip, `false` to keep. The `name` argument is the raw\n * (un-normalized) field name as it appeared in the submitted form.\n */\nexport type SensitiveFieldsOption = boolean | readonly string[] | ((name: string) => boolean);\n\n// ─── Built-in deny-list ──────────────────────────────────────────────────\n\n/**\n * Substring patterns matched against the normalized field name.\n * Normalization = lowercase + strip `_` and `-`.\n *\n * Any field whose normalized name *contains* one of these strings is\n * considered sensitive. Entries like `currentPassword`, `passwordConfirmation`,\n * and `user.password` all match via the `password` substring.\n */\nconst BUILTIN_SUBSTRING_PATTERNS: readonly string[] = [\n 'password',\n 'passwd',\n 'pwd',\n 'secret',\n 'apikey',\n 'accesstoken',\n 'refreshtoken',\n 'cvv',\n 'cvc',\n 'cardnumber',\n 'cardcvc',\n 'ssn',\n 'socialsecuritynumber',\n 'otp',\n 'totp',\n 'mfacode',\n 'twofactorcode',\n 'privatekey',\n];\n\n/**\n * Exact matches against the normalized field name. These are field names that\n * are too short or too common to substring-match safely. e.g. `token` alone\n * would match `csrfToken`, which is not sensitive — so `token` is exact-only,\n * while legitimate token fields are covered by `accesstoken` / `refreshtoken`.\n */\nconst BUILTIN_EXACT_PATTERNS: readonly string[] = ['token'];\n\n/**\n * Normalize a field name for deny-list comparison.\n * Lowercases the string and strips `_` and `-` so camelCase, snake_case, and\n * kebab-case variants all compare equal (`api_key` / `apiKey` / `api-key` →\n * `apikey`).\n */\nfunction normalize(name: string): string {\n let out = '';\n for (let i = 0; i < name.length; i++) {\n const ch = name.charCodeAt(i);\n if (ch === 0x5f /* _ */ || ch === 0x2d /* - */) continue;\n // A-Z → a-z\n if (ch >= 0x41 && ch <= 0x5a) {\n out += String.fromCharCode(ch + 32);\n } else {\n out += name[i];\n }\n }\n return out;\n}\n\n/**\n * Check whether a name matches the built-in deny-list (with optional extras).\n * Extras are merged into the substring pattern list after normalization.\n */\nfunction isBuiltinSensitive(name: string, extras?: readonly string[]): boolean {\n const normalized = normalize(name);\n if (BUILTIN_EXACT_PATTERNS.includes(normalized)) return true;\n for (const pattern of BUILTIN_SUBSTRING_PATTERNS) {\n if (normalized.includes(pattern)) return true;\n }\n if (extras && extras.length > 0) {\n for (const extra of extras) {\n const normExtra = normalize(extra);\n if (normExtra.length === 0) continue;\n if (normalized.includes(normExtra)) return true;\n }\n }\n return false;\n}\n\n// ─── Predicate resolution ────────────────────────────────────────────────\n\n/**\n * A resolved predicate: `null` means \"don't strip anything\" (the option was\n * explicitly `false`). Otherwise a function from raw field name → boolean.\n */\nexport type ResolvedSensitivePredicate = ((name: string) => boolean) | null;\n\n/**\n * Resolve a `SensitiveFieldsOption` into a concrete predicate.\n * Precedence: per-action > global > built-in default.\n *\n * - Per-action `undefined` → fall back to global.\n * - Global `undefined` → use built-in list.\n * - Either level set to `false` → disable stripping entirely (returns `null`).\n * - `true` → built-in list.\n * - `string[]` → built-in ∪ extras.\n * - function → custom, replaces the built-in list entirely.\n */\nexport function resolveSensitivePredicate(\n perAction: SensitiveFieldsOption | undefined,\n global: SensitiveFieldsOption | undefined\n): ResolvedSensitivePredicate {\n const chosen = perAction !== undefined ? perAction : global;\n\n if (chosen === false) return null;\n if (chosen === undefined || chosen === true) {\n return (name) => isBuiltinSensitive(name);\n }\n if (typeof chosen === 'function') {\n return chosen;\n }\n // Array of extra names merged with the built-in list.\n const extras = chosen;\n return (name) => isBuiltinSensitive(name, extras);\n}\n\n// ─── Module-level global config ──────────────────────────────────────────\n\nlet globalConfig: SensitiveFieldsOption | undefined;\n\n/**\n * Set the global `forms.stripSensitiveFields` config from `timber.config.ts`.\n * Called once at startup from `rsc-entry`.\n */\nexport function setGlobalSensitiveFieldsConfig(option: SensitiveFieldsOption | undefined): void {\n globalConfig = option;\n}\n\n/** Read the global `forms.stripSensitiveFields` config. */\nexport function getGlobalSensitiveFieldsConfig(): SensitiveFieldsOption | undefined {\n return globalConfig;\n}\n\n// ─── Stripping ───────────────────────────────────────────────────────────\n\n// One warning per field name per process — prevents log spam when a form is\n// submitted many times in dev mode.\nconst warnedFields = new Set<string>();\n\nfunction warnStripped(name: string): void {\n if (!isDebug()) return;\n if (warnedFields.has(name)) return;\n warnedFields.add(name);\n console.warn(\n `[timber] stripped sensitive field \"${name}\" from submittedValues. ` +\n `Override via forms.stripSensitiveFields in timber.config.ts.`\n );\n}\n\n/**\n * Walk an object (recursively) and return a copy with every key matching\n * `predicate` removed. Nested objects like `{ user: { password: '...' } }`\n * are handled — `user.password` is stripped while other `user.*` fields remain.\n *\n * - Arrays are walked element-wise (object entries inside arrays are cleaned).\n * - Non-plain values (strings, numbers, Files, Dates, etc.) are returned as-is.\n * - When a stripped key is encountered, it is omitted from the result entirely\n * — we do NOT set it to an empty string, because that would overwrite a\n * valid `defaultValue` the form author might have set.\n */\nexport function stripSensitiveFields<T>(value: T, predicate: ResolvedSensitivePredicate): T {\n // Null predicate = stripping disabled entirely.\n if (predicate === null) return value;\n if (value === null || value === undefined) return value;\n if (typeof value !== 'object') return value;\n if (value instanceof File || value instanceof Date) return value;\n\n if (Array.isArray(value)) {\n return value.map((item) => stripSensitiveFields(item, predicate)) as unknown as T;\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {\n if (predicate(key)) {\n warnStripped(key);\n continue;\n }\n result[key] = stripSensitiveFields(nested, predicate);\n }\n return result as unknown as T;\n}\n\n// ─── Test helpers ────────────────────────────────────────────────────────\n\n/** Reset the \"warned once\" cache. Exposed for tests. */\nexport function __resetSensitiveFieldsWarnings(): void {\n warnedFields.clear();\n}\n","/**\n * createActionClient — typed middleware and schema validation for server actions.\n *\n * Inspired by next-safe-action. Provides a builder API:\n * createActionClient({ middleware }) → .schema(z.object(...)) → .action(fn)\n *\n * The resulting action function satisfies both:\n * 1. Direct call: action(input) → Promise<ActionResult>\n * 2. React useActionState: (prevState, formData) => Promise<ActionResult>\n *\n * See design/08-forms-and-actions.md §\"Middleware and Server Actions\"\n */\n\n// ─── ActionError ─────────────────────────────────────────────────────────\n\n/**\n * Typed error class for server actions. Carries a string code and optional data.\n * When thrown from middleware or the action body, the action short-circuits and\n * the client receives `result.serverError`.\n *\n * In production, unexpected errors (non-ActionError) return `{ code: 'INTERNAL_ERROR' }`\n * with no message. In dev, `data.message` is included.\n */\nexport class ActionError<TCode extends string = string> extends Error {\n readonly code: TCode;\n readonly data: Record<string, unknown> | undefined;\n\n constructor(code: TCode, data?: Record<string, unknown>) {\n super(`ActionError: ${code}`);\n this.name = 'ActionError';\n this.code = code;\n this.data = data;\n }\n}\n\n// ─── Standard Schema ──────────────────────────────────────────────────────\n\n/**\n * Standard Schema v1 interface (subset).\n * Zod ≥3.24, Valibot ≥1.0, and ArkType all implement this.\n * See https://github.com/standard-schema/standard-schema\n *\n * We use permissive types here to accept all compliant libraries without\n * requiring exact structural matches on issues/path shapes.\n */\ninterface StandardSchemaV1<Output = unknown> {\n readonly '~standard': {\n readonly validate: (\n value: unknown\n ) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;\n };\n}\n\ntype StandardSchemaResult<Output> =\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<StandardSchemaIssue> };\n\ninterface StandardSchemaIssue {\n message: string;\n path?: ReadonlyArray<PropertyKey | { key: PropertyKey }>;\n}\n\n/** Check if a schema implements the Standard Schema protocol. */\nfunction isStandardSchema(schema: unknown): schema is StandardSchemaV1 {\n return (\n typeof schema === 'object' &&\n schema !== null &&\n '~standard' in schema &&\n typeof (schema as StandardSchemaV1)['~standard'].validate === 'function'\n );\n}\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/**\n * Minimal schema interface — compatible with Zod, Valibot, ArkType, etc.\n *\n * Accepts either:\n * - Standard Schema (preferred): any object with `~standard.validate()`\n * - Legacy parse interface: objects with `.parse()` / `.safeParse()`\n *\n * At runtime, Standard Schema is detected via `~standard` property and\n * takes priority over the legacy interface.\n */\nexport type ActionSchema<T = unknown> = StandardSchemaV1<T> | LegacyActionSchema<T>;\n\n/** Legacy schema interface with .parse() / .safeParse(). */\ninterface LegacyActionSchema<T = unknown> {\n 'parse'(data: unknown): T;\n 'safeParse'?(data: unknown): { success: true; data: T } | { success: false; error: SchemaError };\n // Exclude Standard Schema objects from matching this interface\n '~standard'?: never;\n}\n\n/** Schema validation error shape (for legacy .safeParse()/.parse() interface). */\nexport interface SchemaError {\n issues?: Array<{ path?: Array<string | number>; message: string }>;\n flatten?(): { fieldErrors: Record<string, string[]> };\n}\n\n/** Flattened validation errors keyed by field name. */\nexport type ValidationErrors = Record<string, string[]>;\n\n/** Middleware function: returns context to merge into the action body's ctx. */\nexport type ActionMiddleware<TCtx = Record<string, unknown>> = () => Promise<TCtx> | TCtx;\n\n/** The result type returned to the client. */\nexport type ActionResult<TData = unknown> =\n | { data: TData; validationErrors?: never; serverError?: never; submittedValues?: never }\n | {\n data?: never;\n validationErrors: ValidationErrors;\n serverError?: never;\n /** Raw input values on validation failure — for repopulating form fields. */\n submittedValues?: Record<string, unknown>;\n }\n | {\n data?: never;\n validationErrors?: never;\n serverError: { code: string; data?: Record<string, unknown> };\n submittedValues?: never;\n };\n\n/** Context passed to the action body. */\nexport interface ActionContext<TCtx, TInput> {\n ctx: TCtx;\n input: TInput;\n}\n\n// ─── Builder ─────────────────────────────────────────────────────────────\n\ninterface ActionClientConfig<TCtx> {\n middleware?: ActionMiddleware<TCtx> | ActionMiddleware<Record<string, unknown>>[];\n /** Max file size in bytes. Files exceeding this are rejected with validation errors. */\n fileSizeLimit?: number;\n /**\n * Override the sensitive-field deny-list for this action client.\n * See `SensitiveFieldsOption` in `./sensitive-fields.ts`. Per-action config\n * takes precedence over the global `forms.stripSensitiveFields` option in\n * `timber.config.ts`. See design/08-forms-and-actions.md and TIM-816.\n */\n stripSensitiveFields?: SensitiveFieldsOption;\n}\n\n/** Intermediate builder returned by createActionClient(). */\nexport interface ActionBuilder<TCtx> {\n /** Declare the input schema. Validation errors are returned typed. */\n schema<TInput>(schema: ActionSchema<TInput>): ActionBuilderWithSchema<TCtx, TInput>;\n /** Define the action body without input validation. */\n action<TData>(\n fn: (ctx: ActionContext<TCtx, undefined>) => Promise<TData>\n ): ActionFn<TData, undefined>;\n}\n\n/** Builder after .schema() has been called. */\nexport interface ActionBuilderWithSchema<TCtx, TInput> {\n /** Define the action body with validated input. */\n action<TData>(fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>): ActionFn<TData, TInput>;\n}\n\n/**\n * The final action function. Callable three ways:\n * - Direct: action(input) → Promise<ActionResult<TData>>\n * - React useActionState: action(prevState, payload) → Promise<ActionResult<TData>>,\n * where payload is the submitted FormData or the value dispatch was called with\n * - React <form action={fn}>: action(formData) → void (return value ignored by React)\n *\n * The third overload exists purely for type compatibility with React's\n * `<form action>` prop, which expects `(formData: FormData) => void`.\n * At runtime the function still returns Promise<ActionResult>, but React\n * discards it. This lets validated actions be passed directly to forms\n * without casts.\n */\n/**\n * Map schema output keys to `string | undefined` for form-facing APIs.\n * HTML form values are always strings, and fields can be absent.\n * Gives autocomplete for field names without lying about value types.\n */\nexport type InputHint<T> =\n T extends Record<string, unknown> ? { [K in keyof T]: string | undefined } : T;\n\n/**\n * ActionFn — the callable returned by `createActionClient().action()`.\n *\n * Generic order: `<TData, TInput>` — TData first for backward compatibility.\n * Previously ActionFn had a single `<TData>` generic, so existing code like\n * `ActionFn<MyResult>` must still work with TData in the first position.\n * See TIM-797.\n */\nexport type ActionFn<TData = unknown, TInput = unknown> = {\n /** <form action={fn}> compatibility — React discards the return value. */\n (formData: FormData): void;\n /** Direct call: action(input) — optional when TInput is undefined/unknown (no-schema actions). */\n (\n ...args: undefined extends TInput ? [input?: TInput] : [input: TInput]\n ): Promise<ActionResult<TData>>;\n /**\n * React useActionState: action(prevState, payload). The payload is the\n * submitted FormData, or whatever the hook's dispatch was called with.\n */\n (prevState: ActionResult<TData> | null, payload: TInput | FormData): Promise<ActionResult<TData>>;\n};\n\n// ─── Implementation ──────────────────────────────────────────────────────\n\n/**\n * Run middleware array or single function. Returns merged context.\n */\nasync function runActionMiddleware<TCtx>(\n middleware: ActionMiddleware<TCtx> | ActionMiddleware<Record<string, unknown>>[] | undefined\n): Promise<TCtx> {\n if (!middleware) {\n return {} as TCtx;\n }\n\n if (Array.isArray(middleware)) {\n let merged = {} as Record<string, unknown>;\n for (const mw of middleware) {\n const result = await mw();\n merged = { ...merged, ...result };\n }\n return merged as TCtx;\n }\n\n return await middleware();\n}\n\n// Re-export parseFormData for use throughout the framework\nimport { parseFormData } from './form-data.ts';\nimport { formatSize } from '../utils/format.ts';\nimport { isDebug, isDevMode } from './debug.ts';\nimport { isDenySignal, isRedirectSignal } from './signal-identity.ts';\nimport {\n stripSensitiveFields,\n resolveSensitivePredicate,\n getGlobalSensitiveFieldsConfig,\n type SensitiveFieldsOption,\n} from './sensitive-fields.ts';\n\n/**\n * Extract validation errors from a schema error.\n * Supports Zod's flatten() and generic issues array.\n */\nfunction extractValidationErrors(error: SchemaError): ValidationErrors {\n // Zod-style flatten\n if (typeof error.flatten === 'function') {\n return error.flatten().fieldErrors;\n }\n\n // Generic issues array\n if (error.issues) {\n const errors: ValidationErrors = {};\n for (const issue of error.issues) {\n const path = issue.path?.join('.') ?? '_root';\n if (!errors[path]) errors[path] = [];\n errors[path].push(issue.message);\n }\n return errors;\n }\n\n return { _root: ['Validation failed'] };\n}\n\n/**\n * Extract validation errors from Standard Schema issues.\n */\nfunction extractStandardSchemaErrors(issues: ReadonlyArray<StandardSchemaIssue>): ValidationErrors {\n const errors: ValidationErrors = {};\n for (const issue of issues) {\n const path =\n issue.path\n ?.map((p) => {\n // Standard Schema path items can be { key: ... } objects or bare PropertyKey values\n if (typeof p === 'object' && p !== null && 'key' in p) return String(p.key);\n return String(p);\n })\n .join('.') ?? '_root';\n if (!errors[path]) errors[path] = [];\n errors[path].push(issue.message);\n }\n return Object.keys(errors).length > 0 ? errors : { _root: ['Validation failed'] };\n}\n\n/**\n * Wrap unexpected errors into a safe server error result.\n * ActionError → typed result. Other errors → INTERNAL_ERROR (no leak).\n *\n * `createActionClient` actions only. A raw `'use server'` function that\n * throws is rejected on the client instead (action-handler.ts).\n */\nexport function handleActionError(error: unknown): ActionResult<never> {\n if (error instanceof ActionError) {\n return {\n serverError: {\n code: error.code,\n ...(error.data ? { data: error.data } : {}),\n },\n };\n }\n\n // In dev, include the message for debugging.\n // Uses isDevMode() — NOT isDebug() — because this data is sent to the\n // browser. TIMBER_DEBUG must never cause error messages to leak to clients.\n // See design/13-security.md principle 4: \"Errors don't leak.\"\n const devMode = isDevMode();\n return {\n serverError: {\n code: 'INTERNAL_ERROR',\n ...(devMode && error instanceof Error ? { data: { message: error.message } } : {}),\n },\n };\n}\n\n/**\n * Create a typed action client with middleware and schema validation.\n *\n * @example\n * ```ts\n * const action = createActionClient({\n * middleware: async () => {\n * const user = await getUser()\n * if (!user) throw new ActionError('UNAUTHORIZED')\n * return { user }\n * },\n * })\n *\n * export const createTodo = action\n * .schema(z.object({ title: z.string().min(1) }))\n * .action(async ({ input, ctx }) => {\n * await db.todos.create({ ...input, userId: ctx.user.id })\n * })\n * ```\n */\nexport function createActionClient<TCtx = Record<string, never>>(\n config: ActionClientConfig<TCtx> = {}\n): ActionBuilder<TCtx> {\n function buildAction<TInput, TData>(\n schema: ActionSchema<TInput> | undefined,\n fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>\n ): ActionFn<TData, TInput> {\n async function actionHandler(...args: unknown[]): Promise<ActionResult<TData>> {\n try {\n // Run middleware\n const ctx = await runActionMiddleware(config.middleware);\n\n // The input is the last argument in every call shape: `(input)`,\n // `(formData)`, `(prevState, payload)` from useActionState's\n // dispatch, `(initialState, formData)` on the no-JS path (Fizz binds\n // the initial state into the form, and decodeAction binds the\n // FormData after it), and `(...bound, prevState, payload)` for an\n // action with bound arguments. Nothing before it is input:\n // `prevState` comes from the client, so it is never read.\n const last = args.at(-1);\n const rawInput: unknown = schema && last instanceof FormData ? parseFormData(last) : last;\n\n // Resolve the sensitive-field stripping predicate once per invocation.\n // Precedence: per-action (config.stripSensitiveFields) > global\n // (forms.stripSensitiveFields from timber.config.ts) > built-in deny-list.\n // See TIM-816.\n const sensitivePredicate = resolveSensitivePredicate(\n config.stripSensitiveFields,\n getGlobalSensitiveFieldsConfig()\n );\n\n // Capture a \"safe-to-echo\" snapshot of the raw input once. Files are\n // stripped (can't serialize, shouldn't echo back) and sensitive fields\n // (passwords, tokens, CVV, etc.) are removed before they would land\n // in the RSC payload → client form `defaultValue` → DOM.\n const buildSubmittedValues = (): Record<string, unknown> | undefined => {\n const withoutFiles = stripFiles(rawInput);\n if (withoutFiles === undefined) return undefined;\n return stripSensitiveFields(withoutFiles, sensitivePredicate);\n };\n\n // Validate file sizes before schema validation.\n if (config.fileSizeLimit !== undefined && rawInput && typeof rawInput === 'object') {\n const fileSizeErrors = validateFileSizes(\n rawInput as Record<string, unknown>,\n config.fileSizeLimit\n );\n if (fileSizeErrors) {\n return { validationErrors: fileSizeErrors, submittedValues: buildSubmittedValues() };\n }\n }\n\n // Capture submitted values for repopulation on validation failure.\n const submittedValues = schema ? buildSubmittedValues() : undefined;\n\n // Validate with schema if provided\n let input: TInput;\n if (schema) {\n if (isStandardSchema(schema)) {\n // Standard Schema protocol (Zod ≥3.24, Valibot ≥1.0, ArkType)\n const result = schema['~standard'].validate(rawInput);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] createActionClient: schema returned a Promise — only sync schemas are supported.'\n );\n }\n if (result.issues) {\n const validationErrors = extractStandardSchemaErrors(result.issues);\n logValidationFailure(validationErrors);\n return { validationErrors, submittedValues };\n }\n input = result.value;\n } else if (typeof schema.safeParse === 'function') {\n const result = schema.safeParse(rawInput);\n if (!result.success) {\n const validationErrors = extractValidationErrors(result.error);\n logValidationFailure(validationErrors);\n return { validationErrors, submittedValues };\n }\n input = result.data;\n } else {\n try {\n input = schema.parse(rawInput);\n } catch (parseError) {\n const validationErrors = extractValidationErrors(parseError as SchemaError);\n logValidationFailure(validationErrors);\n return { validationErrors, submittedValues };\n }\n }\n } else {\n input = rawInput as TInput;\n }\n\n // Execute the action body\n const data = await fn({ ctx, input });\n return { data };\n } catch (error) {\n // Re-throw redirect/deny signals — these are control flow, not errors.\n // They must propagate to executeAction() which converts them to proper\n // HTTP responses (302 redirect, 4xx deny). Catching them here would\n // wrap them as INTERNAL_ERROR and break redirect()/redirectExternal()/deny().\n if (isRedirectSignal(error) || isDenySignal(error)) {\n throw error;\n }\n return handleActionError(error);\n }\n }\n\n return actionHandler as ActionFn<TData, TInput>;\n }\n\n return {\n schema<TInput>(schema: ActionSchema<TInput>) {\n return {\n action<TData>(\n fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>\n ): ActionFn<TData, TInput> {\n return buildAction(schema, fn);\n },\n };\n },\n action<TData>(\n fn: (ctx: ActionContext<TCtx, undefined>) => Promise<TData>\n ): ActionFn<TData, undefined> {\n return buildAction(undefined, fn as (ctx: ActionContext<TCtx, unknown>) => Promise<TData>);\n },\n };\n}\n\n// ─── validated() ────────────────────────────────────────────────────────\n\n/**\n * Convenience wrapper for the common case: validate input, run handler.\n * No middleware needed.\n *\n * @example\n * ```ts\n * 'use server'\n * import { validated } from '@timber-js/app/server'\n * import { z } from 'zod'\n *\n * export const createTodo = validated(\n * z.object({ title: z.string().min(1) }),\n * async (input) => {\n * await db.todos.create(input)\n * }\n * )\n * ```\n */\nexport function validated<TInput, TData>(\n schema: ActionSchema<TInput>,\n handler: (input: TInput) => Promise<TData>\n): ActionFn<TData, TInput> {\n return createActionClient()\n .schema(schema)\n .action(async ({ input }) => handler(input));\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────\n\n/**\n * Log validation failures in dev mode so developers can see what went wrong.\n * In production, validation errors are only returned to the client.\n */\nfunction logValidationFailure(errors: ValidationErrors): void {\n const isDev = isDebug();\n if (!isDev) return;\n\n const fields = Object.entries(errors)\n .map(([field, messages]) => ` ${field}: ${messages.join(', ')}`)\n .join('\\n');\n console.warn(`[timber] action schema validation failed:\\n${fields}`);\n}\n\n/**\n * Validate that all File objects in the input are within the size limit.\n * Returns validation errors keyed by field name, or null if all files are ok.\n */\nfunction validateFileSizes(input: Record<string, unknown>, limit: number): ValidationErrors | null {\n const limitKb = Math.round(limit / 1024);\n const limitLabel =\n limit >= 1024 * 1024 ? `${Math.round(limit / (1024 * 1024))}MB` : `${limitKb}KB`;\n\n const errors: ValidationErrors = {};\n\n function walk(obj: Record<string, unknown>, prefix: string): void {\n for (const [key, value] of Object.entries(obj)) {\n const path = prefix ? `${prefix}.${key}` : key;\n if (value instanceof File && value.size > limit) {\n errors[path] = [\n `File \"${value.name}\" (${formatSize(value.size)}) exceeds the ${limitLabel} limit`,\n ];\n } else if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const item = value[i];\n const itemPath = `${path}[${i}]`;\n if (item instanceof File && item.size > limit) {\n (errors[itemPath] ??= []).push(\n `File \"${item.name}\" (${formatSize(item.size)}) exceeds the ${limitLabel} limit`\n );\n } else if (typeof item === 'object' && item !== null && !(item instanceof File)) {\n walk(item as Record<string, unknown>, itemPath);\n }\n }\n } else if (typeof value === 'object' && value !== null && !(value instanceof File)) {\n walk(value as Record<string, unknown>, path);\n }\n }\n }\n\n walk(input, '');\n return Object.keys(errors).length > 0 ? errors : null;\n}\n\n/**\n * Strip File objects from a value, returning a plain object safe for\n * serialization. File objects can't be serialized and shouldn't be echoed back.\n */\nfunction stripFiles(value: unknown): Record<string, unknown> | undefined {\n if (value === null || value === undefined) return undefined;\n if (typeof value !== 'object') return undefined;\n\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (v instanceof File) continue;\n if (Array.isArray(v)) {\n result[k] = v\n .filter((item) => !(item instanceof File))\n .map((item) =>\n typeof item === 'object' && item !== null && !(item instanceof File)\n ? (stripFiles(item) ?? {})\n : item\n );\n } else if (typeof v === 'object' && v !== null && !(v instanceof File)) {\n result[k] = stripFiles(v) ?? {};\n } else {\n result[k] = v;\n }\n }\n return result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/shared/redirect-type.ts","../../src/server/sensitive-fields.ts","../../src/server/action-client.ts"],"sourcesContent":["/**\n * Next.js redirect type discriminator.\n *\n * Provided for API compatibility with libraries that import `RedirectType`\n * from `next/navigation`. In timber, `redirect()` always uses `replace`\n * semantics (no history entry for the redirect itself).\n *\n * Lives in shared/ (isomorphic) so both the server primitives and the\n * client-only next/navigation shim export the same definition without the\n * client shim pulling in server code.\n */\nexport const RedirectType = {\n push: 'push',\n replace: 'replace',\n} as const;\n\nexport type RedirectTypeValue = (typeof RedirectType)[keyof typeof RedirectType];\n","/**\n * Sensitive field stripping — removes password/token/CVV-style fields\n * from form values before they are echoed back to the client as\n * `submittedValues` for form repopulation.\n *\n * Applied to both action paths:\n * - With-JS action path: `createActionClient()` in `action-client.ts`\n * - No-JS form POST path: `handleFormAction()` in `action-handler.ts`\n *\n * Why: on a validation failure, timber echoes submitted form values back so\n * the user doesn't have to re-type everything. Without filtering, plaintext\n * passwords / credit-card numbers / TOTP codes would travel through the RSC\n * stream (with-JS) or land in the HTML as `defaultValue` attributes (no-JS)\n * — ending up in browser history, proxy logs, disk caches, and the\n * back-forward cache.\n *\n * Safe by default: the built-in deny-list is applied unconditionally unless\n * the user explicitly opts out via `forms.stripSensitiveFields: false` in\n * `timber.config.ts` or per-action via `createActionClient({ stripSensitiveFields: false })`.\n *\n * See design/08-forms-and-actions.md §\"Validation errors\"\n * See design/13-security.md §\"Sensitive field stripping\"\n * See TIM-816\n */\n\nimport { isDebug } from './debug.ts';\n\n// ─── Public types ────────────────────────────────────────────────────────\n\n/**\n * How to strip sensitive fields from `submittedValues`.\n *\n * - `true` / `undefined` — use the built-in deny-list (default, safe).\n * - `false` — do not strip anything (dev convenience; never do this in prod).\n * - `string[]` — additional field names to strip, merged with the built-in list.\n * - `(name) => boolean` — custom predicate, fully replaces the built-in list.\n * Return `true` to strip, `false` to keep. The `name` argument is the raw\n * (un-normalized) field name as it appeared in the submitted form.\n */\nexport type SensitiveFieldsOption = boolean | readonly string[] | ((name: string) => boolean);\n\n// ─── Built-in deny-list ──────────────────────────────────────────────────\n\n/**\n * Substring patterns matched against the normalized field name.\n * Normalization = lowercase + strip `_` and `-`.\n *\n * Any field whose normalized name *contains* one of these strings is\n * considered sensitive. Entries like `currentPassword`, `passwordConfirmation`,\n * and `user.password` all match via the `password` substring.\n */\nconst BUILTIN_SUBSTRING_PATTERNS: readonly string[] = [\n 'password',\n 'passwd',\n 'pwd',\n 'secret',\n 'apikey',\n 'accesstoken',\n 'refreshtoken',\n 'cvv',\n 'cvc',\n 'cardnumber',\n 'cardcvc',\n 'ssn',\n 'socialsecuritynumber',\n 'otp',\n 'totp',\n 'mfacode',\n 'twofactorcode',\n 'privatekey',\n];\n\n/**\n * Exact matches against the normalized field name. These are field names that\n * are too short or too common to substring-match safely. e.g. `token` alone\n * would match `csrfToken`, which is not sensitive — so `token` is exact-only,\n * while legitimate token fields are covered by `accesstoken` / `refreshtoken`.\n */\nconst BUILTIN_EXACT_PATTERNS: readonly string[] = ['token'];\n\n/**\n * Normalize a field name for deny-list comparison.\n * Lowercases the string and strips `_` and `-` so camelCase, snake_case, and\n * kebab-case variants all compare equal (`api_key` / `apiKey` / `api-key` →\n * `apikey`).\n */\nfunction normalize(name: string): string {\n let out = '';\n for (let i = 0; i < name.length; i++) {\n const ch = name.charCodeAt(i);\n if (ch === 0x5f /* _ */ || ch === 0x2d /* - */) continue;\n // A-Z → a-z\n if (ch >= 0x41 && ch <= 0x5a) {\n out += String.fromCharCode(ch + 32);\n } else {\n out += name[i];\n }\n }\n return out;\n}\n\n/**\n * Check whether a name matches the built-in deny-list (with optional extras).\n * Extras are merged into the substring pattern list after normalization.\n */\nfunction isBuiltinSensitive(name: string, extras?: readonly string[]): boolean {\n const normalized = normalize(name);\n if (BUILTIN_EXACT_PATTERNS.includes(normalized)) return true;\n for (const pattern of BUILTIN_SUBSTRING_PATTERNS) {\n if (normalized.includes(pattern)) return true;\n }\n if (extras && extras.length > 0) {\n for (const extra of extras) {\n const normExtra = normalize(extra);\n if (normExtra.length === 0) continue;\n if (normalized.includes(normExtra)) return true;\n }\n }\n return false;\n}\n\n// ─── Predicate resolution ────────────────────────────────────────────────\n\n/**\n * A resolved predicate: `null` means \"don't strip anything\" (the option was\n * explicitly `false`). Otherwise a function from raw field name → boolean.\n */\nexport type ResolvedSensitivePredicate = ((name: string) => boolean) | null;\n\n/**\n * Resolve a `SensitiveFieldsOption` into a concrete predicate.\n * Precedence: per-action > global > built-in default.\n *\n * - Per-action `undefined` → fall back to global.\n * - Global `undefined` → use built-in list.\n * - Either level set to `false` → disable stripping entirely (returns `null`).\n * - `true` → built-in list.\n * - `string[]` → built-in ∪ extras.\n * - function → custom, replaces the built-in list entirely.\n */\nexport function resolveSensitivePredicate(\n perAction: SensitiveFieldsOption | undefined,\n global: SensitiveFieldsOption | undefined\n): ResolvedSensitivePredicate {\n const chosen = perAction !== undefined ? perAction : global;\n\n if (chosen === false) return null;\n if (chosen === undefined || chosen === true) {\n return (name) => isBuiltinSensitive(name);\n }\n if (typeof chosen === 'function') {\n return chosen;\n }\n // Array of extra names merged with the built-in list.\n const extras = chosen;\n return (name) => isBuiltinSensitive(name, extras);\n}\n\n// ─── Module-level global config ──────────────────────────────────────────\n\nlet globalConfig: SensitiveFieldsOption | undefined;\n\n/**\n * Set the global `forms.stripSensitiveFields` config from `timber.config.ts`.\n * Called once at startup from `rsc-entry`.\n */\nexport function setGlobalSensitiveFieldsConfig(option: SensitiveFieldsOption | undefined): void {\n globalConfig = option;\n}\n\n/** Read the global `forms.stripSensitiveFields` config. */\nexport function getGlobalSensitiveFieldsConfig(): SensitiveFieldsOption | undefined {\n return globalConfig;\n}\n\n// ─── Stripping ───────────────────────────────────────────────────────────\n\n// One warning per field name per process — prevents log spam when a form is\n// submitted many times in dev mode.\nconst warnedFields = new Set<string>();\n\nfunction warnStripped(name: string): void {\n if (!isDebug()) return;\n if (warnedFields.has(name)) return;\n warnedFields.add(name);\n console.warn(\n `[timber] stripped sensitive field \"${name}\" from submittedValues. ` +\n `Override via forms.stripSensitiveFields in timber.config.ts.`\n );\n}\n\n/**\n * Walk an object (recursively) and return a copy with every key matching\n * `predicate` removed. Nested objects like `{ user: { password: '...' } }`\n * are handled — `user.password` is stripped while other `user.*` fields remain.\n *\n * - Arrays are walked element-wise (object entries inside arrays are cleaned).\n * - Non-plain values (strings, numbers, Files, Dates, etc.) are returned as-is.\n * - When a stripped key is encountered, it is omitted from the result entirely\n * — we do NOT set it to an empty string, because that would overwrite a\n * valid `defaultValue` the form author might have set.\n */\nexport function stripSensitiveFields<T>(value: T, predicate: ResolvedSensitivePredicate): T {\n // Null predicate = stripping disabled entirely.\n if (predicate === null) return value;\n if (value === null || value === undefined) return value;\n if (typeof value !== 'object') return value;\n if (value instanceof File || value instanceof Date) return value;\n\n if (Array.isArray(value)) {\n return value.map((item) => stripSensitiveFields(item, predicate)) as unknown as T;\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {\n if (predicate(key)) {\n warnStripped(key);\n continue;\n }\n result[key] = stripSensitiveFields(nested, predicate);\n }\n return result as unknown as T;\n}\n\n// ─── Test helpers ────────────────────────────────────────────────────────\n\n/** Reset the \"warned once\" cache. Exposed for tests. */\nexport function __resetSensitiveFieldsWarnings(): void {\n warnedFields.clear();\n}\n","/**\n * createActionClient — typed middleware and schema validation for server actions.\n *\n * Inspired by next-safe-action. Provides a builder API:\n * createActionClient({ middleware }) → .schema(z.object(...)) → .action(fn)\n *\n * The resulting action function satisfies both:\n * 1. Direct call: action(input) → Promise<ActionResult>\n * 2. React useActionState: (prevState, formData) => Promise<ActionResult>\n *\n * See design/08-forms-and-actions.md §\"Middleware and Server Actions\"\n */\n\n// ─── ActionError ─────────────────────────────────────────────────────────\n\n/**\n * Typed error class for server actions. Carries a string code and optional data.\n * When thrown from middleware or the action body, the action short-circuits and\n * the client receives `result.serverError`.\n *\n * In production, unexpected errors (non-ActionError) return `{ code: 'INTERNAL_ERROR' }`\n * with no message. In dev, `data.message` is included.\n */\nexport class ActionError<TCode extends string = string> extends Error {\n readonly code: TCode;\n readonly data: Record<string, unknown> | undefined;\n\n constructor(code: TCode, data?: Record<string, unknown>) {\n super(`ActionError: ${code}`);\n this.name = 'ActionError';\n this.code = code;\n this.data = data;\n }\n}\n\n// ─── Standard Schema ──────────────────────────────────────────────────────\n\n/**\n * Standard Schema v1 interface (subset).\n * Zod ≥3.24, Valibot ≥1.0, and ArkType all implement this.\n * See https://github.com/standard-schema/standard-schema\n *\n * We use permissive types here to accept all compliant libraries without\n * requiring exact structural matches on issues/path shapes.\n */\ninterface StandardSchemaV1<Output = unknown> {\n readonly '~standard': {\n readonly validate: (\n value: unknown\n ) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;\n };\n}\n\ntype StandardSchemaResult<Output> =\n | { value: Output; issues?: undefined }\n | { value?: undefined; issues: ReadonlyArray<StandardSchemaIssue> };\n\ninterface StandardSchemaIssue {\n message: string;\n path?: ReadonlyArray<PropertyKey | { key: PropertyKey }>;\n}\n\n/** Check if a schema implements the Standard Schema protocol. */\nfunction isStandardSchema(schema: unknown): schema is StandardSchemaV1 {\n return (\n typeof schema === 'object' &&\n schema !== null &&\n '~standard' in schema &&\n typeof (schema as StandardSchemaV1)['~standard'].validate === 'function'\n );\n}\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\n/**\n * Minimal schema interface — compatible with Zod, Valibot, ArkType, etc.\n *\n * Accepts either:\n * - Standard Schema (preferred): any object with `~standard.validate()`\n * - Legacy parse interface: objects with `.parse()` / `.safeParse()`\n *\n * At runtime, Standard Schema is detected via `~standard` property and\n * takes priority over the legacy interface.\n */\nexport type ActionSchema<T = unknown> = StandardSchemaV1<T> | LegacyActionSchema<T>;\n\n/** Legacy schema interface with .parse() / .safeParse(). */\ninterface LegacyActionSchema<T = unknown> {\n 'parse'(data: unknown): T;\n 'safeParse'?(data: unknown): { success: true; data: T } | { success: false; error: SchemaError };\n // Exclude Standard Schema objects from matching this interface\n '~standard'?: never;\n}\n\n/** Schema validation error shape (for legacy .safeParse()/.parse() interface). */\nexport interface SchemaError {\n issues?: Array<{ path?: Array<string | number>; message: string }>;\n flatten?(): { fieldErrors: Record<string, string[]> };\n}\n\n/** Flattened validation errors keyed by field name. */\nexport type ValidationErrors = Record<string, string[]>;\n\n/** Middleware function: returns context to merge into the action body's ctx. */\nexport type ActionMiddleware<TCtx = Record<string, unknown>> = () => Promise<TCtx> | TCtx;\n\n/** The result type returned to the client. */\nexport type ActionResult<TData = unknown> =\n | { data: TData; validationErrors?: never; serverError?: never; submittedValues?: never }\n | {\n data?: never;\n validationErrors: ValidationErrors;\n serverError?: never;\n /** Raw input values on validation failure — for repopulating form fields. */\n submittedValues?: Record<string, unknown>;\n }\n | {\n data?: never;\n validationErrors?: never;\n serverError: { code: string; data?: Record<string, unknown> };\n /**\n * The submitted form, when the input was FormData — for repopulating\n * form fields, which React resets after the action (TIM-1573).\n */\n submittedValues?: Record<string, unknown>;\n };\n\n/** Context passed to the action body. */\nexport interface ActionContext<TCtx, TInput> {\n ctx: TCtx;\n input: TInput;\n}\n\n// ─── Builder ─────────────────────────────────────────────────────────────\n\ninterface ActionClientConfig<TCtx> {\n middleware?: ActionMiddleware<TCtx> | ActionMiddleware<Record<string, unknown>>[];\n /** Max file size in bytes. Files exceeding this are rejected with validation errors. */\n fileSizeLimit?: number;\n /**\n * Override the sensitive-field deny-list for this action client.\n * See `SensitiveFieldsOption` in `./sensitive-fields.ts`. Per-action config\n * takes precedence over the global `forms.stripSensitiveFields` option in\n * `timber.config.ts`. See design/08-forms-and-actions.md and TIM-816.\n */\n stripSensitiveFields?: SensitiveFieldsOption;\n}\n\n/** Intermediate builder returned by createActionClient(). */\nexport interface ActionBuilder<TCtx> {\n /** Declare the input schema. Validation errors are returned typed. */\n schema<TInput>(schema: ActionSchema<TInput>): ActionBuilderWithSchema<TCtx, TInput>;\n /** Define the action body without input validation. */\n action<TData>(\n fn: (ctx: ActionContext<TCtx, undefined>) => Promise<TData>\n ): ActionFn<TData, undefined>;\n}\n\n/** Builder after .schema() has been called. */\nexport interface ActionBuilderWithSchema<TCtx, TInput> {\n /** Define the action body with validated input. */\n action<TData>(fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>): ActionFn<TData, TInput>;\n}\n\n/**\n * The final action function. Callable three ways:\n * - Direct: action(input) → Promise<ActionResult<TData>>\n * - React useActionState: action(prevState, payload) → Promise<ActionResult<TData>>,\n * where payload is the submitted FormData or the value dispatch was called with\n * - React <form action={fn}>: action(formData) → void (return value ignored by React)\n *\n * The third overload exists purely for type compatibility with React's\n * `<form action>` prop, which expects `(formData: FormData) => void`.\n * At runtime the function still returns Promise<ActionResult>, but React\n * discards it. This lets validated actions be passed directly to forms\n * without casts.\n */\n/**\n * Map schema output keys to `string | undefined` for form-facing APIs.\n * HTML form values are always strings, and fields can be absent.\n * Gives autocomplete for field names without lying about value types.\n */\nexport type InputHint<T> =\n T extends Record<string, unknown> ? { [K in keyof T]: string | undefined } : T;\n\n/**\n * ActionFn — the callable returned by `createActionClient().action()`.\n *\n * Generic order: `<TData, TInput>` — TData first for backward compatibility.\n * Previously ActionFn had a single `<TData>` generic, so existing code like\n * `ActionFn<MyResult>` must still work with TData in the first position.\n * See TIM-797.\n */\nexport type ActionFn<TData = unknown, TInput = unknown> = {\n /** <form action={fn}> compatibility — React discards the return value. */\n (formData: FormData): void;\n /** Direct call: action(input) — optional when TInput is undefined/unknown (no-schema actions). */\n (\n ...args: undefined extends TInput ? [input?: TInput] : [input: TInput]\n ): Promise<ActionResult<TData>>;\n /**\n * React useActionState: action(prevState, payload). The payload is the\n * submitted FormData, or whatever the hook's dispatch was called with.\n */\n (prevState: ActionResult<TData> | null, payload: TInput | FormData): Promise<ActionResult<TData>>;\n};\n\n// ─── Implementation ──────────────────────────────────────────────────────\n\n/**\n * Run middleware array or single function. Returns merged context.\n */\nasync function runActionMiddleware<TCtx>(\n middleware: ActionMiddleware<TCtx> | ActionMiddleware<Record<string, unknown>>[] | undefined\n): Promise<TCtx> {\n if (!middleware) {\n return {} as TCtx;\n }\n\n if (Array.isArray(middleware)) {\n let merged = {} as Record<string, unknown>;\n for (const mw of middleware) {\n const result = await mw();\n merged = { ...merged, ...result };\n }\n return merged as TCtx;\n }\n\n return await middleware();\n}\n\n// Re-export parseFormData for use throughout the framework\nimport { parseFormData } from './form-data.ts';\nimport { formatSize } from '../utils/format.ts';\nimport { isDebug, isDevMode } from './debug.ts';\nimport { isDenySignal, isRedirectSignal } from './signal-identity.ts';\nimport {\n stripSensitiveFields,\n resolveSensitivePredicate,\n getGlobalSensitiveFieldsConfig,\n type SensitiveFieldsOption,\n} from './sensitive-fields.ts';\n\n/**\n * Extract validation errors from a schema error.\n * Supports Zod's flatten() and generic issues array.\n */\nfunction extractValidationErrors(error: SchemaError): ValidationErrors {\n // Zod-style flatten\n if (typeof error.flatten === 'function') {\n return error.flatten().fieldErrors;\n }\n\n // Generic issues array\n if (error.issues) {\n const errors: ValidationErrors = {};\n for (const issue of error.issues) {\n const path = issue.path?.join('.') ?? '_root';\n if (!errors[path]) errors[path] = [];\n errors[path].push(issue.message);\n }\n return errors;\n }\n\n return { _root: ['Validation failed'] };\n}\n\n/**\n * Extract validation errors from Standard Schema issues.\n */\nfunction extractStandardSchemaErrors(issues: ReadonlyArray<StandardSchemaIssue>): ValidationErrors {\n const errors: ValidationErrors = {};\n for (const issue of issues) {\n const path =\n issue.path\n ?.map((p) => {\n // Standard Schema path items can be { key: ... } objects or bare PropertyKey values\n if (typeof p === 'object' && p !== null && 'key' in p) return String(p.key);\n return String(p);\n })\n .join('.') ?? '_root';\n if (!errors[path]) errors[path] = [];\n errors[path].push(issue.message);\n }\n return Object.keys(errors).length > 0 ? errors : { _root: ['Validation failed'] };\n}\n\n/**\n * Wrap unexpected errors into a safe server error result.\n * ActionError → typed result. Other errors → INTERNAL_ERROR (no leak).\n *\n * `createActionClient` actions only. A raw `'use server'` function that\n * throws is rejected on the client instead (action-handler.ts).\n */\nexport function handleActionError(error: unknown): {\n serverError: { code: string; data?: Record<string, unknown> };\n} {\n if (error instanceof ActionError) {\n return {\n serverError: {\n code: error.code,\n ...(error.data ? { data: error.data } : {}),\n },\n };\n }\n\n // In dev, include the message for debugging.\n // Uses isDevMode() — NOT isDebug() — because this data is sent to the\n // browser. TIMBER_DEBUG must never cause error messages to leak to clients.\n // See design/13-security.md principle 4: \"Errors don't leak.\"\n const devMode = isDevMode();\n return {\n serverError: {\n code: 'INTERNAL_ERROR',\n ...(devMode && error instanceof Error ? { data: { message: error.message } } : {}),\n },\n };\n}\n\n/**\n * Create a typed action client with middleware and schema validation.\n *\n * @example\n * ```ts\n * const action = createActionClient({\n * middleware: async () => {\n * const user = await getUser()\n * if (!user) throw new ActionError('UNAUTHORIZED')\n * return { user }\n * },\n * })\n *\n * export const createTodo = action\n * .schema(z.object({ title: z.string().min(1) }))\n * .action(async ({ input, ctx }) => {\n * await db.todos.create({ ...input, userId: ctx.user.id })\n * })\n * ```\n */\nexport function createActionClient<TCtx = Record<string, never>>(\n config: ActionClientConfig<TCtx> = {}\n): ActionBuilder<TCtx> {\n function buildAction<TInput, TData>(\n schema: ActionSchema<TInput> | undefined,\n fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>\n ): ActionFn<TData, TInput> {\n async function actionHandler(...args: unknown[]): Promise<ActionResult<TData>> {\n // The input is the last argument in every call shape: `(input)`,\n // `(formData)`, `(prevState, payload)` from useActionState's\n // dispatch, `(initialState, formData)` on the no-JS path (Fizz binds\n // the initial state into the form, and decodeAction binds the\n // FormData after it), and `(...bound, prevState, payload)` for an\n // action with bound arguments. Nothing before it is input:\n // `prevState` comes from the client, so it is never read.\n const last = args.at(-1);\n const form = last instanceof FormData ? parseFormData(last) : undefined;\n\n // Resolve the sensitive-field stripping predicate once per invocation.\n // Precedence: per-action (config.stripSensitiveFields) > global\n // (forms.stripSensitiveFields from timber.config.ts) > built-in deny-list.\n // See TIM-816.\n const sensitivePredicate = resolveSensitivePredicate(\n config.stripSensitiveFields,\n getGlobalSensitiveFieldsConfig()\n );\n\n // A \"safe-to-echo\" copy of the input. Files are stripped (can't\n // serialize, shouldn't echo back) and sensitive fields (passwords,\n // tokens, CVV, etc.) are removed before they would land in the RSC\n // payload → client form `defaultValue` → DOM.\n const echo = (value: unknown): Record<string, unknown> | undefined => {\n const withoutFiles = stripFiles(value);\n if (withoutFiles === undefined) return undefined;\n return stripSensitiveFields(withoutFiles, sensitivePredicate);\n };\n\n try {\n // Run middleware\n const ctx = await runActionMiddleware(config.middleware);\n\n // Without a schema the action receives the FormData itself. Checks\n // and echoes read the parsed form: a FormData has no own entries.\n const rawInput: unknown = schema && form ? form : last;\n const submitted: unknown = form ?? rawInput;\n const buildSubmittedValues = () => echo(submitted);\n\n // Validate file sizes before schema validation.\n if (config.fileSizeLimit !== undefined && submitted && typeof submitted === 'object') {\n const fileSizeErrors = validateFileSizes(\n submitted as Record<string, unknown>,\n config.fileSizeLimit\n );\n if (fileSizeErrors) {\n return { validationErrors: fileSizeErrors, submittedValues: buildSubmittedValues() };\n }\n }\n\n // Capture submitted values for repopulation on validation failure.\n const submittedValues = schema ? buildSubmittedValues() : undefined;\n\n // Validate with schema if provided\n let input: TInput;\n if (schema) {\n if (isStandardSchema(schema)) {\n // Standard Schema protocol (Zod ≥3.24, Valibot ≥1.0, ArkType)\n const result = schema['~standard'].validate(rawInput);\n if (result instanceof Promise) {\n throw new Error(\n '[timber] createActionClient: schema returned a Promise — only sync schemas are supported.'\n );\n }\n if (result.issues) {\n const validationErrors = extractStandardSchemaErrors(result.issues);\n logValidationFailure(validationErrors);\n return { validationErrors, submittedValues };\n }\n input = result.value;\n } else if (typeof schema.safeParse === 'function') {\n const result = schema.safeParse(rawInput);\n if (!result.success) {\n const validationErrors = extractValidationErrors(result.error);\n logValidationFailure(validationErrors);\n return { validationErrors, submittedValues };\n }\n input = result.data;\n } else {\n try {\n input = schema.parse(rawInput);\n } catch (parseError) {\n const validationErrors = extractValidationErrors(parseError as SchemaError);\n logValidationFailure(validationErrors);\n return { validationErrors, submittedValues };\n }\n }\n } else {\n input = rawInput as TInput;\n }\n\n // Execute the action body\n const data = await fn({ ctx, input });\n return { data };\n } catch (error) {\n // Re-throw redirect/deny signals — these are control flow, not errors.\n // They must propagate to executeAction() which converts them to proper\n // HTTP responses (302 redirect, 4xx deny). Catching them here would\n // wrap them as INTERNAL_ERROR and break redirect()/redirectExternal()/deny().\n if (isRedirectSignal(error) || isDenySignal(error)) {\n throw error;\n }\n // A `serverError` echoes the form like a validation failure does.\n // React resets the form after the action, and a form whose defaults\n // fell back to page data would lose what the user typed (TIM-1573).\n const result = handleActionError(error);\n const submittedValues = form && echo(form);\n return submittedValues ? { ...result, submittedValues } : result;\n }\n }\n\n return actionHandler as ActionFn<TData, TInput>;\n }\n\n return {\n schema<TInput>(schema: ActionSchema<TInput>) {\n return {\n action<TData>(\n fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>\n ): ActionFn<TData, TInput> {\n return buildAction(schema, fn);\n },\n };\n },\n action<TData>(\n fn: (ctx: ActionContext<TCtx, undefined>) => Promise<TData>\n ): ActionFn<TData, undefined> {\n return buildAction(undefined, fn as (ctx: ActionContext<TCtx, unknown>) => Promise<TData>);\n },\n };\n}\n\n// ─── validated() ────────────────────────────────────────────────────────\n\n/**\n * Convenience wrapper for the common case: validate input, run handler.\n * No middleware needed.\n *\n * @example\n * ```ts\n * 'use server'\n * import { validated } from '@timber-js/app/server'\n * import { z } from 'zod'\n *\n * export const createTodo = validated(\n * z.object({ title: z.string().min(1) }),\n * async (input) => {\n * await db.todos.create(input)\n * }\n * )\n * ```\n */\nexport function validated<TInput, TData>(\n schema: ActionSchema<TInput>,\n handler: (input: TInput) => Promise<TData>\n): ActionFn<TData, TInput> {\n return createActionClient()\n .schema(schema)\n .action(async ({ input }) => handler(input));\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────\n\n/**\n * Log validation failures in dev mode so developers can see what went wrong.\n * In production, validation errors are only returned to the client.\n */\nfunction logValidationFailure(errors: ValidationErrors): void {\n const isDev = isDebug();\n if (!isDev) return;\n\n const fields = Object.entries(errors)\n .map(([field, messages]) => ` ${field}: ${messages.join(', ')}`)\n .join('\\n');\n console.warn(`[timber] action schema validation failed:\\n${fields}`);\n}\n\n/**\n * Validate that all File objects in the input are within the size limit.\n * Returns validation errors keyed by field name, or null if all files are ok.\n * Keys are dot paths (`rows.0.file`), the form a field name and a Standard\n * Schema error use, so `getFieldError` finds a file in a list.\n */\nfunction validateFileSizes(input: Record<string, unknown>, limit: number): ValidationErrors | null {\n const limitKb = Math.round(limit / 1024);\n const limitLabel =\n limit >= 1024 * 1024 ? `${Math.round(limit / (1024 * 1024))}MB` : `${limitKb}KB`;\n\n const errors: ValidationErrors = {};\n\n function walk(obj: Record<string, unknown>, prefix: string): void {\n for (const [key, value] of Object.entries(obj)) {\n const path = prefix ? `${prefix}.${key}` : key;\n if (value instanceof File && value.size > limit) {\n errors[path] = [\n `File \"${value.name}\" (${formatSize(value.size)}) exceeds the ${limitLabel} limit`,\n ];\n } else if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const item = value[i];\n const itemPath = `${path}.${i}`;\n if (item instanceof File && item.size > limit) {\n (errors[itemPath] ??= []).push(\n `File \"${item.name}\" (${formatSize(item.size)}) exceeds the ${limitLabel} limit`\n );\n } else if (typeof item === 'object' && item !== null && !(item instanceof File)) {\n walk(item as Record<string, unknown>, itemPath);\n }\n }\n } else if (typeof value === 'object' && value !== null && !(value instanceof File)) {\n walk(value as Record<string, unknown>, path);\n }\n }\n }\n\n walk(input, '');\n return Object.keys(errors).length > 0 ? errors : null;\n}\n\n/**\n * Strip File objects from a value, returning a copy safe for serialization.\n * File objects can't be serialized and shouldn't be echoed back.\n *\n * Mirrors `parseFormData`'s output: objects and arrays at any depth, arrays\n * of arrays included. A File in an array becomes `undefined` rather than\n * being removed, so `rows.2` still names the third element (TIM-1573).\n */\nfunction stripFiles(value: unknown): Record<string, unknown> | undefined {\n if (value === null || typeof value !== 'object' || value instanceof File) return undefined;\n return stripFilesFrom(value) as Record<string, unknown>;\n}\n\nfunction stripFilesFrom(value: unknown): unknown {\n if (value instanceof File) return undefined;\n if (Array.isArray(value)) return value.map(stripFilesFrom);\n if (typeof value !== 'object' || value === null) return value;\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n if (!(v instanceof File)) result[k] = stripFilesFrom(v);\n }\n return result;\n}\n"],"mappings":"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"}
|
package/dist/server/internal.js
CHANGED
|
@@ -3,12 +3,12 @@ import { n as isRscContentType, r as isRscRequest } from "../_chunks/rsc-media-t
|
|
|
3
3
|
import { n as classifyUrlSegment, r as effectiveUrlSegment } from "../_chunks/segment-classify-C539Pa2O.js";
|
|
4
4
|
import { t as computeSegmentTreePaths } from "../_chunks/segment-keys-D5hu1hz4.js";
|
|
5
5
|
import { i as getMetadataRouteServePath, n as classifyMetadataRoute, r as getMetadataRouteAutoLink, t as METADATA_ROUTE_CONVENTIONS } from "../_chunks/metadata-routes-DSDjM_hJ.js";
|
|
6
|
-
import {
|
|
6
|
+
import { i as canonicalize, r as canonicalRequestPath } from "../_chunks/canonicalize-BAWkWiKK.js";
|
|
7
7
|
import { i as requestContextAls, n as prebuiltCaptureAls, o as timingAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-BZqHCtq-.js";
|
|
8
8
|
import { C as isRedirectSignal, F as withSpan, M as replaceTraceId, N as runWithTraceId, O as generateTraceId, P as setSpanAttribute, S as isDenySignal, _ as setLogger, a as logMiddlewareError, c as logRenderError, d as logRouteError, f as logRouteSetupError, g as logWaitUntilUnsupported, h as logWaitUntilRejected, j as getTraceId, k as getOtelTraceId, l as logRequestCompleted, m as logSwrRefetchFailed, n as logCacheMiss, o as logMiddlewareShortCircuit, p as logSlowRequest, r as logCsrfRejected, s as logProxyError, t as getLogger, u as logRequestReceived, v as swallow, w as isDebug, x as isControlFlowSignal } from "../_chunks/logger-CbLdcy-W.js";
|
|
9
9
|
import { i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DWppEdLB.js";
|
|
10
|
-
import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-ClUiXQnK.js";
|
|
11
|
-
import { C as setMutableCookieContext, S as setMatchedSegmentPath, T as runOutsideReactCacheScope, b as markResponseFlushed, f as getCookie, g as getHeader, h as applyRequestHeaderOverlay, i as coerce, m as getSetCookieHeaders, o as DenySignal, s as RedirectSignal, t as executeAction, v as getSearchParams, w as setSegmentParams, x as runWithRequestContext, y as getSegmentParams } from "../_chunks/actions-
|
|
10
|
+
import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, r as appVisibleSearch, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-ClUiXQnK.js";
|
|
11
|
+
import { C as setMutableCookieContext, S as setMatchedSegmentPath, T as runOutsideReactCacheScope, b as markResponseFlushed, f as getCookie, g as getHeader, h as applyRequestHeaderOverlay, i as coerce, m as getSetCookieHeaders, o as DenySignal, s as RedirectSignal, t as executeAction, v as getSearchParams, w as setSegmentParams, x as runWithRequestContext, y as getSegmentParams } from "../_chunks/actions-CUh3cClk.js";
|
|
12
12
|
import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as getWaitUntil, y as lastInvalidationEpochFor } from "../_chunks/cache-api-LA3sBpUS.js";
|
|
13
13
|
import { n as createSingleflight } from "../_chunks/singleflight-BeVMMKi0.js";
|
|
14
14
|
import { n as toBracketKey } from "../_chunks/resolve-schema-CBR6Lm4i.js";
|
|
@@ -580,6 +580,55 @@ function csrfGate(req, canonicalPath, csrf, matchRoute) {
|
|
|
580
580
|
return exempt ? null : csrfRejectionResponse(req, result);
|
|
581
581
|
}
|
|
582
582
|
/**
|
|
583
|
+
* The redirect for a safe request whose URL is not canonical, or null to
|
|
584
|
+
* serve it.
|
|
585
|
+
*
|
|
586
|
+
* A GET for `/admin/` used to render with a 200 at that URL. The server
|
|
587
|
+
* rendered with the canonical `/admin` while the browser hydrated with the
|
|
588
|
+
* raw `/admin/`, so anything branching on `usePathname()` hydrated
|
|
589
|
+
* mismatched (TIM-1571). Redirecting makes the address bar — the client's
|
|
590
|
+
* copy of the URL — the canonical one, and leaves one URL per page for
|
|
591
|
+
* caches and crawlers.
|
|
592
|
+
*
|
|
593
|
+
* Only GET and HEAD are redirected. Every other method — an action's POST,
|
|
594
|
+
* a CORS preflight — is served at the canonical path, as before: a
|
|
595
|
+
* redirected POST is re-sent by the browser, which an action must not
|
|
596
|
+
* depend on.
|
|
597
|
+
*
|
|
598
|
+
* Runs inside proxy.ts's `next()`, not before it, so proxy.ts still runs
|
|
599
|
+
* on every request and its headers land on the redirect. A cross-origin
|
|
600
|
+
* `GET /api/items/` needs proxy.ts's CORS headers on the 308 itself:
|
|
601
|
+
* browsers apply the CORS check to a redirect response.
|
|
602
|
+
*
|
|
603
|
+
* A document request gets a relative 308 (never built from `Host`). A page
|
|
604
|
+
* navigation gets the router's soft redirect, decided exactly as for a
|
|
605
|
+
* `redirect()` outcome: an RSC request that does not match a route.ts
|
|
606
|
+
* handler (`buildRedirectResponse`). The framework's `_rsc` cache key is
|
|
607
|
+
* dropped from the soft target; the router appends a fresh one when it
|
|
608
|
+
* fetches the canonical URL.
|
|
609
|
+
*
|
|
610
|
+
* See design/07-routing.md §"Non-Canonical URLs Redirect".
|
|
611
|
+
*/
|
|
612
|
+
function canonicalRedirect(req, canonicalPath, stripTrailingSlash, matchRoute) {
|
|
613
|
+
if (req.method !== "GET" && req.method !== "HEAD") return null;
|
|
614
|
+
const url = new URL(req.url);
|
|
615
|
+
const target = canonicalRequestPath(url.pathname, stripTrailingSlash);
|
|
616
|
+
if (target === url.pathname) return null;
|
|
617
|
+
const navigation = isRscRequest(req) && !isApiRoutePath(canonicalPath, matchRoute);
|
|
618
|
+
const search = navigation ? appVisibleSearch(url).search : url.search;
|
|
619
|
+
return buildRedirectResponse(new RedirectSignal(target + search, 308), new Headers(), navigation);
|
|
620
|
+
}
|
|
621
|
+
/** Whether `path` matches a route.ts handler. A matcher that throws: no. */
|
|
622
|
+
function isApiRoutePath(path, matchRoute) {
|
|
623
|
+
try {
|
|
624
|
+
const match = matchRoute(path);
|
|
625
|
+
return match !== null && isApiRouteChain(match.segments);
|
|
626
|
+
} catch (err) {
|
|
627
|
+
swallow(err, "canonical redirect: route match for route.ts lookup threw");
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
583
632
|
* Build a proxy resolver closure from the declared source. Called exactly
|
|
584
633
|
* once at `createPipeline` setup time, so the hot path sees only the branch
|
|
585
634
|
* that corresponds to this pipeline's configured variant.
|
|
@@ -2027,6 +2076,8 @@ function createPipeline(config) {
|
|
|
2027
2076
|
return csrfRejection;
|
|
2028
2077
|
}
|
|
2029
2078
|
const innerHandler = async () => {
|
|
2079
|
+
const redirect = canonicalRedirect(req, canonicalPath, stripTrailingSlash, config.matchRoute);
|
|
2080
|
+
if (redirect) return redirect;
|
|
2030
2081
|
if (config.dispatchAction) {
|
|
2031
2082
|
const actionResult = await config.dispatchAction(canonicalReq, canonicalPath, pipelineFn);
|
|
2032
2083
|
if (actionResult) return actionResult;
|