@nifrajs/web 3.3.0 → 4.0.0
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/backend-only.d.ts +15 -0
- package/dist/backend-only.d.ts.map +1 -0
- package/dist/backend-only.js +16 -0
- package/dist/backend-only.js.map +1 -0
- package/dist/boundary.d.ts +5 -0
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +11 -2
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +35 -6
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +89 -35
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +18 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +97 -17
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +72 -23
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +411 -98
- package/dist/build.js.map +1 -1
- package/dist/bun-dev-entry.d.ts +1 -1
- package/dist/bun-dev-entry.js +1 -1
- package/dist/cdn.d.ts +74 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +132 -0
- package/dist/cdn.js.map +1 -0
- package/dist/client.d.ts +21 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +226 -15
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +4 -3
- package/dist/conformance.d.ts.map +1 -1
- package/dist/conformance.js +12 -3
- package/dist/conformance.js.map +1 -1
- package/dist/csp.d.ts +93 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +122 -0
- package/dist/csp.js.map +1 -0
- package/dist/css-contract.d.ts +21 -0
- package/dist/css-contract.d.ts.map +1 -0
- package/dist/css-contract.js +28 -0
- package/dist/css-contract.js.map +1 -0
- package/dist/dev-client.d.ts +76 -0
- package/dist/dev-client.d.ts.map +1 -0
- package/dist/dev-client.js +280 -0
- package/dist/dev-client.js.map +1 -0
- package/dist/dev-diagnostics.d.ts +26 -2
- package/dist/dev-diagnostics.d.ts.map +1 -1
- package/dist/dev-diagnostics.js +38 -7
- package/dist/dev-diagnostics.js.map +1 -1
- package/dist/dev-error.d.ts +4 -13
- package/dist/dev-error.d.ts.map +1 -1
- package/dist/dev-error.js +46 -4
- package/dist/dev-error.js.map +1 -1
- package/dist/dev-feed.d.ts +298 -0
- package/dist/dev-feed.d.ts.map +1 -0
- package/dist/dev-feed.js +797 -0
- package/dist/dev-feed.js.map +1 -0
- package/dist/dev-indicator.d.ts +38 -0
- package/dist/dev-indicator.d.ts.map +1 -0
- package/dist/dev-indicator.js +93 -0
- package/dist/dev-indicator.js.map +1 -0
- package/dist/dev-session.d.ts +77 -0
- package/dist/dev-session.d.ts.map +1 -0
- package/dist/dev-session.js +580 -0
- package/dist/dev-session.js.map +1 -0
- package/dist/dev-sourcemap.d.ts +27 -0
- package/dist/dev-sourcemap.d.ts.map +1 -0
- package/dist/dev-sourcemap.js +235 -0
- package/dist/dev-sourcemap.js.map +1 -0
- package/dist/dev-ssr-graph.d.ts.map +1 -1
- package/dist/dev-ssr-graph.js +13 -5
- package/dist/dev-ssr-graph.js.map +1 -1
- package/dist/dev.d.ts +15 -2
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +69 -44
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +35 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -0
- package/dist/diagnostic-catalog.js +203 -0
- package/dist/diagnostic-catalog.js.map +1 -0
- package/dist/diagnostic-prompt.d.ts +49 -0
- package/dist/diagnostic-prompt.d.ts.map +1 -0
- package/dist/diagnostic-prompt.js +303 -0
- package/dist/diagnostic-prompt.js.map +1 -0
- package/dist/diagnostic.d.ts +17 -23
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +31 -39
- package/dist/diagnostic.js.map +1 -1
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +4 -1
- package/dist/draft.js.map +1 -1
- package/dist/fn.d.ts +5 -0
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +13 -3
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -2
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +15 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +48 -0
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
- package/dist/internal/cdn/cloudflare.js +94 -0
- package/dist/internal/cdn/cloudflare.js.map +1 -0
- package/dist/internal/cdn/core.d.ts +91 -0
- package/dist/internal/cdn/core.d.ts.map +1 -0
- package/dist/internal/cdn/core.js +220 -0
- package/dist/internal/cdn/core.js.map +1 -0
- package/dist/internal/cdn/fastly.d.ts +17 -0
- package/dist/internal/cdn/fastly.d.ts.map +1 -0
- package/dist/internal/cdn/fastly.js +43 -0
- package/dist/internal/cdn/fastly.js.map +1 -0
- package/dist/internal/cdn/vercel.d.ts +31 -0
- package/dist/internal/cdn/vercel.d.ts.map +1 -0
- package/dist/internal/cdn/vercel.js +56 -0
- package/dist/internal/cdn/vercel.js.map +1 -0
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/codegen.d.ts +3 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +151 -30
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-csp.d.ts +24 -0
- package/dist/internal/dev-csp.d.ts.map +1 -0
- package/dist/internal/dev-csp.js +160 -0
- package/dist/internal/dev-csp.js.map +1 -0
- package/dist/internal/dev-reserved.d.ts +25 -0
- package/dist/internal/dev-reserved.d.ts.map +1 -0
- package/dist/internal/dev-reserved.js +39 -0
- package/dist/internal/dev-reserved.js.map +1 -0
- package/dist/internal/html-spans.d.ts +10 -0
- package/dist/internal/html-spans.d.ts.map +1 -0
- package/dist/internal/html-spans.js +46 -0
- package/dist/internal/html-spans.js.map +1 -0
- package/dist/internal/loading-runtime.d.ts +44 -0
- package/dist/internal/loading-runtime.d.ts.map +1 -0
- package/dist/internal/loading-runtime.js +206 -0
- package/dist/internal/loading-runtime.js.map +1 -0
- package/dist/internal/matches-runtime.d.ts +8 -0
- package/dist/internal/matches-runtime.d.ts.map +1 -0
- package/dist/internal/matches-runtime.js +61 -0
- package/dist/internal/matches-runtime.js.map +1 -0
- package/dist/internal/mount-shadow.d.ts +36 -0
- package/dist/internal/mount-shadow.d.ts.map +1 -0
- package/dist/internal/mount-shadow.js +65 -0
- package/dist/internal/mount-shadow.js.map +1 -0
- package/dist/internal/node-builtins.d.ts +3 -0
- package/dist/internal/node-builtins.d.ts.map +1 -0
- package/dist/internal/node-builtins.js +79 -0
- package/dist/internal/node-builtins.js.map +1 -0
- package/dist/internal/output-guard.d.ts +65 -0
- package/dist/internal/output-guard.d.ts.map +1 -0
- package/dist/internal/output-guard.js +630 -0
- package/dist/internal/output-guard.js.map +1 -0
- package/dist/internal/page-execution.d.ts +69 -0
- package/dist/internal/page-execution.d.ts.map +1 -0
- package/dist/internal/page-execution.js +1021 -0
- package/dist/internal/page-execution.js.map +1 -0
- package/dist/internal/parity.d.ts +21 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +104 -22
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +12 -0
- package/dist/internal/private-env.d.ts.map +1 -0
- package/dist/internal/private-env.js +335 -0
- package/dist/internal/private-env.js.map +1 -0
- package/dist/internal/public-url.d.ts +3 -0
- package/dist/internal/public-url.d.ts.map +1 -0
- package/dist/internal/public-url.js +18 -0
- package/dist/internal/public-url.js.map +1 -0
- package/dist/internal/render-document.d.ts +37 -21
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +104 -65
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-url.d.ts +3 -0
- package/dist/internal/request-url.d.ts.map +1 -0
- package/dist/internal/request-url.js +14 -0
- package/dist/internal/request-url.js.map +1 -0
- package/dist/internal/response-controls.d.ts +75 -0
- package/dist/internal/response-controls.d.ts.map +1 -0
- package/dist/internal/response-controls.js +256 -0
- package/dist/internal/response-controls.js.map +1 -0
- package/dist/internal/runtime-contract.d.ts +11 -16
- package/dist/internal/runtime-contract.d.ts.map +1 -1
- package/dist/internal/runtime-contract.js.map +1 -1
- package/dist/internal/secret-scan.d.ts +104 -0
- package/dist/internal/secret-scan.d.ts.map +1 -0
- package/dist/internal/secret-scan.js +485 -0
- package/dist/internal/secret-scan.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +6 -29
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +6 -66
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/source-map.d.ts +47 -0
- package/dist/internal/source-map.d.ts.map +1 -0
- package/dist/internal/source-map.js +225 -0
- package/dist/internal/source-map.js.map +1 -0
- package/dist/internal/target-compat.d.ts +27 -0
- package/dist/internal/target-compat.d.ts.map +1 -0
- package/dist/internal/target-compat.js +77 -0
- package/dist/internal/target-compat.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +6 -0
- package/dist/internal/timing-safe-equal.d.ts.map +1 -1
- package/dist/internal/timing-safe-equal.js +10 -0
- package/dist/internal/timing-safe-equal.js.map +1 -1
- package/dist/internal/vitals-runtime.d.ts +18 -0
- package/dist/internal/vitals-runtime.d.ts.map +1 -0
- package/dist/internal/vitals-runtime.js +38 -0
- package/dist/internal/vitals-runtime.js.map +1 -0
- package/dist/internal/web-app.d.ts +56 -8
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +215 -772
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/zone-graph.d.ts +89 -0
- package/dist/internal/zone-graph.d.ts.map +1 -0
- package/dist/internal/zone-graph.js +492 -0
- package/dist/internal/zone-graph.js.map +1 -0
- package/dist/isr.d.ts +83 -6
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +301 -39
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +3 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +37 -9
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +221 -14
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +386 -9
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +26 -5
- package/dist/module-graph.d.ts.map +1 -1
- package/dist/module-graph.js +17 -7
- package/dist/module-graph.js.map +1 -1
- package/dist/navigation.d.ts +17 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +11 -5
- package/dist/navigation.js.map +1 -1
- package/dist/nonce.d.ts +47 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/nonce.js +63 -0
- package/dist/nonce.js.map +1 -0
- package/dist/plugins/css-modules.d.ts.map +1 -1
- package/dist/plugins/css-modules.js +4 -3
- package/dist/plugins/css-modules.js.map +1 -1
- package/dist/plugins/kit.d.ts +9 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +18 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/svg.d.ts +22 -3
- package/dist/plugins/svg.d.ts.map +1 -1
- package/dist/plugins/svg.js +112 -1
- package/dist/plugins/svg.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +107 -3
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +378 -17
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +61 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
- package/dist/plugins/vite-zone-guard.js +208 -0
- package/dist/plugins/vite-zone-guard.js.map +1 -0
- package/dist/plugins/zone-guard.d.ts +27 -0
- package/dist/plugins/zone-guard.d.ts.map +1 -0
- package/dist/plugins/zone-guard.js +72 -0
- package/dist/plugins/zone-guard.js.map +1 -0
- package/dist/prerender.d.ts +10 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +35 -3
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +9 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +131 -48
- package/dist/public-dir.js.map +1 -1
- package/dist/pwa-manifest.d.ts +68 -0
- package/dist/pwa-manifest.d.ts.map +1 -0
- package/dist/pwa-manifest.js +178 -0
- package/dist/pwa-manifest.js.map +1 -0
- package/dist/query.js +4 -4
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +42 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +6 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -0
- package/dist/route-manifest.d.ts.map +1 -1
- package/dist/route-manifest.js +26 -3
- package/dist/route-manifest.js.map +1 -1
- package/dist/route-types.d.ts +24 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +195 -0
- package/dist/route-types.js.map +1 -0
- package/dist/router.d.ts +28 -6
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +129 -40
- package/dist/router.js.map +1 -1
- package/dist/trusted-html.d.ts +34 -0
- package/dist/trusted-html.d.ts.map +1 -0
- package/dist/trusted-html.js +35 -0
- package/dist/trusted-html.js.map +1 -0
- package/dist/vitals.d.ts +33 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +16 -0
- package/dist/vitals.js.map +1 -0
- package/dist/vite.d.ts +16 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +187 -36
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +70 -0
- package/dist/zones.d.ts.map +1 -0
- package/dist/zones.js +365 -0
- package/dist/zones.js.map +1 -0
- package/package.json +67 -14
- package/src/backend-only.ts +16 -0
- package/src/boundary.ts +18 -5
- package/src/build-plan.ts +113 -36
- package/src/build-vite.ts +124 -15
- package/src/build.ts +535 -102
- package/src/bun-dev-entry.ts +1 -1
- package/src/cdn.ts +216 -0
- package/src/client.ts +251 -18
- package/src/conformance.ts +16 -3
- package/src/csp.ts +194 -0
- package/src/css-contract.ts +44 -0
- package/src/dev-client.ts +349 -0
- package/src/dev-diagnostics.ts +60 -7
- package/src/dev-error.ts +49 -5
- package/src/dev-feed.ts +1140 -0
- package/src/dev-indicator.ts +113 -0
- package/src/dev-session.ts +729 -0
- package/src/dev-sourcemap.ts +281 -0
- package/src/dev-ssr-graph.ts +16 -5
- package/src/dev.ts +86 -43
- package/src/diagnostic-catalog.ts +243 -0
- package/src/diagnostic-prompt.ts +355 -0
- package/src/diagnostic.ts +63 -66
- package/src/draft.ts +4 -1
- package/src/fn.ts +19 -3
- package/src/fs.ts +13 -2
- package/src/index.ts +57 -10
- package/src/internal/cdn/cloudflare.ts +146 -0
- package/src/internal/cdn/core.ts +316 -0
- package/src/internal/cdn/fastly.ts +62 -0
- package/src/internal/cdn/vercel.ts +95 -0
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/codegen.ts +180 -29
- package/src/internal/dev-csp.ts +176 -0
- package/src/internal/dev-reserved.ts +52 -0
- package/src/internal/html-spans.ts +50 -0
- package/src/internal/loading-runtime.ts +244 -0
- package/src/internal/matches-runtime.ts +62 -0
- package/src/internal/mount-shadow.ts +88 -0
- package/src/internal/node-builtins.ts +79 -0
- package/src/internal/output-guard.ts +701 -0
- package/src/internal/page-execution.ts +1371 -0
- package/src/internal/parity.ts +150 -36
- package/src/internal/private-env.ts +334 -0
- package/src/internal/public-url.ts +22 -0
- package/src/internal/render-document.ts +153 -73
- package/src/internal/request-url.ts +14 -0
- package/src/internal/response-controls.ts +284 -0
- package/src/internal/runtime-contract.ts +10 -15
- package/src/internal/secret-scan.ts +623 -0
- package/src/internal/server-boundary.ts +6 -69
- package/src/internal/source-map.ts +256 -0
- package/src/internal/target-compat.ts +92 -0
- package/src/internal/timing-safe-equal.ts +13 -0
- package/src/internal/vitals-runtime.ts +53 -0
- package/src/internal/web-app.ts +290 -879
- package/src/internal/zone-graph.ts +593 -0
- package/src/isr.ts +366 -39
- package/src/llms-txt.ts +44 -8
- package/src/manifest.ts +671 -17
- package/src/module-graph.ts +53 -9
- package/src/navigation.ts +29 -6
- package/src/nonce.ts +104 -0
- package/src/plugins/css-modules.ts +5 -3
- package/src/plugins/kit.ts +24 -0
- package/src/plugins/svg.ts +135 -4
- package/src/plugins/vite-leak-guard.ts +529 -16
- package/src/plugins/vite-zone-guard.ts +263 -0
- package/src/plugins/zone-guard.ts +110 -0
- package/src/prerender.ts +49 -3
- package/src/public-dir.ts +135 -49
- package/src/pwa-manifest.ts +226 -0
- package/src/query.ts +4 -4
- package/src/render-seam.ts +46 -1
- package/src/route-manifest.ts +15 -3
- package/src/route-types.ts +264 -0
- package/src/router.ts +163 -63
- package/src/trusted-html.ts +44 -0
- package/src/vitals.ts +44 -0
- package/src/vite.ts +207 -36
- package/src/zones.ts +430 -0
- package/dist/plugins/vite-server-only.d.ts +0 -49
- package/dist/plugins/vite-server-only.d.ts.map +0 -1
- package/dist/plugins/vite-server-only.js +0 -57
- package/dist/plugins/vite-server-only.js.map +0 -1
- package/dist/server-only.d.ts +0 -21
- package/dist/server-only.d.ts.map +0 -1
- package/dist/server-only.js +0 -25
- package/dist/server-only.js.map +0 -1
- package/src/plugins/vite-server-only.ts +0 -75
- package/src/server-only.ts +0 -25
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CookieOptions,
|
|
3
|
+
type ResponseResult,
|
|
4
|
+
status as statusResult,
|
|
5
|
+
} from "@nifrajs/core/server"
|
|
6
|
+
import type { LoaderResponseControls } from "../manifest.ts"
|
|
7
|
+
import { DATA_HEADER } from "../router.ts"
|
|
8
|
+
import { isResponseResult } from "./render-document.ts"
|
|
9
|
+
|
|
10
|
+
/** The serving app's response controls - the structural subset of a route handler's `c.set` used here. */
|
|
11
|
+
export interface CoreResponseControls {
|
|
12
|
+
readonly headers: Record<string, string>
|
|
13
|
+
cookie(name: string, value: string, options?: CookieOptions): void
|
|
14
|
+
deleteCookie(name: string, options?: Pick<CookieOptions, "path" | "domain">): void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PRIVATE_NO_STORE = "private, no-store"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Headers of a navigation data response. It shares its URL with the document and differs only by a
|
|
21
|
+
* request header, so it must never be stored by a cache keyed on the URL alone: a stored data payload
|
|
22
|
+
* would be served in place of the page.
|
|
23
|
+
*/
|
|
24
|
+
export const DATA_RESPONSE_HEADERS: Readonly<Record<string, string>> = Object.freeze({
|
|
25
|
+
"cache-control": PRIVATE_NO_STORE,
|
|
26
|
+
vary: DATA_HEADER,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Merge order of the header writers: middleware root to leaf (`MIDDLEWARE_SCOPE + i`), layouts root
|
|
31
|
+
* to leaf (`i`), then the page loader, then the action.
|
|
32
|
+
*/
|
|
33
|
+
export const PAGE_SCOPE = 1e9
|
|
34
|
+
export const ACTION_SCOPE = PAGE_SCOPE + 1
|
|
35
|
+
export const MIDDLEWARE_SCOPE = -PAGE_SCOPE
|
|
36
|
+
|
|
37
|
+
// RFC 9110 `token` and `field-value` (visible ASCII, space, tab, obs-text). A value outside this set
|
|
38
|
+
// could split the header block or be rejected by the transport after the loader already ran.
|
|
39
|
+
const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/
|
|
40
|
+
const FIELD_VALUE = /^[\t\x20-\x7e\x80-\xff]*$/
|
|
41
|
+
|
|
42
|
+
const FRAMEWORK_PREFIX = "x-nifra-"
|
|
43
|
+
|
|
44
|
+
const TRANSPORT_OWNED = new Set([
|
|
45
|
+
"connection",
|
|
46
|
+
"content-encoding",
|
|
47
|
+
"content-length",
|
|
48
|
+
"keep-alive",
|
|
49
|
+
"te",
|
|
50
|
+
"trailer",
|
|
51
|
+
"transfer-encoding",
|
|
52
|
+
"upgrade",
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
const reservedReason = (name: string): string | undefined => {
|
|
56
|
+
if (name === "set-cookie") return "use ctx.set.cookie(), which applies the secure cookie defaults"
|
|
57
|
+
if (name === "location") return "return redirect() instead"
|
|
58
|
+
if (name === "content-type") return "the framework sets it for the representation it renders"
|
|
59
|
+
if (TRANSPORT_OWNED.has(name)) return "the transport owns it"
|
|
60
|
+
if (name.startsWith(FRAMEWORK_PREFIX)) return "the x-nifra- prefix is reserved for the framework"
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `existing` with the data-request header added, so a cache keeps the document and its data apart. */
|
|
65
|
+
export function varyOnDataHeader(existing: string | null | undefined): string {
|
|
66
|
+
if (existing === null || existing === undefined) return DATA_HEADER
|
|
67
|
+
let empty = true
|
|
68
|
+
for (const part of existing.split(",")) {
|
|
69
|
+
const name = part.trim().toLowerCase()
|
|
70
|
+
if (name === "") continue
|
|
71
|
+
if (name === "*" || name === DATA_HEADER) return existing
|
|
72
|
+
empty = false
|
|
73
|
+
}
|
|
74
|
+
return empty ? DATA_HEADER : `${existing}, ${DATA_HEADER}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const CACHE_CONTROL = "cache-control"
|
|
78
|
+
|
|
79
|
+
/** Headers of a rendered page that carries a cookie queued through `ctx.set`. */
|
|
80
|
+
export const PRIVATE_PAGE_HEADERS: Readonly<Record<string, string>> = Object.freeze({
|
|
81
|
+
[CACHE_CONTROL]: PRIVATE_NO_STORE,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `outcome` kept out of shared caches, for a request that queued a cookie through `ctx.set`.
|
|
86
|
+
*
|
|
87
|
+
* A rendered page is already private, and a plain render (`redirect()`, `status()`) inherits the
|
|
88
|
+
* serving app's header record. That leaves a hand-built `Response`, which inherits nothing, and a
|
|
89
|
+
* plain render that names its own `cache-control`, which would win over the inherited one.
|
|
90
|
+
*/
|
|
91
|
+
export function privateOutcome(outcome: unknown): unknown {
|
|
92
|
+
if (outcome instanceof Response) return privateResponse(outcome)
|
|
93
|
+
if (!isResponseResult(outcome)) return outcome
|
|
94
|
+
const plain = outcome.plain
|
|
95
|
+
const own = plain?.headers
|
|
96
|
+
if (plain === undefined || own === undefined) return outcome
|
|
97
|
+
let headers: Record<string, string> | undefined
|
|
98
|
+
for (const name of Object.keys(own)) {
|
|
99
|
+
if (name.toLowerCase() !== CACHE_CONTROL) continue
|
|
100
|
+
headers ??= { ...own }
|
|
101
|
+
delete headers[name]
|
|
102
|
+
}
|
|
103
|
+
if (headers === undefined) return outcome
|
|
104
|
+
headers[CACHE_CONTROL] = PRIVATE_NO_STORE
|
|
105
|
+
return statusResult(plain.status, plain.body, { headers }) satisfies ResponseResult
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function privateResponse(response: Response): Response {
|
|
109
|
+
if (response.headers.get(CACHE_CONTROL) === PRIVATE_NO_STORE) return response
|
|
110
|
+
try {
|
|
111
|
+
response.headers.set(CACHE_CONTROL, PRIVATE_NO_STORE)
|
|
112
|
+
return response
|
|
113
|
+
} catch {
|
|
114
|
+
// Immutable headers (`Response.redirect()`, a `fetch()` result): answer with a copy.
|
|
115
|
+
const headers = new Headers(response.headers)
|
|
116
|
+
headers.set(CACHE_CONTROL, PRIVATE_NO_STORE)
|
|
117
|
+
return new Response(response.body, {
|
|
118
|
+
status: response.status,
|
|
119
|
+
statusText: response.statusText,
|
|
120
|
+
headers,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface WrittenHeaders {
|
|
126
|
+
readonly order: number
|
|
127
|
+
readonly headers: Record<string, string>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* One page request's `ctx.set`: the response headers and cookies its loaders and action write.
|
|
132
|
+
*
|
|
133
|
+
* Every loader gets its own header record ({@link scope}), merged in a fixed order when the response
|
|
134
|
+
* is committed, so layout loaders that run in parallel with the page loader cannot race on a name.
|
|
135
|
+
* Cookies go straight to the serving app's own controls, which serialize them with the secure defaults
|
|
136
|
+
* and append them to whatever the request answers with.
|
|
137
|
+
*/
|
|
138
|
+
export class PageResponseControls {
|
|
139
|
+
readonly #c: { readonly set: CoreResponseControls }
|
|
140
|
+
#written: WrittenHeaders[] | undefined
|
|
141
|
+
#sealed = false
|
|
142
|
+
#personalized = false
|
|
143
|
+
|
|
144
|
+
constructor(c: { readonly set: CoreResponseControls }) {
|
|
145
|
+
this.#c = c
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** True once a cookie was queued through `ctx.set`: the response is specific to this visitor. */
|
|
149
|
+
get personalized(): boolean {
|
|
150
|
+
return this.#personalized
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The `ctx.set` of one loader or action. `order` fixes where its headers land in the merge. */
|
|
154
|
+
scope(order: number): LoaderResponseControls {
|
|
155
|
+
return new ScopedResponseControls(this, order)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A scope's header record, created on first use. */
|
|
159
|
+
track(order: number): Record<string, string> {
|
|
160
|
+
const headers = Object.create(null) as Record<string, string>
|
|
161
|
+
this.#written ??= []
|
|
162
|
+
this.#written.push({ order, headers })
|
|
163
|
+
return headers
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The serving app's controls, marked as carrying per-visitor state. Throws once sealed. */
|
|
167
|
+
cookies(member: string): CoreResponseControls {
|
|
168
|
+
this.assertOpen(member)
|
|
169
|
+
this.personalize()
|
|
170
|
+
return this.#c.set
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Mark the response as one visitor's - a queued cookie, or a draft render - so no cache stores it. */
|
|
174
|
+
personalize(): void {
|
|
175
|
+
if (this.#personalized) return
|
|
176
|
+
this.#personalized = true
|
|
177
|
+
// A redirect or status render merges the serving app's header record, so this keeps those
|
|
178
|
+
// outcomes out of shared caches too (a 301 is cacheable by default).
|
|
179
|
+
this.#c.set.headers["cache-control"] = PRIVATE_NO_STORE
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
assertOpen(member: string): void {
|
|
183
|
+
if (!this.#sealed) return
|
|
184
|
+
throw new Error(
|
|
185
|
+
`[nifra/web] ctx.set.${member} was used after the response was committed. Write response headers and cookies before the loader or action returns - a deferred promise settles too late.`,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Close the controls: the response is decided. A later write - a deferred promise that settles after
|
|
191
|
+
* the loader returned - throws instead of being dropped without a trace.
|
|
192
|
+
*/
|
|
193
|
+
seal(): void {
|
|
194
|
+
if (this.#sealed) return
|
|
195
|
+
this.#sealed = true
|
|
196
|
+
if (this.#written === undefined) return
|
|
197
|
+
for (const entry of this.#written) Object.freeze(entry.headers)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Seal, validate, and merge every written header. Returns `undefined` when none was written.
|
|
202
|
+
* Throws on a name or value that cannot be sent, and on a name the framework or transport owns.
|
|
203
|
+
*/
|
|
204
|
+
commit(): Record<string, string> | undefined {
|
|
205
|
+
this.seal()
|
|
206
|
+
const written = this.#written
|
|
207
|
+
if (written === undefined) return undefined
|
|
208
|
+
if (written.length > 1) written.sort((a, b) => a.order - b.order)
|
|
209
|
+
let merged: Record<string, string> | undefined
|
|
210
|
+
for (const entry of written) {
|
|
211
|
+
for (const rawName of Object.keys(entry.headers)) {
|
|
212
|
+
const name = validHeaderName(rawName)
|
|
213
|
+
merged ??= Object.create(null) as Record<string, string>
|
|
214
|
+
merged[name] = validHeaderValue(name, entry.headers[rawName])
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return merged
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The committed headers for the document. Written headers make the document vary on the
|
|
222
|
+
* data-request header; a queued cookie makes it private, whatever a loader asked for.
|
|
223
|
+
*/
|
|
224
|
+
documentHeaders(): Record<string, string> | undefined {
|
|
225
|
+
const written = this.commit()
|
|
226
|
+
if (written !== undefined) written.vary = varyOnDataHeader(written.vary)
|
|
227
|
+
if (!this.#personalized) return written
|
|
228
|
+
const headers = written ?? (Object.create(null) as Record<string, string>)
|
|
229
|
+
headers["cache-control"] = PRIVATE_NO_STORE
|
|
230
|
+
return headers
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
class ScopedResponseControls implements LoaderResponseControls {
|
|
235
|
+
readonly #owner: PageResponseControls
|
|
236
|
+
readonly #order: number
|
|
237
|
+
#headers: Record<string, string> | undefined
|
|
238
|
+
|
|
239
|
+
constructor(owner: PageResponseControls, order: number) {
|
|
240
|
+
this.#owner = owner
|
|
241
|
+
this.#order = order
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
get headers(): Record<string, string> {
|
|
245
|
+
this.#owner.assertOpen("headers")
|
|
246
|
+
this.#headers ??= this.#owner.track(this.#order)
|
|
247
|
+
return this.#headers
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
cookie(name: string, value: string, options?: CookieOptions): void {
|
|
251
|
+
this.#owner.cookies("cookie()").cookie(name, value, options)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
deleteCookie(name: string, options?: Pick<CookieOptions, "path" | "domain">): void {
|
|
255
|
+
this.#owner.cookies("deleteCookie()").deleteCookie(name, options)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function validHeaderName(rawName: string): string {
|
|
260
|
+
if (!TOKEN.test(rawName)) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`[nifra/web] ctx.set.headers: ${JSON.stringify(rawName)} is not a valid header name`,
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
const name = rawName.toLowerCase()
|
|
266
|
+
const reason = reservedReason(name)
|
|
267
|
+
if (reason !== undefined) {
|
|
268
|
+
throw new Error(`[nifra/web] ctx.set.headers: "${name}" is reserved - ${reason}`)
|
|
269
|
+
}
|
|
270
|
+
return name
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// The message names the header only: a value can carry a token or a visitor's data.
|
|
274
|
+
function validHeaderValue(name: string, value: unknown): string {
|
|
275
|
+
if (typeof value !== "string") {
|
|
276
|
+
throw new Error(`[nifra/web] ctx.set.headers: the value of "${name}" must be a string`)
|
|
277
|
+
}
|
|
278
|
+
if (!FIELD_VALUE.test(value)) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`[nifra/web] ctx.set.headers: the value of "${name}" contains a character a header cannot carry (a line break, a control character, or one outside Latin-1)`,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
return value
|
|
284
|
+
}
|
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
/** Phantom brand key for {@link
|
|
2
|
-
declare const
|
|
1
|
+
/** Phantom brand key for {@link BackendOnly}. */
|
|
2
|
+
declare const BACKEND_ONLY_BRAND: unique symbol
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Type-level intent marker for a value that must only exist on the server - a secret, a DB handle, a
|
|
6
|
-
* server-only client. `
|
|
6
|
+
* server-only client. `BackendOnly<T>` is structurally `T` (the brand is an optional phantom field, so
|
|
7
7
|
* existing code keeps type-checking), but it advertises to readers + the compiler that the value is
|
|
8
8
|
* not meant to cross to the browser. It is **purely type-level** and erases at build - it does NOT,
|
|
9
9
|
* by itself, keep the value out of the client bundle.
|
|
10
10
|
*
|
|
11
|
-
* The enforcement is
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* chunk (the poison-import marker);
|
|
15
|
-
* - or name the file `*.server.ts` - the `.server` convention empties it in the client build.
|
|
16
|
-
*
|
|
17
|
-
* Use this brand to express the intent in the types; pair it with one of those runtime markers so a
|
|
18
|
-
* leak is caught at build time rather than shipping a secret to the client.
|
|
11
|
+
* The enforcement is the zone rules: code under `backend/` or in a `*.backend.ts` file never reaches a
|
|
12
|
+
* browser build, and a module that imports `@nifrajs/web/backend-only` fails every browser build that
|
|
13
|
+
* reaches it, wherever it lives.
|
|
19
14
|
*
|
|
20
15
|
* @example
|
|
21
|
-
* import "@nifrajs/web/
|
|
22
|
-
* import type {
|
|
23
|
-
* export const apiKey:
|
|
16
|
+
* import "@nifrajs/web/backend-only"
|
|
17
|
+
* import type { BackendOnly } from "@nifrajs/web"
|
|
18
|
+
* export const apiKey: BackendOnly<string> = process.env.SECRET_API_KEY!
|
|
24
19
|
*/
|
|
25
|
-
export type
|
|
20
|
+
export type BackendOnly<T> = T & { readonly [BACKEND_ONLY_BRAND]?: never }
|
|
26
21
|
|
|
27
22
|
/**
|
|
28
23
|
* Pre-hydration form guard - a tiny inline script flushed in `<head>` (it runs in the window between
|