@timber-js/app 0.2.0-alpha.133 → 0.2.0-alpha.134
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/LICENSE +8 -0
- package/dist/_chunks/{actions-BNhpxrnV.js → actions-B8Fhsosm.js} +51 -12
- package/dist/_chunks/actions-B8Fhsosm.js.map +1 -0
- package/dist/_chunks/build-output-helper-K7oUCRqU.js.map +1 -1
- package/dist/_chunks/cli-schema-sync-q-Ce6-iG.js.map +1 -1
- package/dist/_chunks/codec-registry-DqkeAydt.js.map +1 -1
- package/dist/_chunks/define-DMq-Yjp7.js.map +1 -1
- package/dist/_chunks/define-jeHDXZGq.js.map +1 -1
- package/dist/_chunks/format-BjN9HLu9.js.map +1 -1
- package/dist/_chunks/handler-store-bIAstrxQ.js.map +1 -1
- package/dist/_chunks/href-validation-BJmD5mZh.js +124 -0
- package/dist/_chunks/href-validation-BJmD5mZh.js.map +1 -0
- package/dist/_chunks/logger-CMd9P1xQ.js.map +1 -1
- package/dist/_chunks/metadata-routes-DESG8uk5.js.map +1 -1
- package/dist/_chunks/navigation-root-B5-RmsPi.js.map +1 -1
- package/dist/_chunks/registry-BI9cgCt7.js.map +1 -1
- package/dist/_chunks/resolve-schema-DU2nbVOi.js.map +1 -1
- package/dist/_chunks/router-ref-B2-2kIIC.js.map +1 -1
- package/dist/_chunks/schema-bridge-o0SwFtXW.js.map +1 -1
- package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -1
- package/dist/_chunks/segment-context-Bg0pFbLT.js.map +1 -1
- package/dist/_chunks/ssr-data-DVX8S7vj.js.map +1 -1
- package/dist/_chunks/stale-reload-CCqb62Z3.js.map +1 -1
- package/dist/_chunks/tracing-DP5djRq5.js.map +1 -1
- package/dist/_chunks/use-query-states-C9G6LP0F.js.map +1 -1
- package/dist/_chunks/use-segment-params-CIeinIx2.js.map +1 -1
- package/dist/_chunks/validation-AfEFM5ze.js.map +1 -1
- package/dist/_chunks/walkers-BbEu2_Pp.js.map +1 -1
- package/dist/_chunks/warnings-CAzunaGo.js.map +1 -1
- package/dist/adapters/cloudflare-dev.js.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/index.js +5 -17
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +2 -4
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/use-router.d.ts.map +1 -1
- package/dist/codec.js.map +1 -1
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -1
- package/dist/params/index.js.map +1 -1
- package/dist/search-params/index.js.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +22 -11
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +5 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +5 -2
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/shared/href-validation.d.ts +38 -0
- package/dist/shared/href-validation.d.ts.map +1 -0
- package/dist/shims/font-google.js.map +1 -1
- package/dist/shims/font-local.js.map +1 -1
- package/package.json +10 -11
- package/src/cli.ts +0 -0
- package/src/client/link.tsx +5 -32
- package/src/client/use-router.ts +3 -0
- package/src/server/deny-renderer.ts +0 -1
- package/src/server/pipeline-interception.ts +1 -0
- package/src/server/pipeline-phases.ts +30 -19
- package/src/server/pipeline.ts +18 -1
- package/src/server/primitives.ts +8 -17
- package/src/server/rsc-entry/rsc-payload.ts +0 -4
- package/src/shared/href-validation.ts +123 -0
- package/dist/_chunks/actions-BNhpxrnV.js.map +0 -1
- package/dist/_chunks/merge-search-params-Cm1j99HC.js +0 -41
- package/dist/_chunks/merge-search-params-Cm1j99HC.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline-interception.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-interception.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,iDAAiD;AACjD,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"pipeline-interception.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-interception.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,iDAAiD;AACjD,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;CACxB;AAcD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,4BAA4B,EAAE,mBAAmB,EAAE,GACnE,uBAAuB,GAAG,IAAI,CAahC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAyBjF"}
|
|
@@ -46,7 +46,11 @@ export declare function runRenderPhase(config: PipelineConfig, req: Request, mat
|
|
|
46
46
|
*
|
|
47
47
|
* Used both as the top-level entry (when no proxy.ts is configured) and as
|
|
48
48
|
* the `next()` continuation passed to `runProxy()`.
|
|
49
|
+
*
|
|
50
|
+
* @param pathIsCanonical When true, `path` has already been canonicalized by
|
|
51
|
+
* `createPipeline` — skip re-canonicalization to prevent double-decode.
|
|
52
|
+
* When false (default), runs canonicalize as a safety net for direct callers.
|
|
49
53
|
*/
|
|
50
|
-
export declare function handleRequest(config: PipelineConfig, req: Request, method: string, path: string): Promise<Response>;
|
|
54
|
+
export declare function handleRequest(config: PipelineConfig, req: Request, method: string, path: string, pathIsCanonical?: boolean): Promise<Response>;
|
|
51
55
|
export {};
|
|
52
56
|
//# sourceMappingURL=pipeline-phases.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline-phases.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-phases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,OAAO,EAA2C,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEpG,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKrF,UAAU,aAAa;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAwBD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,CAavB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,YAAY,CAAC,CA6DvB;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,EAAE,iBAAiB,EAAE,YAAY,EAAE,EAAE,aAAa,GACjD,OAAO,CAAC,YAAY,CAAC,CAkBvB;AAID
|
|
1
|
+
{"version":3,"file":"pipeline-phases.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-phases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,OAAO,EAA2C,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEpG,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKrF,UAAU,aAAa;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAwBD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,CAavB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,YAAY,CAAC,CA6DvB;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,EAAE,iBAAiB,EAAE,YAAY,EAAE,EAAE,aAAa,GACjD,OAAO,CAAC,YAAY,CAAC,CAkBvB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,QAAQ,CAAC,CAwRnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/server/pipeline.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/server/pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -130,10 +130,13 @@ export declare function redirect(path: string, statusOrOptions?: number | Redire
|
|
|
130
130
|
*/
|
|
131
131
|
export declare function permanentRedirect(path: string, options?: Omit<RedirectOptions, 'status'>): never;
|
|
132
132
|
/**
|
|
133
|
-
* Redirect to an external URL. The
|
|
133
|
+
* Redirect to an external URL. The origin must be in the provided allow-list.
|
|
134
|
+
*
|
|
135
|
+
* Only http: and https: schemes are permitted. The allow-list is matched
|
|
136
|
+
* against full origins (scheme + host + port), not bare hostnames.
|
|
134
137
|
*
|
|
135
138
|
* @param url - Absolute URL to redirect to.
|
|
136
|
-
* @param allowList - Array of allowed
|
|
139
|
+
* @param allowList - Array of allowed origins (e.g. ['https://example.com', 'https://auth.example.com']).
|
|
137
140
|
* @param status - HTTP redirect status code (3xx). Defaults to 302.
|
|
138
141
|
*/
|
|
139
142
|
export declare function redirectExternal(url: string, allowList: string[], status?: number): never;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/server/primitives.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/server/primitives.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rsc-payload.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACrF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAQ/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,YAAY,EAAE,WAAW,EAAE,EAC3B,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"rsc-payload.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/rsc-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACrF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAQ/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,EACrC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,gBAAgB,EAAE,oBAAoB,EAAE,EACxC,YAAY,EAAE,WAAW,EAAE,EAC3B,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB,OAAO,CAAC,QAAQ,CAAC,CAqKnB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared href/URL validation for all navigation sinks.
|
|
3
|
+
*
|
|
4
|
+
* Used by <Link>, useRouter().push/replace, and redirectExternal().
|
|
5
|
+
* Single source of truth for dangerous-scheme detection — prevents
|
|
6
|
+
* control-character smuggling that bypasses naive regex checks.
|
|
7
|
+
*
|
|
8
|
+
* See design/13-security.md §"Link scheme injection" (test #9)
|
|
9
|
+
* and design/07-routing.md §"<Link> Scheme Validation".
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Validate an href for navigation sinks (<Link>, useRouter, etc.).
|
|
13
|
+
*
|
|
14
|
+
* Rejects dangerous schemes (javascript:, data:, vbscript:) even when
|
|
15
|
+
* control characters are embedded to bypass naive regex checks.
|
|
16
|
+
* Also rejects protocol-relative URLs (//evil.com).
|
|
17
|
+
*
|
|
18
|
+
* Throws on rejection. Safe hrefs pass through silently.
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateNavigationHref(href: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Check if an href is internal (same-origin navigation) or external.
|
|
23
|
+
*
|
|
24
|
+
* Strips control characters before checking so that "java\tscript:" is
|
|
25
|
+
* correctly identified as having a scheme (and thus not internal).
|
|
26
|
+
*/
|
|
27
|
+
export declare function isInternalHref(href: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Validate and canonicalize an external redirect URL.
|
|
30
|
+
*
|
|
31
|
+
* Requires http: or https: scheme and checks origin against an allow-list.
|
|
32
|
+
* Returns the parsed canonical URL string (from URL.href) to prevent
|
|
33
|
+
* raw user input from reaching the Location header.
|
|
34
|
+
*
|
|
35
|
+
* @throws On invalid URL, dangerous scheme, or origin not in allow-list.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateExternalRedirectUrl(url: string, allowList: string[]): string;
|
|
38
|
+
//# sourceMappingURL=href-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"href-validation.d.ts","sourceRoot":"","sources":["../../src/shared/href-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsBH;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAkBzD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUpD;AAID;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAiCpF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"font-google.js","names":[],"sources":["../../src/shims/font-google.ts"],"sourcesContent":["/**\n * Fallback shim for `next/font/google` → `@timber/fonts/google`.\n *\n * At build time, the timber-fonts plugin's transform hook replaces font\n * function calls with static FontResult objects containing real class names\n * and font stacks. This file serves two purposes:\n *\n * 1. **TypeScript resolution** — provides types for IDEs and `tsc` outside\n * of Vite's module graph (where the virtual module handles it).\n * 2. **Runtime fallback** — returns empty className/fontFamily values when\n * the plugin hasn't processed a call (e.g. in tests or non-Vite environments).\n *\n * The shim resolution path (`next/font/google` → `\\0@timber/fonts/google`)\n * is unchanged — this file is NOT on that resolution path. It exists as a\n * physical module for direct imports and type re-exports.\n *\n * Design doc: 24-fonts.md, \"Next.js Font Compatibility\"\n */\n\nimport type { GoogleFontConfig, FontResult } from '../fonts/types.js';\n\nexport type { GoogleFontConfig, FontResult };\n\n/**\n * Create a stub FontResult with empty values.\n *\n * The timber-fonts plugin replaces these calls at build time with real\n * class names and font stacks. This stub ensures code that imports\n * font functions outside of the build pipeline (tests, type-checking)\n * gets a valid FontResult shape without runtime errors.\n */\nfunction createStubFontResult(config?: GoogleFontConfig): FontResult {\n return {\n className: '',\n style: { fontFamily: '' },\n variable: config?.variable,\n };\n}\n\n/**\n * Generic font loader — accepts any font name, returns a stub FontResult.\n *\n * Named exports like `Inter`, `Roboto`, etc. are generated dynamically\n * by the virtual module at build time. This function provides a catch-all\n * for non-Vite contexts.\n */\nexport function createFont(_family: string, config?: GoogleFontConfig): FontResult {\n return createStubFontResult(config);\n}\n\n// Common Google Font exports for TypeScript autocomplete.\n// These are stubs — the virtual module and transform hook provide real values.\nexport const Inter = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Roboto = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Open_Sans = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Lato = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Montserrat = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Poppins = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Geist = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Geist_Mono = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const JetBrains_Mono = (config?: GoogleFontConfig): FontResult =>\n createStubFontResult(config);\nexport const Source_Code_Pro = (config?: GoogleFontConfig): FontResult =>\n createStubFontResult(config);\nexport const Playfair_Display = (config?: GoogleFontConfig): FontResult =>\n createStubFontResult(config);\nexport const Merriweather = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\n"],"mappings":";;;;;;;;;AA+BA,SAAS,qBAAqB,QAAuC;
|
|
1
|
+
{"version":3,"file":"font-google.js","names":[],"sources":["../../src/shims/font-google.ts"],"sourcesContent":["/**\n * Fallback shim for `next/font/google` → `@timber/fonts/google`.\n *\n * At build time, the timber-fonts plugin's transform hook replaces font\n * function calls with static FontResult objects containing real class names\n * and font stacks. This file serves two purposes:\n *\n * 1. **TypeScript resolution** — provides types for IDEs and `tsc` outside\n * of Vite's module graph (where the virtual module handles it).\n * 2. **Runtime fallback** — returns empty className/fontFamily values when\n * the plugin hasn't processed a call (e.g. in tests or non-Vite environments).\n *\n * The shim resolution path (`next/font/google` → `\\0@timber/fonts/google`)\n * is unchanged — this file is NOT on that resolution path. It exists as a\n * physical module for direct imports and type re-exports.\n *\n * Design doc: 24-fonts.md, \"Next.js Font Compatibility\"\n */\n\nimport type { GoogleFontConfig, FontResult } from '../fonts/types.js';\n\nexport type { GoogleFontConfig, FontResult };\n\n/**\n * Create a stub FontResult with empty values.\n *\n * The timber-fonts plugin replaces these calls at build time with real\n * class names and font stacks. This stub ensures code that imports\n * font functions outside of the build pipeline (tests, type-checking)\n * gets a valid FontResult shape without runtime errors.\n */\nfunction createStubFontResult(config?: GoogleFontConfig): FontResult {\n return {\n className: '',\n style: { fontFamily: '' },\n variable: config?.variable,\n };\n}\n\n/**\n * Generic font loader — accepts any font name, returns a stub FontResult.\n *\n * Named exports like `Inter`, `Roboto`, etc. are generated dynamically\n * by the virtual module at build time. This function provides a catch-all\n * for non-Vite contexts.\n */\nexport function createFont(_family: string, config?: GoogleFontConfig): FontResult {\n return createStubFontResult(config);\n}\n\n// Common Google Font exports for TypeScript autocomplete.\n// These are stubs — the virtual module and transform hook provide real values.\nexport const Inter = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Roboto = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Open_Sans = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Lato = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Montserrat = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Poppins = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Geist = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const Geist_Mono = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\nexport const JetBrains_Mono = (config?: GoogleFontConfig): FontResult =>\n createStubFontResult(config);\nexport const Source_Code_Pro = (config?: GoogleFontConfig): FontResult =>\n createStubFontResult(config);\nexport const Playfair_Display = (config?: GoogleFontConfig): FontResult =>\n createStubFontResult(config);\nexport const Merriweather = (config?: GoogleFontConfig): FontResult => createStubFontResult(config);\n"],"mappings":";;;;;;;;;AA+BA,SAAS,qBAAqB,QAAuC;CACnE,OAAO;EACL,WAAW;EACX,OAAO,EAAE,YAAY,IAAI;EACzB,UAAU,QAAQ;EACnB;;;;;;;;;AAUH,SAAgB,WAAW,SAAiB,QAAuC;CACjF,OAAO,qBAAqB,OAAO;;AAKrC,IAAa,SAAS,WAA0C,qBAAqB,OAAO;AAC5F,IAAa,UAAU,WAA0C,qBAAqB,OAAO;AAC7F,IAAa,aAAa,WAA0C,qBAAqB,OAAO;AAChG,IAAa,QAAQ,WAA0C,qBAAqB,OAAO;AAC3F,IAAa,cAAc,WAA0C,qBAAqB,OAAO;AACjG,IAAa,WAAW,WAA0C,qBAAqB,OAAO;AAC9F,IAAa,SAAS,WAA0C,qBAAqB,OAAO;AAC5F,IAAa,cAAc,WAA0C,qBAAqB,OAAO;AACjG,IAAa,kBAAkB,WAC7B,qBAAqB,OAAO;AAC9B,IAAa,mBAAmB,WAC9B,qBAAqB,OAAO;AAC9B,IAAa,oBAAoB,WAC/B,qBAAqB,OAAO;AAC9B,IAAa,gBAAgB,WAA0C,qBAAqB,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"font-local.js","names":[],"sources":["../../src/shims/font-local.ts"],"sourcesContent":["/**\n * Fallback shim for `next/font/local` → `@timber/fonts/local`.\n *\n * At build time, the timber-fonts plugin's transform hook replaces\n * `localFont()` calls with static FontResult objects containing real\n * class names and font stacks. This file serves two purposes:\n *\n * 1. **TypeScript resolution** — provides types for IDEs and `tsc` outside\n * of Vite's module graph (where the virtual module handles it).\n * 2. **Runtime fallback** — returns empty className/fontFamily values when\n * the plugin hasn't processed a call (e.g. in tests or non-Vite environments).\n *\n * Design doc: 24-fonts.md, \"Next.js Font Compatibility\"\n */\n\nimport type { LocalFontConfig, FontResult } from '../fonts/types.js';\n\nexport type { LocalFontConfig, FontResult };\n\n/**\n * Create a local font.\n *\n * The timber-fonts plugin replaces these calls at build time with real\n * class names and font stacks. This stub ensures code that imports\n * `localFont` outside of the build pipeline (tests, type-checking)\n * gets a valid FontResult shape without runtime errors.\n */\nexport default function localFont(config?: LocalFontConfig): FontResult {\n return {\n className: '',\n style: { fontFamily: '' },\n variable: config?.variable,\n };\n}\n"],"mappings":";;;;;;;;;AA2BA,SAAwB,UAAU,QAAsC;
|
|
1
|
+
{"version":3,"file":"font-local.js","names":[],"sources":["../../src/shims/font-local.ts"],"sourcesContent":["/**\n * Fallback shim for `next/font/local` → `@timber/fonts/local`.\n *\n * At build time, the timber-fonts plugin's transform hook replaces\n * `localFont()` calls with static FontResult objects containing real\n * class names and font stacks. This file serves two purposes:\n *\n * 1. **TypeScript resolution** — provides types for IDEs and `tsc` outside\n * of Vite's module graph (where the virtual module handles it).\n * 2. **Runtime fallback** — returns empty className/fontFamily values when\n * the plugin hasn't processed a call (e.g. in tests or non-Vite environments).\n *\n * Design doc: 24-fonts.md, \"Next.js Font Compatibility\"\n */\n\nimport type { LocalFontConfig, FontResult } from '../fonts/types.js';\n\nexport type { LocalFontConfig, FontResult };\n\n/**\n * Create a local font.\n *\n * The timber-fonts plugin replaces these calls at build time with real\n * class names and font stacks. This stub ensures code that imports\n * `localFont` outside of the build pipeline (tests, type-checking)\n * gets a valid FontResult shape without runtime errors.\n */\nexport default function localFont(config?: LocalFontConfig): FontResult {\n return {\n className: '',\n style: { fontFamily: '' },\n variable: config?.variable,\n };\n}\n"],"mappings":";;;;;;;;;AA2BA,SAAwB,UAAU,QAAsC;CACtE,OAAO;EACL,WAAW;EACX,OAAO,EAAE,YAAY,IAAI;EACzB,UAAU,QAAQ;EACnB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.134",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -114,17 +114,12 @@
|
|
|
114
114
|
"publishConfig": {
|
|
115
115
|
"access": "public"
|
|
116
116
|
},
|
|
117
|
-
"scripts": {
|
|
118
|
-
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
119
|
-
"typecheck": "tsgo --noEmit",
|
|
120
|
-
"prepublishOnly": "pnpm run build"
|
|
121
|
-
},
|
|
122
117
|
"dependencies": {
|
|
123
118
|
"@opentelemetry/api": "^1.9.1",
|
|
124
|
-
"@opentelemetry/context-async-hooks": "^2.
|
|
125
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
119
|
+
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
120
|
+
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
126
121
|
"magic-string": "^0.30.21",
|
|
127
|
-
"nitro": "3.0.
|
|
122
|
+
"nitro": "3.0.260429-beta"
|
|
128
123
|
},
|
|
129
124
|
"peerDependencies": {
|
|
130
125
|
"@content-collections/core": "^0.14.0",
|
|
@@ -136,7 +131,7 @@
|
|
|
136
131
|
"nuqs": "^2.0.0",
|
|
137
132
|
"react": "19.3.0-canary-d5736f09-20260507",
|
|
138
133
|
"react-dom": "19.3.0-canary-d5736f09-20260507",
|
|
139
|
-
"vite": "^8.0.
|
|
134
|
+
"vite": "^8.0.8",
|
|
140
135
|
"wrangler": "^4.0.0",
|
|
141
136
|
"zod": "^3.22.0 || ^4.0.0"
|
|
142
137
|
},
|
|
@@ -162,5 +157,9 @@
|
|
|
162
157
|
},
|
|
163
158
|
"engines": {
|
|
164
159
|
"node": ">=22.12.0"
|
|
160
|
+
},
|
|
161
|
+
"scripts": {
|
|
162
|
+
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
163
|
+
"typecheck": "tsgo --noEmit"
|
|
165
164
|
}
|
|
166
|
-
}
|
|
165
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/client/link.tsx
CHANGED
|
@@ -28,6 +28,10 @@ import type { SearchParamsDefinition } from '../search-params/define.js';
|
|
|
28
28
|
import { getSearchParamsDefinition } from '../search-params/registry.js';
|
|
29
29
|
import type { LinkFunction } from './index.js';
|
|
30
30
|
import { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.js';
|
|
31
|
+
import {
|
|
32
|
+
validateNavigationHref as validateLinkHref,
|
|
33
|
+
isInternalHref,
|
|
34
|
+
} from '../shared/href-validation.js';
|
|
31
35
|
import { LinkStatusContext } from './use-link-status.js';
|
|
32
36
|
import { getRouterOrNull } from './router-ref.js';
|
|
33
37
|
import { getSsrData } from './ssr-data.js';
|
|
@@ -168,38 +172,7 @@ export type LinkPropsWithParams = LinkRuntimeProps & {
|
|
|
168
172
|
};
|
|
169
173
|
export type LinkProps = LinkRuntimeProps;
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Reject dangerous URL schemes that could execute script.
|
|
175
|
-
* Security: design/13-security.md § Link scheme injection (test #9)
|
|
176
|
-
*/
|
|
177
|
-
const DANGEROUS_SCHEMES = /^\s*(javascript|data|vbscript):/i;
|
|
178
|
-
|
|
179
|
-
export function validateLinkHref(href: string): void {
|
|
180
|
-
if (DANGEROUS_SCHEMES.test(href)) {
|
|
181
|
-
throw new Error(
|
|
182
|
-
`<Link> received a dangerous href: "${href}". ` +
|
|
183
|
-
'javascript:, data:, and vbscript: URLs are not allowed.'
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ─── Internal Link Detection ─────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
/** Returns true if the href is an internal path (not an external URL) */
|
|
191
|
-
export function isInternalHref(href: string): boolean {
|
|
192
|
-
// Relative paths, root-relative paths, and hash links are internal
|
|
193
|
-
if (href.startsWith('/') || href.startsWith('#') || href.startsWith('?')) {
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
// Anything with a protocol scheme is external
|
|
197
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(href)) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
// Bare relative paths (e.g., "dashboard") are internal
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
175
|
+
export { validateLinkHref, isInternalHref };
|
|
203
176
|
|
|
204
177
|
// ─── URL Interpolation ──────────────────────────────────────────
|
|
205
178
|
|
package/src/client/use-router.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { getRouterOrNull } from './router-ref.js';
|
|
27
|
+
import { validateNavigationHref } from '../shared/href-validation.js';
|
|
27
28
|
|
|
28
29
|
export interface AppRouterInstance {
|
|
29
30
|
/** Navigate to a URL, pushing a new history entry */
|
|
@@ -60,6 +61,7 @@ export interface AppRouterInstance {
|
|
|
60
61
|
export function useRouter(): AppRouterInstance {
|
|
61
62
|
return {
|
|
62
63
|
push(href: string, options?: { scroll?: boolean }) {
|
|
64
|
+
validateNavigationHref(href);
|
|
63
65
|
const router = getRouterOrNull();
|
|
64
66
|
if (!router) {
|
|
65
67
|
if (process.env.NODE_ENV === 'development') {
|
|
@@ -72,6 +74,7 @@ export function useRouter(): AppRouterInstance {
|
|
|
72
74
|
void router.navigate(href, { scroll: options?.scroll });
|
|
73
75
|
},
|
|
74
76
|
replace(href: string, options?: { scroll?: boolean }) {
|
|
77
|
+
validateNavigationHref(href);
|
|
75
78
|
const router = getRouterOrNull();
|
|
76
79
|
if (!router) {
|
|
77
80
|
if (process.env.NODE_ENV === 'development') {
|
|
@@ -228,7 +228,6 @@ export async function renderDenyPageAsRsc(
|
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
responseHeaders.set('content-type', `${RSC_CONTENT_TYPE}; charset=utf-8`);
|
|
231
|
-
responseHeaders.set('Vary', 'Accept');
|
|
232
231
|
return new Response(rscStream, {
|
|
233
232
|
status: deny.status,
|
|
234
233
|
headers: responseHeaders,
|
|
@@ -24,6 +24,7 @@ export interface InterceptionMatchResult {
|
|
|
24
24
|
* followed by '/' or be an exact match.
|
|
25
25
|
*/
|
|
26
26
|
function hasSegmentPrefix(pathname: string, prefix: string): boolean {
|
|
27
|
+
if (prefix === '/') return true;
|
|
27
28
|
if (!pathname.startsWith(prefix)) return false;
|
|
28
29
|
return pathname.length === prefix.length || pathname[prefix.length] === '/';
|
|
29
30
|
}
|
|
@@ -82,7 +82,7 @@ export async function runProxyPhase(
|
|
|
82
82
|
try {
|
|
83
83
|
const proxyExport = await getProxy();
|
|
84
84
|
const proxyFn = () =>
|
|
85
|
-
runProxy(proxyExport, req, () => handleRequest(config, req, method, path));
|
|
85
|
+
runProxy(proxyExport, req, () => handleRequest(config, req, method, path, true));
|
|
86
86
|
const response = await withSpan('timber.proxy', {}, () =>
|
|
87
87
|
detailed ? withTiming('proxy', 'proxy.ts', proxyFn) : proxyFn()
|
|
88
88
|
);
|
|
@@ -214,33 +214,44 @@ export async function runRenderPhase(
|
|
|
214
214
|
*
|
|
215
215
|
* Used both as the top-level entry (when no proxy.ts is configured) and as
|
|
216
216
|
* the `next()` continuation passed to `runProxy()`.
|
|
217
|
+
*
|
|
218
|
+
* @param pathIsCanonical When true, `path` has already been canonicalized by
|
|
219
|
+
* `createPipeline` — skip re-canonicalization to prevent double-decode.
|
|
220
|
+
* When false (default), runs canonicalize as a safety net for direct callers.
|
|
217
221
|
*/
|
|
218
222
|
export async function handleRequest(
|
|
219
223
|
config: PipelineConfig,
|
|
220
224
|
req: Request,
|
|
221
225
|
method: string,
|
|
222
|
-
path: string
|
|
226
|
+
path: string,
|
|
227
|
+
pathIsCanonical?: boolean
|
|
223
228
|
): Promise<Response> {
|
|
224
229
|
const stripTrailingSlash = config.stripTrailingSlash ?? true;
|
|
225
230
|
|
|
226
|
-
// Stage 1: URL canonicalization
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)
|
|
231
|
+
// Stage 1: URL canonicalization.
|
|
232
|
+
// When pathIsCanonical is true, createPipeline has already run canonicalize()
|
|
233
|
+
// and passed the result as `path`. Re-canonicalizing would double-decode
|
|
234
|
+
// percent-encoded characters (e.g., %61dmin → admin). See TIM-1004.
|
|
235
|
+
//
|
|
236
|
+
// When pathIsCanonical is false, this is a safety net for direct callers
|
|
237
|
+
// (tests, non-pipeline usage) that haven't pre-canonicalized.
|
|
238
|
+
let canonicalPathname: string;
|
|
239
|
+
if (pathIsCanonical) {
|
|
240
|
+
canonicalPathname = path;
|
|
241
|
+
} else {
|
|
242
|
+
const result = canonicalize(path, stripTrailingSlash);
|
|
243
|
+
if (!result.ok) {
|
|
244
|
+
if (isDebug()) {
|
|
245
|
+
console.warn(
|
|
246
|
+
`[timber] URL canonicalization rejected ${method} ${path} — responding with ${result.status}\n` +
|
|
247
|
+
` This usually means the URL contains encoded separators (%2f, %5c),\n` +
|
|
248
|
+
` null bytes (%00), path traversal (..), or malformed percent-encoding.`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return new Response(null, { status: result.status });
|
|
240
252
|
}
|
|
241
|
-
|
|
253
|
+
canonicalPathname = result.pathname;
|
|
242
254
|
}
|
|
243
|
-
const canonicalPathname = result.pathname;
|
|
244
255
|
|
|
245
256
|
// Stage 1b: Metadata route matching — runs before regular route matching.
|
|
246
257
|
// Metadata routes skip middleware.ts and access.ts (public endpoints for crawlers).
|
|
@@ -377,7 +388,7 @@ export async function handleRequest(
|
|
|
377
388
|
if (isDebug()) {
|
|
378
389
|
console.warn(
|
|
379
390
|
`[timber] No route matched for pathname: ${canonicalPathname}\n` +
|
|
380
|
-
`
|
|
391
|
+
` Input path: ${path}\n` +
|
|
381
392
|
` Method: ${method}`
|
|
382
393
|
);
|
|
383
394
|
}
|
package/src/server/pipeline.ts
CHANGED
|
@@ -327,13 +327,30 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
|
|
|
327
327
|
path: canonicalPath,
|
|
328
328
|
});
|
|
329
329
|
} else {
|
|
330
|
-
result = await handleRequest(config, canonicalReq, method, canonicalPath);
|
|
330
|
+
result = await handleRequest(config, canonicalReq, method, canonicalPath, true);
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
// Set response status on the root span before it ends —
|
|
334
334
|
// DevSpanProcessor reads this for tree/summary output.
|
|
335
335
|
await setSpanAttribute('http.response.status_code', result.status);
|
|
336
336
|
|
|
337
|
+
// Vary: Accept — CDNs must cache HTML and RSC payload responses
|
|
338
|
+
// separately for the same URL. Without this, a shared cache may
|
|
339
|
+
// serve an RSC Flight payload to a browser expecting HTML (or
|
|
340
|
+
// vice versa). See GHSA-wfc6-r584-vfw7, design/13-security.md.
|
|
341
|
+
const existingVary = result.headers.get('Vary');
|
|
342
|
+
if (!existingVary) {
|
|
343
|
+
result.headers.set('Vary', 'Accept');
|
|
344
|
+
} else {
|
|
345
|
+
const tokens = existingVary
|
|
346
|
+
.toLowerCase()
|
|
347
|
+
.split(',')
|
|
348
|
+
.map((t) => t.trim());
|
|
349
|
+
if (!tokens.includes('accept')) {
|
|
350
|
+
result.headers.set('Vary', `${existingVary}, Accept`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
337
354
|
// Append Server-Timing header based on configured mode.
|
|
338
355
|
// Header mutability is guaranteed by the producer-side clone
|
|
339
356
|
// in `outcomeToResponse` and the metadata-route / auto-sitemap
|
package/src/server/primitives.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { getWaitUntil as _getWaitUntil } from './waituntil-bridge.js';
|
|
|
8
8
|
import { isDebug } from './debug.js';
|
|
9
9
|
import { getRequestSearchString } from './request-context.js';
|
|
10
10
|
import { mergePreservedSearchParams } from '../shared/merge-search-params.js';
|
|
11
|
+
import { validateExternalRedirectUrl } from '../shared/href-validation.js';
|
|
11
12
|
|
|
12
13
|
// ─── Dev-mode validation ────────────────────────────────────────────────────
|
|
13
14
|
|
|
@@ -325,10 +326,13 @@ export function permanentRedirect(path: string, options?: Omit<RedirectOptions,
|
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
/**
|
|
328
|
-
* Redirect to an external URL. The
|
|
329
|
+
* Redirect to an external URL. The origin must be in the provided allow-list.
|
|
330
|
+
*
|
|
331
|
+
* Only http: and https: schemes are permitted. The allow-list is matched
|
|
332
|
+
* against full origins (scheme + host + port), not bare hostnames.
|
|
329
333
|
*
|
|
330
334
|
* @param url - Absolute URL to redirect to.
|
|
331
|
-
* @param allowList - Array of allowed
|
|
335
|
+
* @param allowList - Array of allowed origins (e.g. ['https://example.com', 'https://auth.example.com']).
|
|
332
336
|
* @param status - HTTP redirect status code (3xx). Defaults to 302.
|
|
333
337
|
*/
|
|
334
338
|
export function redirectExternal(url: string, allowList: string[], status: number = 302): never {
|
|
@@ -336,21 +340,8 @@ export function redirectExternal(url: string, allowList: string[], status: numbe
|
|
|
336
340
|
throw new Error(`redirectExternal() requires a 3xx status code, got ${status}.`);
|
|
337
341
|
}
|
|
338
342
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
hostname = new URL(url).hostname;
|
|
342
|
-
} catch {
|
|
343
|
-
throw new Error(`redirectExternal() received an invalid URL: "${url}"`);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (!allowList.includes(hostname)) {
|
|
347
|
-
throw new Error(
|
|
348
|
-
`redirectExternal() target "${hostname}" is not in the allow-list. ` +
|
|
349
|
-
`Allowed: [${allowList.join(', ')}]`
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
throw new RedirectSignal(url, status);
|
|
343
|
+
const canonicalUrl = validateExternalRedirectUrl(url, allowList);
|
|
344
|
+
throw new RedirectSignal(canonicalUrl, status);
|
|
354
345
|
}
|
|
355
346
|
|
|
356
347
|
// ─── RenderError ────────────────────────────────────────────────────────────
|
|
@@ -172,10 +172,6 @@ export async function buildRscPayloadResponse(
|
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
responseHeaders.set('content-type', `${RSC_CONTENT_TYPE}; charset=utf-8`);
|
|
175
|
-
// Vary on Accept so CDNs cache HTML and RSC responses separately
|
|
176
|
-
// for the same URL. The client appends ?_rsc=<id> as a cache-bust,
|
|
177
|
-
// but Vary ensures correct behavior even without the query param.
|
|
178
|
-
responseHeaders.set('Vary', 'Accept');
|
|
179
175
|
|
|
180
176
|
// Send resolved head elements so the client can update document.title
|
|
181
177
|
// and <meta> tags after SPA navigation. See design/16-metadata.md.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared href/URL validation for all navigation sinks.
|
|
3
|
+
*
|
|
4
|
+
* Used by <Link>, useRouter().push/replace, and redirectExternal().
|
|
5
|
+
* Single source of truth for dangerous-scheme detection — prevents
|
|
6
|
+
* control-character smuggling that bypasses naive regex checks.
|
|
7
|
+
*
|
|
8
|
+
* See design/13-security.md §"Link scheme injection" (test #9)
|
|
9
|
+
* and design/07-routing.md §"<Link> Scheme Validation".
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ASCII control characters that browsers strip during URL preprocessing.
|
|
14
|
+
* A naive prefix regex like /^javascript:/i can be bypassed with
|
|
15
|
+
* "java\tscript:alert(1)" because the browser removes the tab before
|
|
16
|
+
* interpreting the scheme. We strip these before checking the scheme.
|
|
17
|
+
*/
|
|
18
|
+
const CONTROL_CHARS_RE = /[\t\n\r]/g;
|
|
19
|
+
|
|
20
|
+
const DANGEROUS_SCHEMES = new Set(['javascript:', 'data:', 'vbscript:']);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Strip C0 control chars and leading whitespace, then extract the scheme.
|
|
24
|
+
* Returns the lowercase scheme (e.g. "javascript:") or null if no scheme.
|
|
25
|
+
*/
|
|
26
|
+
function extractScheme(href: string): string | null {
|
|
27
|
+
const cleaned = href.replace(CONTROL_CHARS_RE, '').trimStart();
|
|
28
|
+
const match = /^([a-z][a-z0-9+.-]*:)/i.exec(cleaned);
|
|
29
|
+
return match ? match[1].toLowerCase() : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate an href for navigation sinks (<Link>, useRouter, etc.).
|
|
34
|
+
*
|
|
35
|
+
* Rejects dangerous schemes (javascript:, data:, vbscript:) even when
|
|
36
|
+
* control characters are embedded to bypass naive regex checks.
|
|
37
|
+
* Also rejects protocol-relative URLs (//evil.com).
|
|
38
|
+
*
|
|
39
|
+
* Throws on rejection. Safe hrefs pass through silently.
|
|
40
|
+
*/
|
|
41
|
+
export function validateNavigationHref(href: string): void {
|
|
42
|
+
const cleaned = href.replace(CONTROL_CHARS_RE, '').trimStart();
|
|
43
|
+
|
|
44
|
+
// Protocol-relative URLs can redirect to attacker-controlled origins
|
|
45
|
+
if (cleaned.startsWith('//')) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Navigation received a protocol-relative URL: "${href}". ` +
|
|
48
|
+
'Use an absolute URL with an explicit scheme or a relative path.'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const scheme = extractScheme(href);
|
|
53
|
+
if (scheme && DANGEROUS_SCHEMES.has(scheme)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Navigation received a dangerous href: "${href}". ` +
|
|
56
|
+
'javascript:, data:, and vbscript: URLs are not allowed.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if an href is internal (same-origin navigation) or external.
|
|
63
|
+
*
|
|
64
|
+
* Strips control characters before checking so that "java\tscript:" is
|
|
65
|
+
* correctly identified as having a scheme (and thus not internal).
|
|
66
|
+
*/
|
|
67
|
+
export function isInternalHref(href: string): boolean {
|
|
68
|
+
const cleaned = href.replace(CONTROL_CHARS_RE, '').trimStart();
|
|
69
|
+
|
|
70
|
+
if (cleaned.startsWith('//')) return false;
|
|
71
|
+
if (cleaned.startsWith('/') || cleaned.startsWith('#') || cleaned.startsWith('?')) return true;
|
|
72
|
+
|
|
73
|
+
const scheme = extractScheme(href);
|
|
74
|
+
if (scheme) return false;
|
|
75
|
+
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const SAFE_REDIRECT_SCHEMES = new Set(['http:', 'https:']);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate and canonicalize an external redirect URL.
|
|
83
|
+
*
|
|
84
|
+
* Requires http: or https: scheme and checks origin against an allow-list.
|
|
85
|
+
* Returns the parsed canonical URL string (from URL.href) to prevent
|
|
86
|
+
* raw user input from reaching the Location header.
|
|
87
|
+
*
|
|
88
|
+
* @throws On invalid URL, dangerous scheme, or origin not in allow-list.
|
|
89
|
+
*/
|
|
90
|
+
export function validateExternalRedirectUrl(url: string, allowList: string[]): string {
|
|
91
|
+
// Strip control chars before parsing to prevent scheme smuggling
|
|
92
|
+
const cleaned = url.replace(CONTROL_CHARS_RE, '').trimStart();
|
|
93
|
+
|
|
94
|
+
if (cleaned.startsWith('//')) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`redirectExternal() received a protocol-relative URL: "${url}". ` +
|
|
97
|
+
'Use an absolute URL with http: or https: scheme.'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let parsed: URL;
|
|
102
|
+
try {
|
|
103
|
+
parsed = new URL(cleaned);
|
|
104
|
+
} catch {
|
|
105
|
+
throw new Error(`redirectExternal() received an invalid URL: "${url}"`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!SAFE_REDIRECT_SCHEMES.has(parsed.protocol)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`redirectExternal() received a non-http(s) URL: "${url}". ` +
|
|
111
|
+
'Only http: and https: schemes are allowed for external redirects.'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!allowList.includes(parsed.origin)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`redirectExternal() target "${parsed.origin}" is not in the allow-list. ` +
|
|
118
|
+
`Allowed: [${allowList.join(', ')}]`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return parsed.href;
|
|
123
|
+
}
|