@timber-js/app 0.2.0-alpha.87 → 0.2.0-alpha.89
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/client/index.d.ts +44 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +7 -44
- package/dist/client/link.d.ts.map +1 -1
- package/dist/config-types.d.ts +39 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/fonts/bundle.d.ts +48 -0
- package/dist/fonts/bundle.d.ts.map +1 -0
- package/dist/fonts/dev-middleware.d.ts +22 -0
- package/dist/fonts/dev-middleware.d.ts.map +1 -0
- package/dist/fonts/pipeline.d.ts +138 -0
- package/dist/fonts/pipeline.d.ts.map +1 -0
- package/dist/fonts/transform.d.ts +72 -0
- package/dist/fonts/transform.d.ts.map +1 -0
- package/dist/fonts/types.d.ts +45 -1
- package/dist/fonts/types.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts +59 -0
- package/dist/fonts/virtual-modules.d.ts.map +1 -0
- package/dist/index.js +753 -575
- package/dist/index.js.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +16 -83
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/server/action-client.d.ts +8 -0
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +7 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/index.js +158 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/route-matcher.d.ts +7 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/sensitive-fields.d.ts +74 -0
- package/dist/server/sensitive-fields.d.ts.map +1 -0
- package/package.json +6 -7
- package/src/cli.ts +0 -0
- package/src/client/index.ts +77 -1
- package/src/client/link.tsx +15 -65
- package/src/config-types.ts +39 -0
- package/src/config-validation.ts +7 -3
- package/src/fonts/bundle.ts +142 -0
- package/src/fonts/dev-middleware.ts +74 -0
- package/src/fonts/pipeline.ts +275 -0
- package/src/fonts/transform.ts +353 -0
- package/src/fonts/types.ts +50 -1
- package/src/fonts/virtual-modules.ts +159 -0
- package/src/plugins/entries.ts +37 -0
- package/src/plugins/fonts.ts +102 -704
- package/src/plugins/routing.ts +6 -5
- package/src/server/action-client.ts +34 -4
- package/src/server/action-handler.ts +32 -2
- package/src/server/route-matcher.ts +7 -0
- package/src/server/rsc-entry/index.ts +19 -3
- package/src/server/sensitive-fields.ts +230 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/server/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAM9B,6DAA6D;AAC7D,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EACP,QAAQ,GACR,SAAS,GACT,WAAW,GACX,oBAAoB,GACpB,OAAO,GACP,MAAM,GACN,cAAc,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC3D,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAE9C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC5C;AAED,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;CAC5B;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAEzC;AAyMD,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,IAAI,EAAE,iBAAiB,CAAC;IACxB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,CAAC;IACnB,0DAA0D;IAC1D,OAAO,EAAE,mBAAmB,CAAC;IAC7B;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,kBAAkB,GAAG,IAAI,CAMjD"}
|
|
1
|
+
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/server/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAM9B,6DAA6D;AAC7D,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EACP,QAAQ,GACR,SAAS,GACT,WAAW,GACX,oBAAoB,GACpB,OAAO,GACP,MAAM,GACN,cAAc,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC3D,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAE9C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC5C;AAED,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,mBAAmB,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;CAC5B;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAEzC;AAyMD,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,IAAI,EAAE,iBAAiB,CAAC;IACxB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,CAAC;IACnB,0DAA0D;IAC1D,OAAO,EAAE,mBAAmB,CAAC;IAC7B;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,kBAAkB,GAAG,IAAI,CAMjD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA4DA,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAoCtB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB,EAAE,KAAK,IAAI,GACtF,IAAI,CAEN;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAE5E;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA4DA,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAoCtB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB,EAAE,KAAK,IAAI,GACtF,IAAI,CAEN;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAE5E;AAqjBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAInE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;8BA5SrC,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA8ShD,wBAAiE"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sensitive field stripping — removes password/token/CVV-style fields
|
|
3
|
+
* from form values before they are echoed back to the client as
|
|
4
|
+
* `submittedValues` for form repopulation.
|
|
5
|
+
*
|
|
6
|
+
* Applied to both action paths:
|
|
7
|
+
* - With-JS action path: `createActionClient()` in `action-client.ts`
|
|
8
|
+
* - No-JS form POST path: `handleFormAction()` in `action-handler.ts`
|
|
9
|
+
*
|
|
10
|
+
* Why: on a validation failure, timber echoes submitted form values back so
|
|
11
|
+
* the user doesn't have to re-type everything. Without filtering, plaintext
|
|
12
|
+
* passwords / credit-card numbers / TOTP codes would travel through the RSC
|
|
13
|
+
* stream (with-JS) or land in the HTML as `defaultValue` attributes (no-JS)
|
|
14
|
+
* — ending up in browser history, proxy logs, disk caches, and the
|
|
15
|
+
* back-forward cache.
|
|
16
|
+
*
|
|
17
|
+
* Safe by default: the built-in deny-list is applied unconditionally unless
|
|
18
|
+
* the user explicitly opts out via `forms.stripSensitiveFields: false` in
|
|
19
|
+
* `timber.config.ts` or per-action via `createActionClient({ stripSensitiveFields: false })`.
|
|
20
|
+
*
|
|
21
|
+
* See design/08-forms-and-actions.md §"Validation errors"
|
|
22
|
+
* See design/13-security.md §"Sensitive field stripping"
|
|
23
|
+
* See TIM-816
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* How to strip sensitive fields from `submittedValues`.
|
|
27
|
+
*
|
|
28
|
+
* - `true` / `undefined` — use the built-in deny-list (default, safe).
|
|
29
|
+
* - `false` — do not strip anything (dev convenience; never do this in prod).
|
|
30
|
+
* - `string[]` — additional field names to strip, merged with the built-in list.
|
|
31
|
+
* - `(name) => boolean` — custom predicate, fully replaces the built-in list.
|
|
32
|
+
* Return `true` to strip, `false` to keep. The `name` argument is the raw
|
|
33
|
+
* (un-normalized) field name as it appeared in the submitted form.
|
|
34
|
+
*/
|
|
35
|
+
export type SensitiveFieldsOption = boolean | readonly string[] | ((name: string) => boolean);
|
|
36
|
+
/**
|
|
37
|
+
* A resolved predicate: `null` means "don't strip anything" (the option was
|
|
38
|
+
* explicitly `false`). Otherwise a function from raw field name → boolean.
|
|
39
|
+
*/
|
|
40
|
+
export type ResolvedSensitivePredicate = ((name: string) => boolean) | null;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a `SensitiveFieldsOption` into a concrete predicate.
|
|
43
|
+
* Precedence: per-action > global > built-in default.
|
|
44
|
+
*
|
|
45
|
+
* - Per-action `undefined` → fall back to global.
|
|
46
|
+
* - Global `undefined` → use built-in list.
|
|
47
|
+
* - Either level set to `false` → disable stripping entirely (returns `null`).
|
|
48
|
+
* - `true` → built-in list.
|
|
49
|
+
* - `string[]` → built-in ∪ extras.
|
|
50
|
+
* - function → custom, replaces the built-in list entirely.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveSensitivePredicate(perAction: SensitiveFieldsOption | undefined, global: SensitiveFieldsOption | undefined): ResolvedSensitivePredicate;
|
|
53
|
+
/**
|
|
54
|
+
* Set the global `forms.stripSensitiveFields` config from `timber.config.ts`.
|
|
55
|
+
* Called once at startup from `rsc-entry`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function setGlobalSensitiveFieldsConfig(option: SensitiveFieldsOption | undefined): void;
|
|
58
|
+
/** Read the global `forms.stripSensitiveFields` config. */
|
|
59
|
+
export declare function getGlobalSensitiveFieldsConfig(): SensitiveFieldsOption | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Walk an object (recursively) and return a copy with every key matching
|
|
62
|
+
* `predicate` removed. Nested objects like `{ user: { password: '...' } }`
|
|
63
|
+
* are handled — `user.password` is stripped while other `user.*` fields remain.
|
|
64
|
+
*
|
|
65
|
+
* - Arrays are walked element-wise (object entries inside arrays are cleaned).
|
|
66
|
+
* - Non-plain values (strings, numbers, Files, Dates, etc.) are returned as-is.
|
|
67
|
+
* - When a stripped key is encountered, it is omitted from the result entirely
|
|
68
|
+
* — we do NOT set it to an empty string, because that would overwrite a
|
|
69
|
+
* valid `defaultValue` the form author might have set.
|
|
70
|
+
*/
|
|
71
|
+
export declare function stripSensitiveFields<T>(value: T, predicate: ResolvedSensitivePredicate): T;
|
|
72
|
+
/** Reset the "warned once" cache. Exposed for tests. */
|
|
73
|
+
export declare function __resetSensitiveFieldsWarnings(): void;
|
|
74
|
+
//# sourceMappingURL=sensitive-fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensitive-fields.d.ts","sourceRoot":"","sources":["../../src/server/sensitive-fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH;;;;;;;;;GASG;AACH,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,SAAS,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;AAoF9F;;;GAGG;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;AAE5E;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,qBAAqB,GAAG,SAAS,EAC5C,MAAM,EAAE,qBAAqB,GAAG,SAAS,GACxC,0BAA0B,CAa5B;AAMD;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,IAAI,CAE9F;AAED,2DAA2D;AAC3D,wBAAgB,8BAA8B,IAAI,qBAAqB,GAAG,SAAS,CAElF;AAkBD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,0BAA0B,GAAG,CAAC,CAoB1F;AAID,wDAAwD;AACxD,wBAAgB,8BAA8B,IAAI,IAAI,CAErD"}
|
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.89",
|
|
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",
|
|
@@ -110,11 +110,6 @@
|
|
|
110
110
|
"publishConfig": {
|
|
111
111
|
"access": "public"
|
|
112
112
|
},
|
|
113
|
-
"scripts": {
|
|
114
|
-
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
115
|
-
"typecheck": "tsgo --noEmit",
|
|
116
|
-
"prepublishOnly": "pnpm run build"
|
|
117
|
-
},
|
|
118
113
|
"dependencies": {
|
|
119
114
|
"@opentelemetry/api": "^1.9.1",
|
|
120
115
|
"@opentelemetry/context-async-hooks": "^2.6.1",
|
|
@@ -157,5 +152,9 @@
|
|
|
157
152
|
},
|
|
158
153
|
"engines": {
|
|
159
154
|
"node": ">=22.12.0"
|
|
155
|
+
},
|
|
156
|
+
"scripts": {
|
|
157
|
+
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
158
|
+
"typecheck": "tsgo --noEmit"
|
|
160
159
|
}
|
|
161
|
-
}
|
|
160
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/client/index.ts
CHANGED
|
@@ -10,10 +10,86 @@
|
|
|
10
10
|
export type { RenderErrorDigest } from './types';
|
|
11
11
|
|
|
12
12
|
// Navigation
|
|
13
|
+
import type { JSX } from 'react';
|
|
14
|
+
import type { SearchParamsDefinition } from '../search-params/define.js';
|
|
15
|
+
import type { LinkBaseProps } from './link';
|
|
13
16
|
export { Link, interpolateParams, resolveHref, validateLinkHref, buildLinkProps } from './link';
|
|
14
17
|
export { mergePreservedSearchParams } from '../shared/merge-search-params.js';
|
|
15
|
-
export type {
|
|
18
|
+
export type { LinkProps, LinkPropsWithHref, LinkPropsWithParams, LinkBaseProps } from './link';
|
|
16
19
|
export type { LinkSegmentParams, OnNavigateHandler, OnNavigateEvent } from './link';
|
|
20
|
+
|
|
21
|
+
// ─── LinkFunction (originally declared here for module augmentation) ───
|
|
22
|
+
//
|
|
23
|
+
// The codegen emits `declare module '@timber-js/app/client' { interface
|
|
24
|
+
// LinkFunction { ... } }` to add per-route call signatures. TypeScript
|
|
25
|
+
// interface merging via module augmentation only merges with interfaces
|
|
26
|
+
// that are ORIGINALLY DECLARED in the augmented module — a re-exported
|
|
27
|
+
// type binding (`export type { LinkFunction } from './link'`) creates a
|
|
28
|
+
// distinct, unmergeable name. So `LinkFunction` and its supporting
|
|
29
|
+
// `ExternalHref` type must live here, not in `./link.tsx`. The `Link`
|
|
30
|
+
// const in `./link.tsx` imports this interface as a type-only import.
|
|
31
|
+
// See TIM-624.
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Href types accepted by the catch-all (non-route) call signature.
|
|
35
|
+
*
|
|
36
|
+
* - External protocols: https://, http://, mailto:, tel:, ftp://
|
|
37
|
+
* - Hash-only and query-only links: #section, ?param=value
|
|
38
|
+
* - Computed `string` variables (non-literal)
|
|
39
|
+
*
|
|
40
|
+
* Internal path literals like "/typo-route" do NOT match — they must
|
|
41
|
+
* be a known route (from codegen) or stored in a `string` variable.
|
|
42
|
+
* This catches wrong hrefs at the type level.
|
|
43
|
+
*/
|
|
44
|
+
type ExternalHref =
|
|
45
|
+
| `http://${string}`
|
|
46
|
+
| `https://${string}`
|
|
47
|
+
| `mailto:${string}`
|
|
48
|
+
| `tel:${string}`
|
|
49
|
+
| `ftp://${string}`
|
|
50
|
+
| `//${string}`
|
|
51
|
+
| `#${string}`
|
|
52
|
+
| `?${string}`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Callable interface for the Link component.
|
|
56
|
+
*
|
|
57
|
+
* Two kinds of call signatures:
|
|
58
|
+
* 1. Per-route (added by codegen via interface merging): DIRECT types
|
|
59
|
+
* for segmentParams — preserves TypeScript excess property checking.
|
|
60
|
+
* 2. Catch-all (below): accepts external hrefs and computed `string`
|
|
61
|
+
* variables. Does NOT accept internal path literals — those must
|
|
62
|
+
* match a known route from the codegen.
|
|
63
|
+
*/
|
|
64
|
+
export interface LinkFunction {
|
|
65
|
+
// External links (literal protocol hrefs)
|
|
66
|
+
(
|
|
67
|
+
props: LinkBaseProps & {
|
|
68
|
+
href: ExternalHref;
|
|
69
|
+
segmentParams?: never;
|
|
70
|
+
searchParams?: {
|
|
71
|
+
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
72
|
+
values: Record<string, unknown>;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
): JSX.Element;
|
|
76
|
+
// Computed/variable href (non-literal string) — e.g. href={myVar}
|
|
77
|
+
// `string extends H` is true only when H is the wide `string` type,
|
|
78
|
+
// not a specific literal. Template literal hrefs like `/blog/${slug}`
|
|
79
|
+
// are handled by resolved-pattern signatures in the codegen.
|
|
80
|
+
<H extends string>(
|
|
81
|
+
props: string extends H
|
|
82
|
+
? LinkBaseProps & {
|
|
83
|
+
href: H;
|
|
84
|
+
segmentParams?: Record<string, string | number | string[]>;
|
|
85
|
+
searchParams?: {
|
|
86
|
+
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
87
|
+
values: Record<string, unknown>;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
: never
|
|
91
|
+
): JSX.Element;
|
|
92
|
+
}
|
|
17
93
|
export { usePendingNavigation } from './use-pending-navigation';
|
|
18
94
|
export { useLinkStatus, LinkStatusContext } from './use-link-status';
|
|
19
95
|
export type { LinkStatus } from './use-link-status';
|
package/src/client/link.tsx
CHANGED
|
@@ -23,11 +23,11 @@ import {
|
|
|
23
23
|
useEffect,
|
|
24
24
|
useRef,
|
|
25
25
|
type AnchorHTMLAttributes,
|
|
26
|
-
type JSX,
|
|
27
26
|
type ReactNode,
|
|
28
27
|
type MouseEvent as ReactMouseEvent,
|
|
29
28
|
} from 'react';
|
|
30
29
|
import type { SearchParamsDefinition } from '../search-params/define.js';
|
|
30
|
+
import type { LinkFunction } from './index.js';
|
|
31
31
|
import { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.js';
|
|
32
32
|
import { LinkStatusContext } from './use-link-status.js';
|
|
33
33
|
import { getRouterOrNull } from './router-ref.js';
|
|
@@ -68,8 +68,12 @@ export type OnNavigateHandler = (e: OnNavigateEvent) => void;
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Base props shared by all Link variants.
|
|
71
|
+
*
|
|
72
|
+
* Exported so the public `LinkFunction` interface (declared in
|
|
73
|
+
* `./index.ts`, where module augmentation can merge into it) can
|
|
74
|
+
* compose this without duplication.
|
|
71
75
|
*/
|
|
72
|
-
interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
|
76
|
+
export interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
|
|
73
77
|
/** Prefetch the RSC payload on hover */
|
|
74
78
|
prefetch?: boolean;
|
|
75
79
|
/**
|
|
@@ -114,69 +118,15 @@ export type LinkSegmentParams<T> = {
|
|
|
114
118
|
};
|
|
115
119
|
|
|
116
120
|
// ─── External Href Types ─────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
* be a known route (from codegen) or stored in a `string` variable.
|
|
127
|
-
* This catches wrong hrefs at the type level. See TIM-624.
|
|
128
|
-
*/
|
|
129
|
-
type ExternalHref =
|
|
130
|
-
| `http://${string}`
|
|
131
|
-
| `https://${string}`
|
|
132
|
-
| `mailto:${string}`
|
|
133
|
-
| `tel:${string}`
|
|
134
|
-
| `ftp://${string}`
|
|
135
|
-
| `//${string}`
|
|
136
|
-
| `#${string}`
|
|
137
|
-
| `?${string}`;
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Callable interface for the Link component.
|
|
141
|
-
*
|
|
142
|
-
* Two kinds of call signatures:
|
|
143
|
-
* 1. Per-route (added by codegen via interface merging): DIRECT types
|
|
144
|
-
* for segmentParams — preserves TypeScript excess property checking.
|
|
145
|
-
* 2. Catch-all (below): accepts external hrefs and computed `string`
|
|
146
|
-
* variables. Does NOT accept internal path literals — those must
|
|
147
|
-
* match a known route from the codegen.
|
|
148
|
-
*
|
|
149
|
-
* See TIM-624.
|
|
150
|
-
*/
|
|
151
|
-
export interface LinkFunction {
|
|
152
|
-
// External links (literal protocol hrefs)
|
|
153
|
-
(
|
|
154
|
-
props: LinkBaseProps & {
|
|
155
|
-
href: ExternalHref;
|
|
156
|
-
segmentParams?: never;
|
|
157
|
-
searchParams?: {
|
|
158
|
-
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
159
|
-
values: Record<string, unknown>;
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
): JSX.Element;
|
|
163
|
-
// Computed/variable href (non-literal string) — e.g. href={myVar}
|
|
164
|
-
// `string extends H` is true only when H is the wide `string` type,
|
|
165
|
-
// not a specific literal. Template literal hrefs like `/blog/${slug}`
|
|
166
|
-
// are handled by resolved-pattern signatures in the codegen.
|
|
167
|
-
<H extends string>(
|
|
168
|
-
props: string extends H
|
|
169
|
-
? LinkBaseProps & {
|
|
170
|
-
href: H;
|
|
171
|
-
segmentParams?: Record<string, string | number | string[]>;
|
|
172
|
-
searchParams?: {
|
|
173
|
-
definition: SearchParamsDefinition<Record<string, unknown>>;
|
|
174
|
-
values: Record<string, unknown>;
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
: never
|
|
178
|
-
): JSX.Element;
|
|
179
|
-
}
|
|
121
|
+
//
|
|
122
|
+
// `ExternalHref` and the public `LinkFunction` interface live in
|
|
123
|
+
// `./index.ts` rather than this file. They MUST be originally declared
|
|
124
|
+
// in the same module that the codegen augments (`@timber-js/app/client`)
|
|
125
|
+
// so that codegen-generated per-route call signatures merge with the
|
|
126
|
+
// same interface that types the `Link` constant. Re-exporting an
|
|
127
|
+
// interface via `export type {}` does NOT participate in module
|
|
128
|
+
// augmentation merging — only originally-declared interfaces do.
|
|
129
|
+
// See TIM-624.
|
|
180
130
|
|
|
181
131
|
/**
|
|
182
132
|
* Runtime-only loose props used internally by the Link implementation.
|
package/src/config-types.ts
CHANGED
|
@@ -54,6 +54,45 @@ export interface TimberUserConfig {
|
|
|
54
54
|
uploadBodySize?: string;
|
|
55
55
|
maxFields?: number;
|
|
56
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Server-action form handling.
|
|
59
|
+
*
|
|
60
|
+
* See design/08-forms-and-actions.md §"Validation errors" and
|
|
61
|
+
* design/13-security.md §"Sensitive field stripping".
|
|
62
|
+
*/
|
|
63
|
+
forms?: {
|
|
64
|
+
/**
|
|
65
|
+
* Strip sensitive fields (passwords, tokens, CVV, SSN, etc.) from the
|
|
66
|
+
* `submittedValues` echoed back on validation failure.
|
|
67
|
+
*
|
|
68
|
+
* Applied to both the with-JS (`createActionClient`) and no-JS (form POST
|
|
69
|
+
* re-render) paths. Safe-by-default: the built-in deny-list is active
|
|
70
|
+
* unless you explicitly opt out.
|
|
71
|
+
*
|
|
72
|
+
* - `true` / omitted — use the built-in deny-list (default, recommended).
|
|
73
|
+
* - `false` — do not strip anything. Dev convenience only; never do this
|
|
74
|
+
* in production — plaintext passwords end up in `defaultValue` attributes,
|
|
75
|
+
* browser history, proxy logs, and the back-forward cache.
|
|
76
|
+
* - `string[]` — additional field names to strip, merged with the built-in list.
|
|
77
|
+
*
|
|
78
|
+
* The built-in list matches (case-insensitive, substring match on
|
|
79
|
+
* normalized names) the following patterns: password / passwd / pwd,
|
|
80
|
+
* secret, apiKey, accessToken, refreshToken, cvv, cvc, cardNumber,
|
|
81
|
+
* cardCvc, ssn, socialSecurityNumber, otp, totp, mfaCode, twoFactorCode,
|
|
82
|
+
* privateKey — plus exact match on `token` (exact-only to avoid
|
|
83
|
+
* false positives on `csrfToken`).
|
|
84
|
+
*
|
|
85
|
+
* **Function predicates are not supported at the global level** because
|
|
86
|
+
* `timber.config.ts` is JSON-serialized into the runtime config and
|
|
87
|
+
* functions cannot cross that boundary. For custom predicates, use
|
|
88
|
+
* the per-action override: `createActionClient({ stripSensitiveFields:
|
|
89
|
+
* (name) => ... })` (functions are supported at the per-action level
|
|
90
|
+
* since `createActionClient` runs in the same module graph as user code).
|
|
91
|
+
*
|
|
92
|
+
* See TIM-816.
|
|
93
|
+
*/
|
|
94
|
+
stripSensitiveFields?: boolean | readonly string[];
|
|
95
|
+
};
|
|
57
96
|
pageExtensions?: string[];
|
|
58
97
|
/**
|
|
59
98
|
* Slow request threshold in milliseconds. Requests exceeding this emit
|
package/src/config-validation.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Design doc: 18-build-system.md
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
11
12
|
import type { TimberUserConfig } from './config-types.js';
|
|
12
13
|
|
|
13
14
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
@@ -262,11 +263,14 @@ const REQUIRED_PEERS = ['react', 'react-dom', '@vitejs/plugin-react', '@vitejs/p
|
|
|
262
263
|
export function checkPeerDependencies(projectRoot: string): PeerDepResult[] {
|
|
263
264
|
const results: PeerDepResult[] = [];
|
|
264
265
|
|
|
266
|
+
// Use createRequire from the project root to resolve as the user would.
|
|
267
|
+
// `createRequire` MUST be a top-level ESM import — inline `require('node:module')`
|
|
268
|
+
// throws "Dynamic require not supported" in ESM and the catch below would
|
|
269
|
+
// mark every peer as missing. Same class of bug as TIM-796.
|
|
270
|
+
const userRequire = createRequire(`${projectRoot}/package.json`);
|
|
271
|
+
|
|
265
272
|
for (const name of REQUIRED_PEERS) {
|
|
266
273
|
try {
|
|
267
|
-
// Use createRequire from the project root to resolve as the user would
|
|
268
|
-
const { createRequire } = require('node:module');
|
|
269
|
-
const userRequire = createRequire(`${projectRoot}/package.json`);
|
|
270
274
|
userRequire.resolve(name);
|
|
271
275
|
results.push({ name, status: 'ok' });
|
|
272
276
|
} catch {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-output emission for the timber-fonts plugin.
|
|
3
|
+
*
|
|
4
|
+
* Splits the `generateBundle` hook into pure helpers so the plugin shell
|
|
5
|
+
* can stay focused on Vite wiring. The functions here are deliberately
|
|
6
|
+
* Rollup-agnostic — they take an `EmitFile` callback so the plugin can
|
|
7
|
+
* pass `this.emitFile` from the `generateBundle` context.
|
|
8
|
+
*
|
|
9
|
+
* Design doc: 24-fonts.md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { resolve, normalize } from 'node:path';
|
|
14
|
+
import type { ManifestFontEntry, BuildManifest } from '../server/build-manifest.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Minimal shape of the asset descriptor accepted by Rollup's `emitFile`.
|
|
18
|
+
* Declared inline rather than importing from `rollup` to keep `bundle.ts`
|
|
19
|
+
* Rollup-agnostic and avoid pulling the type into the build graph.
|
|
20
|
+
*/
|
|
21
|
+
interface EmittedAsset {
|
|
22
|
+
type: 'asset';
|
|
23
|
+
fileName: string;
|
|
24
|
+
source: string | Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
import type { FontPipeline } from './pipeline.js';
|
|
27
|
+
import type { CachedFont } from './google.js';
|
|
28
|
+
import { inferFontFormat } from './local.js';
|
|
29
|
+
|
|
30
|
+
type EmitFile = (asset: EmittedAsset) => string;
|
|
31
|
+
type EmitWarning = (msg: string) => void;
|
|
32
|
+
|
|
33
|
+
/** Group cached Google Font binaries by lowercase family name. */
|
|
34
|
+
export function groupCachedFontsByFamily(
|
|
35
|
+
cachedFonts: readonly CachedFont[]
|
|
36
|
+
): Map<string, CachedFont[]> {
|
|
37
|
+
const cachedByFamily = new Map<string, CachedFont[]>();
|
|
38
|
+
for (const cf of cachedFonts) {
|
|
39
|
+
const key = cf.face.family.toLowerCase();
|
|
40
|
+
const arr = cachedByFamily.get(key) ?? [];
|
|
41
|
+
arr.push(cf);
|
|
42
|
+
cachedByFamily.set(key, arr);
|
|
43
|
+
}
|
|
44
|
+
return cachedByFamily;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Emit cached Google Font binaries and local font files into the build
|
|
49
|
+
* output under `_timber/fonts/`. Local files missing on disk produce a
|
|
50
|
+
* warning rather than an error so the build still succeeds.
|
|
51
|
+
*
|
|
52
|
+
* Cached Google Font binaries are deduplicated by `hashedFilename` so
|
|
53
|
+
* each unique file is written exactly once even when multiple font
|
|
54
|
+
* entries share a family (and therefore share `CachedFont` references).
|
|
55
|
+
*/
|
|
56
|
+
export function emitFontAssets(
|
|
57
|
+
pipeline: FontPipeline,
|
|
58
|
+
emitFile: EmitFile,
|
|
59
|
+
warn: EmitWarning
|
|
60
|
+
): void {
|
|
61
|
+
// Cached Google Font binaries (content-hashed, deduped by filename)
|
|
62
|
+
for (const cf of pipeline.uniqueCachedFiles()) {
|
|
63
|
+
emitFile({
|
|
64
|
+
type: 'asset',
|
|
65
|
+
fileName: `_timber/fonts/${cf.hashedFilename}`,
|
|
66
|
+
source: cf.data,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Local font files (emitted by basename)
|
|
71
|
+
for (const font of pipeline.fonts()) {
|
|
72
|
+
if (font.provider !== 'local' || !font.localSources) continue;
|
|
73
|
+
for (const src of font.localSources) {
|
|
74
|
+
const absolutePath = normalize(resolve(src.path));
|
|
75
|
+
if (!existsSync(absolutePath)) {
|
|
76
|
+
warn(`Local font file not found: ${absolutePath}`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const basename = src.path.split('/').pop() ?? src.path;
|
|
80
|
+
const data = readFileSync(absolutePath);
|
|
81
|
+
emitFile({
|
|
82
|
+
type: 'asset',
|
|
83
|
+
fileName: `_timber/fonts/${basename}`,
|
|
84
|
+
source: data,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Populate `buildManifest.fonts` with `ManifestFontEntry[]` keyed by the
|
|
92
|
+
* importer module path (relative to the project root, matching how Vite's
|
|
93
|
+
* `manifest.json` keys css/js).
|
|
94
|
+
*
|
|
95
|
+
* Google fonts use the content-hashed filenames produced during
|
|
96
|
+
* `buildStart`. Local fonts use the source basename.
|
|
97
|
+
*/
|
|
98
|
+
export function writeFontManifest(
|
|
99
|
+
pipeline: FontPipeline,
|
|
100
|
+
buildManifest: BuildManifest,
|
|
101
|
+
rootDir: string
|
|
102
|
+
): void {
|
|
103
|
+
const fontsByImporter = new Map<string, ManifestFontEntry[]>();
|
|
104
|
+
|
|
105
|
+
for (const entry of pipeline.entries()) {
|
|
106
|
+
const manifestEntries = fontsByImporter.get(entry.importer) ?? [];
|
|
107
|
+
|
|
108
|
+
if (entry.provider === 'local' && entry.localSources) {
|
|
109
|
+
for (const src of entry.localSources) {
|
|
110
|
+
const filename = src.path.split('/').pop() ?? src.path;
|
|
111
|
+
const format = inferFontFormat(src.path);
|
|
112
|
+
manifestEntries.push({
|
|
113
|
+
href: `/_timber/fonts/${filename}`,
|
|
114
|
+
format,
|
|
115
|
+
crossOrigin: 'anonymous',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
} else if (entry.cachedFiles) {
|
|
119
|
+
// Google fonts: use the per-entry cached files attached during
|
|
120
|
+
// buildStart. Each entry owns its own cached binaries (TIM-829),
|
|
121
|
+
// so we no longer need a family-grouped lookup.
|
|
122
|
+
for (const cf of entry.cachedFiles) {
|
|
123
|
+
manifestEntries.push({
|
|
124
|
+
href: `/_timber/fonts/${cf.hashedFilename}`,
|
|
125
|
+
format: 'woff2',
|
|
126
|
+
crossOrigin: 'anonymous',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fontsByImporter.set(entry.importer, manifestEntries);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Normalize importer paths to be relative to project root (matching how
|
|
135
|
+
// Vite's manifest.json keys work for css/js).
|
|
136
|
+
for (const [importer, entries] of fontsByImporter) {
|
|
137
|
+
const relativePath = importer.startsWith(rootDir)
|
|
138
|
+
? importer.slice(rootDir.length + 1)
|
|
139
|
+
: importer;
|
|
140
|
+
buildManifest.fonts[relativePath] = entries;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-mode middleware that serves local font binaries under
|
|
3
|
+
* `/_timber/fonts/<basename>`.
|
|
4
|
+
*
|
|
5
|
+
* Only files registered in the FontPipeline are served, and only by
|
|
6
|
+
* basename — directory traversal and path separators in the requested
|
|
7
|
+
* filename are rejected up front. Font CSS is no longer served here; it
|
|
8
|
+
* goes through Vite's CSS pipeline via virtual modules.
|
|
9
|
+
*
|
|
10
|
+
* Design doc: 24-fonts.md
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ViteDevServer } from 'vite';
|
|
14
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { resolve, normalize } from 'node:path';
|
|
16
|
+
import type { FontPipeline } from './pipeline.js';
|
|
17
|
+
|
|
18
|
+
const FONT_MIME_TYPES: Record<string, string> = {
|
|
19
|
+
woff2: 'font/woff2',
|
|
20
|
+
woff: 'font/woff',
|
|
21
|
+
ttf: 'font/ttf',
|
|
22
|
+
otf: 'font/otf',
|
|
23
|
+
eot: 'application/vnd.ms-fontopen',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wire the timber-fonts dev middleware onto a Vite dev server.
|
|
28
|
+
*
|
|
29
|
+
* Returns synchronously after registering the middleware. The pipeline is
|
|
30
|
+
* captured by reference so that fonts registered after `configureServer`
|
|
31
|
+
* runs (during transform) are still resolvable.
|
|
32
|
+
*/
|
|
33
|
+
export function installFontDevMiddleware(server: ViteDevServer, pipeline: FontPipeline): void {
|
|
34
|
+
server.middlewares.use((req, res, next) => {
|
|
35
|
+
const url = req.url;
|
|
36
|
+
if (!url || !url.startsWith('/_timber/fonts/')) return next();
|
|
37
|
+
|
|
38
|
+
const requestedFilename = url.slice('/_timber/fonts/'.length);
|
|
39
|
+
// Reject path traversal attempts and any subdirectory access. We only
|
|
40
|
+
// serve flat basenames out of the registry.
|
|
41
|
+
if (requestedFilename.includes('..') || requestedFilename.includes('/')) {
|
|
42
|
+
res.statusCode = 400;
|
|
43
|
+
res.end('Bad request');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Find the matching font file in the registry. We iterate every local
|
|
48
|
+
// font's source list and match by basename. The set is small (one per
|
|
49
|
+
// localFont() call) so this is cheap.
|
|
50
|
+
for (const font of pipeline.fonts()) {
|
|
51
|
+
if (font.provider !== 'local' || !font.localSources) continue;
|
|
52
|
+
for (const src of font.localSources) {
|
|
53
|
+
const basename = src.path.split('/').pop() ?? '';
|
|
54
|
+
if (basename !== requestedFilename) continue;
|
|
55
|
+
|
|
56
|
+
const absolutePath = normalize(resolve(src.path));
|
|
57
|
+
if (!existsSync(absolutePath)) {
|
|
58
|
+
res.statusCode = 404;
|
|
59
|
+
res.end('Not found');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const data = readFileSync(absolutePath);
|
|
63
|
+
const ext = absolutePath.split('.').pop()?.toLowerCase();
|
|
64
|
+
res.setHeader('Content-Type', FONT_MIME_TYPES[ext ?? ''] ?? 'application/octet-stream');
|
|
65
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
66
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
67
|
+
res.end(data);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
next();
|
|
73
|
+
});
|
|
74
|
+
}
|