@nifrajs/web 4.0.2 → 5.1.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/README.md +19 -0
- package/dist/build-vite.d.ts +19 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +31 -10
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +8 -10
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +90 -31
- package/dist/build.js.map +1 -1
- package/dist/cdn.d.ts +8 -0
- package/dist/cdn.d.ts.map +1 -1
- package/dist/cdn.js +14 -15
- package/dist/cdn.js.map +1 -1
- package/dist/client.d.ts +6 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +46 -18
- package/dist/client.js.map +1 -1
- package/dist/deferred.d.ts +2 -2
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +28 -7
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +24 -5
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-port.d.ts +3 -2
- package/dist/dev-port.d.ts.map +1 -1
- package/dist/dev-port.js +2 -6
- package/dist/dev-port.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +4 -14
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +7 -17
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +13 -1
- package/dist/dev.js.map +1 -1
- package/dist/draft.d.ts +8 -5
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +30 -13
- package/dist/draft.js.map +1 -1
- package/dist/fonts-google.d.ts.map +1 -1
- package/dist/fonts-google.js +7 -0
- package/dist/fonts-google.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +56 -29
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/disposable-dir.d.ts +8 -0
- package/dist/internal/disposable-dir.d.ts.map +1 -0
- package/dist/internal/disposable-dir.js +25 -0
- package/dist/internal/disposable-dir.js.map +1 -0
- package/dist/internal/entry-chunks.d.ts +9 -0
- package/dist/internal/entry-chunks.d.ts.map +1 -0
- package/dist/internal/entry-chunks.js +15 -0
- package/dist/internal/entry-chunks.js.map +1 -0
- package/dist/internal/evidence-traversal.d.ts +12 -0
- package/dist/internal/evidence-traversal.d.ts.map +1 -0
- package/dist/internal/evidence-traversal.js +48 -0
- package/dist/internal/evidence-traversal.js.map +1 -0
- package/dist/internal/head-merge.d.ts +6 -0
- package/dist/internal/head-merge.d.ts.map +1 -1
- package/dist/internal/head-merge.js +9 -0
- package/dist/internal/head-merge.js.map +1 -1
- package/dist/internal/js-string.d.ts +2 -0
- package/dist/internal/js-string.d.ts.map +1 -1
- package/dist/internal/js-string.js +5 -1
- package/dist/internal/js-string.js.map +1 -1
- package/dist/internal/mount-shadow.js +1 -1
- package/dist/internal/mount-shadow.js.map +1 -1
- package/dist/internal/output-guard.d.ts +7 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +80 -8
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/package-entries.d.ts +34 -0
- package/dist/internal/package-entries.d.ts.map +1 -0
- package/dist/internal/package-entries.js +324 -0
- package/dist/internal/package-entries.js.map +1 -0
- package/dist/internal/page-execution.d.ts +6 -0
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +437 -114
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +15 -2
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +8 -0
- package/dist/internal/private-env.d.ts.map +1 -1
- package/dist/internal/private-env.js +25 -10
- package/dist/internal/private-env.js.map +1 -1
- package/dist/internal/progressive-cookies.d.ts +14 -0
- package/dist/internal/progressive-cookies.d.ts.map +1 -0
- package/dist/internal/progressive-cookies.js +43 -0
- package/dist/internal/progressive-cookies.js.map +1 -0
- package/dist/internal/public-env.d.ts +2 -0
- package/dist/internal/public-env.d.ts.map +1 -0
- package/dist/internal/public-env.js +13 -0
- package/dist/internal/public-env.js.map +1 -0
- package/dist/internal/render-document.d.ts +5 -44
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +51 -63
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +2 -0
- package/dist/internal/response-controls.d.ts.map +1 -1
- package/dist/internal/response-controls.js +26 -16
- package/dist/internal/response-controls.js.map +1 -1
- package/dist/internal/revive-dates.d.ts +16 -0
- package/dist/internal/revive-dates.d.ts.map +1 -0
- package/dist/internal/revive-dates.js +32 -0
- package/dist/internal/revive-dates.js.map +1 -0
- package/dist/internal/route-path.d.ts +14 -0
- package/dist/internal/route-path.d.ts.map +1 -0
- package/dist/internal/route-path.js +19 -0
- package/dist/internal/route-path.js.map +1 -0
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +190 -20
- package/dist/internal/secret-scan.js.map +1 -1
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +33 -5
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/web-app.d.ts +25 -18
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +81 -24
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/wire-date.d.ts +3 -0
- package/dist/internal/wire-date.d.ts.map +1 -0
- package/dist/internal/wire-date.js +14 -0
- package/dist/internal/wire-date.js.map +1 -0
- package/dist/internal/without-query.d.ts +4 -0
- package/dist/internal/without-query.d.ts.map +1 -0
- package/dist/internal/without-query.js +9 -0
- package/dist/internal/without-query.js.map +1 -0
- package/dist/internal/zone-graph.d.ts +4 -0
- package/dist/internal/zone-graph.d.ts.map +1 -1
- package/dist/internal/zone-graph.js +71 -20
- package/dist/internal/zone-graph.js.map +1 -1
- package/dist/isr.d.ts +59 -3
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +311 -41
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +2 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +29 -3
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +37 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +4 -0
- package/dist/manifest.js.map +1 -1
- package/dist/open-resolved.d.ts +18 -0
- package/dist/open-resolved.d.ts.map +1 -0
- package/dist/open-resolved.js +62 -0
- package/dist/open-resolved.js.map +1 -0
- package/dist/plugins/kit.d.ts +1 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +26 -11
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +9 -1
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +84 -7
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +17 -21
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/plugins/zone-guard.d.ts.map +1 -1
- package/dist/plugins/zone-guard.js +44 -3
- package/dist/plugins/zone-guard.js.map +1 -1
- package/dist/prerender.d.ts +5 -1
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +41 -19
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +8 -1
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +17 -6
- package/dist/public-dir.js.map +1 -1
- package/dist/query.d.ts +13 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +89 -26
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +25 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +1 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +20 -9
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +9 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +47 -50
- package/dist/router.js.map +1 -1
- package/dist/seo.d.ts +49 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +48 -0
- package/dist/seo.js.map +1 -0
- package/dist/server-components/build-info.d.ts +3 -0
- package/dist/server-components/build-info.d.ts.map +1 -0
- package/dist/server-components/build-info.js +7 -0
- package/dist/server-components/build-info.js.map +1 -0
- package/dist/server-components/build-target.d.ts +12 -0
- package/dist/server-components/build-target.d.ts.map +1 -0
- package/dist/server-components/build-target.js +153 -0
- package/dist/server-components/build-target.js.map +1 -0
- package/dist/server-components/build.d.ts +24 -0
- package/dist/server-components/build.d.ts.map +1 -0
- package/dist/server-components/build.js +366 -0
- package/dist/server-components/build.js.map +1 -0
- package/dist/server-components/bundle-proof.d.ts +3 -0
- package/dist/server-components/bundle-proof.d.ts.map +1 -0
- package/dist/server-components/bundle-proof.js +23 -0
- package/dist/server-components/bundle-proof.js.map +1 -0
- package/dist/server-components/client.d.ts +46 -0
- package/dist/server-components/client.d.ts.map +1 -0
- package/dist/server-components/client.js +594 -0
- package/dist/server-components/client.js.map +1 -0
- package/dist/server-components/definition.d.ts +11 -0
- package/dist/server-components/definition.d.ts.map +1 -0
- package/dist/server-components/definition.js +94 -0
- package/dist/server-components/definition.js.map +1 -0
- package/dist/server-components/dev-http.d.ts +5 -0
- package/dist/server-components/dev-http.d.ts.map +1 -0
- package/dist/server-components/dev-http.js +71 -0
- package/dist/server-components/dev-http.js.map +1 -0
- package/dist/server-components/error.d.ts +9 -0
- package/dist/server-components/error.d.ts.map +1 -0
- package/dist/server-components/error.js +24 -0
- package/dist/server-components/error.js.map +1 -0
- package/dist/server-components/frames.d.ts +7 -0
- package/dist/server-components/frames.d.ts.map +1 -0
- package/dist/server-components/frames.js +169 -0
- package/dist/server-components/frames.js.map +1 -0
- package/dist/server-components/html-policy.d.ts +6 -0
- package/dist/server-components/html-policy.d.ts.map +1 -0
- package/dist/server-components/html-policy.js +75 -0
- package/dist/server-components/html-policy.js.map +1 -0
- package/dist/server-components/html.d.ts +4 -0
- package/dist/server-components/html.d.ts.map +1 -0
- package/dist/server-components/html.js +67 -0
- package/dist/server-components/html.js.map +1 -0
- package/dist/server-components/islands.d.ts +44 -0
- package/dist/server-components/islands.d.ts.map +1 -0
- package/dist/server-components/islands.js +42 -0
- package/dist/server-components/islands.js.map +1 -0
- package/dist/server-components/native.d.ts +4 -0
- package/dist/server-components/native.d.ts.map +1 -0
- package/dist/server-components/native.js +81 -0
- package/dist/server-components/native.js.map +1 -0
- package/dist/server-components/portal.d.ts +6 -0
- package/dist/server-components/portal.d.ts.map +1 -0
- package/dist/server-components/portal.js +35 -0
- package/dist/server-components/portal.js.map +1 -0
- package/dist/server-components/props.d.ts +3 -0
- package/dist/server-components/props.d.ts.map +1 -0
- package/dist/server-components/props.js +14 -0
- package/dist/server-components/props.js.map +1 -0
- package/dist/server-components/protocol.d.ts +44 -0
- package/dist/server-components/protocol.d.ts.map +1 -0
- package/dist/server-components/protocol.js +139 -0
- package/dist/server-components/protocol.js.map +1 -0
- package/dist/server-components/range.d.ts +24 -0
- package/dist/server-components/range.d.ts.map +1 -0
- package/dist/server-components/range.js +263 -0
- package/dist/server-components/range.js.map +1 -0
- package/dist/server-components/registry.d.ts +6 -0
- package/dist/server-components/registry.d.ts.map +1 -0
- package/dist/server-components/registry.js +30 -0
- package/dist/server-components/registry.js.map +1 -0
- package/dist/server-components/response.d.ts +4 -0
- package/dist/server-components/response.d.ts.map +1 -0
- package/dist/server-components/response.js +32 -0
- package/dist/server-components/response.js.map +1 -0
- package/dist/server-components/router-integration.d.ts +9 -0
- package/dist/server-components/router-integration.d.ts.map +1 -0
- package/dist/server-components/router-integration.js +29 -0
- package/dist/server-components/router-integration.js.map +1 -0
- package/dist/server-components/runtime.d.ts +14 -0
- package/dist/server-components/runtime.d.ts.map +1 -0
- package/dist/server-components/runtime.js +276 -0
- package/dist/server-components/runtime.js.map +1 -0
- package/dist/server-components/scheduler.d.ts +14 -0
- package/dist/server-components/scheduler.d.ts.map +1 -0
- package/dist/server-components/scheduler.js +74 -0
- package/dist/server-components/scheduler.js.map +1 -0
- package/dist/server-components/schema.d.ts +4 -0
- package/dist/server-components/schema.d.ts.map +1 -0
- package/dist/server-components/schema.js +77 -0
- package/dist/server-components/schema.js.map +1 -0
- package/dist/server-components/server-entry.d.ts +12 -0
- package/dist/server-components/server-entry.d.ts.map +1 -0
- package/dist/server-components/server-entry.js +90 -0
- package/dist/server-components/server-entry.js.map +1 -0
- package/dist/server-components/source.d.ts +15 -0
- package/dist/server-components/source.d.ts.map +1 -0
- package/dist/server-components/source.js +269 -0
- package/dist/server-components/source.js.map +1 -0
- package/dist/server-components/state.d.ts +4 -0
- package/dist/server-components/state.d.ts.map +1 -0
- package/dist/server-components/state.js +123 -0
- package/dist/server-components/state.js.map +1 -0
- package/dist/server-components/stream.d.ts +6 -0
- package/dist/server-components/stream.d.ts.map +1 -0
- package/dist/server-components/stream.js +70 -0
- package/dist/server-components/stream.js.map +1 -0
- package/dist/server-components/trusted-html.d.ts +3 -0
- package/dist/server-components/trusted-html.d.ts.map +1 -0
- package/dist/server-components/trusted-html.js +14 -0
- package/dist/server-components/trusted-html.js.map +1 -0
- package/dist/server-components/types.d.ts +107 -0
- package/dist/server-components/types.d.ts.map +1 -0
- package/dist/server-components/types.js +2 -0
- package/dist/server-components/types.js.map +1 -0
- package/dist/server-components/typescript.d.ts +11 -0
- package/dist/server-components/typescript.d.ts.map +1 -0
- package/dist/server-components/typescript.js +28 -0
- package/dist/server-components/typescript.js.map +1 -0
- package/dist/server-components/value.d.ts +5 -0
- package/dist/server-components/value.d.ts.map +1 -0
- package/dist/server-components/value.js +77 -0
- package/dist/server-components/value.js.map +1 -0
- package/dist/server-components/vite.d.ts +12 -0
- package/dist/server-components/vite.d.ts.map +1 -0
- package/dist/server-components/vite.js +197 -0
- package/dist/server-components/vite.js.map +1 -0
- package/dist/server-components.d.ts +5 -0
- package/dist/server-components.d.ts.map +1 -0
- package/dist/server-components.js +4 -0
- package/dist/server-components.js.map +1 -0
- package/dist/service-worker.d.ts +8 -1
- package/dist/service-worker.d.ts.map +1 -1
- package/dist/service-worker.js +20 -6
- package/dist/service-worker.js.map +1 -1
- package/dist/vite.d.ts +12 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +7 -1
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +64 -2
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +331 -28
- package/dist/zones.js.map +1 -1
- package/package.json +102 -4
- package/src/build-vite.ts +31 -9
- package/src/build.ts +109 -31
- package/src/cdn.ts +18 -12
- package/src/client.ts +48 -17
- package/src/deferred.ts +27 -9
- package/src/dev-feed.ts +24 -5
- package/src/dev-port.ts +4 -10
- package/src/dev-session.ts +4 -13
- package/src/dev.ts +29 -1
- package/src/draft.ts +35 -14
- package/src/fonts-google.ts +10 -0
- package/src/index.ts +5 -6
- package/src/internal/codegen.ts +60 -29
- package/src/internal/disposable-dir.ts +24 -0
- package/src/internal/entry-chunks.ts +18 -0
- package/src/internal/evidence-traversal.ts +57 -0
- package/src/internal/head-merge.ts +16 -0
- package/src/internal/js-string.ts +6 -1
- package/src/internal/mount-shadow.ts +1 -1
- package/src/internal/output-guard.ts +88 -7
- package/src/internal/package-entries.ts +331 -0
- package/src/internal/page-execution.ts +573 -114
- package/src/internal/parity.ts +15 -2
- package/src/internal/private-env.ts +27 -13
- package/src/internal/progressive-cookies.ts +60 -0
- package/src/internal/public-env.ts +14 -0
- package/src/internal/render-document.ts +66 -89
- package/src/internal/response-controls.ts +31 -14
- package/src/internal/revive-dates.ts +35 -0
- package/src/internal/route-path.ts +24 -0
- package/src/internal/secret-scan.ts +190 -18
- package/src/internal/server-boundary.ts +29 -5
- package/src/internal/web-app.ts +126 -42
- package/src/internal/wire-date.ts +16 -0
- package/src/internal/without-query.ts +8 -0
- package/src/internal/zone-graph.ts +78 -25
- package/src/isr.ts +364 -45
- package/src/llms-txt.ts +32 -3
- package/src/manifest.ts +38 -4
- package/src/open-resolved.ts +64 -0
- package/src/plugins/kit.ts +31 -11
- package/src/plugins/vite-leak-guard.ts +104 -8
- package/src/plugins/vite-zone-guard.ts +21 -19
- package/src/plugins/zone-guard.ts +41 -2
- package/src/prerender.ts +39 -18
- package/src/public-dir.ts +27 -8
- package/src/query.ts +107 -26
- package/src/render-seam.ts +30 -2
- package/src/route-types.ts +34 -7
- package/src/router.ts +66 -59
- package/src/seo.ts +68 -0
- package/src/server-components/build-info.ts +11 -0
- package/src/server-components/build-target.ts +194 -0
- package/src/server-components/build.ts +419 -0
- package/src/server-components/bundle-proof.ts +33 -0
- package/src/server-components/client.ts +663 -0
- package/src/server-components/definition.ts +125 -0
- package/src/server-components/dev-http.ts +75 -0
- package/src/server-components/error.ts +24 -0
- package/src/server-components/frames.ts +181 -0
- package/src/server-components/html-policy.ts +94 -0
- package/src/server-components/html.ts +81 -0
- package/src/server-components/islands.ts +111 -0
- package/src/server-components/native.ts +101 -0
- package/src/server-components/portal.ts +42 -0
- package/src/server-components/props.ts +19 -0
- package/src/server-components/protocol.ts +167 -0
- package/src/server-components/range.ts +313 -0
- package/src/server-components/registry.ts +34 -0
- package/src/server-components/response.ts +44 -0
- package/src/server-components/router-integration.ts +40 -0
- package/src/server-components/runtime.ts +376 -0
- package/src/server-components/scheduler.ts +84 -0
- package/src/server-components/schema.ts +69 -0
- package/src/server-components/server-entry.ts +98 -0
- package/src/server-components/source.ts +324 -0
- package/src/server-components/state.ts +143 -0
- package/src/server-components/stream.ts +71 -0
- package/src/server-components/trusted-html.ts +21 -0
- package/src/server-components/types.ts +138 -0
- package/src/server-components/typescript.ts +39 -0
- package/src/server-components/value.ts +82 -0
- package/src/server-components/vite.ts +242 -0
- package/src/server-components.ts +32 -0
- package/src/service-worker.ts +24 -7
- package/src/vite.ts +8 -1
- package/src/zones.ts +418 -32
|
@@ -148,11 +148,12 @@ const JWT = /\beyJ[A-Za-z0-9_-]{8,}\.(eyJ[A-Za-z0-9_-]{8,})\.[A-Za-z0-9_-]{16,}/
|
|
|
148
148
|
const PLACEHOLDER =
|
|
149
149
|
/^(?:user(?:name)?|pass(?:word)?|pwd|secret|admin|root|guest|test|demo|example|changeme|change-me|xxx+|\*+|\.\.\.+|my-?password|your-?password|<[^>]*>|\$\{[^}]*\}|\{\{[^}]*\}\}|%[A-Za-z_]+%?)$/i
|
|
150
150
|
|
|
151
|
-
/** A literal assigned to a name: `apiKey: "..."`, `const secret = '...'
|
|
152
|
-
* interpolation are not literals. The lookbehind starts a name only where
|
|
153
|
-
* match from inside a long word run backtracks the rest of that run,
|
|
151
|
+
/** A literal assigned to a name: `apiKey: "..."`, `const secret = '...'`, `"x-api-key": "..."`.
|
|
152
|
+
* Template literals with an interpolation are not literals. The lookbehind starts a name only where
|
|
153
|
+
* an identifier starts: a match from inside a long word run backtracks the rest of that run,
|
|
154
|
+
* quadratic in its length. A hyphenated name starts only right after a quote, for the same reason. */
|
|
154
155
|
const ASSIGNED =
|
|
155
|
-
/(?<![\w$])([A-Za-z_$][\w$]*)(["']?)\s*([:=]|\?\?=|\|\|=)\s*(?:"([^"\\\r\n]{16,256})"|'([^'\\\r\n]{16,256})'|`([^`\\$\r\n]{16,256})`)/g
|
|
156
|
+
/(?<![\w$])((?<=["'])[A-Za-z_$][\w$-]*(?=["'])|[A-Za-z_$][\w$]*)(["']?)\s*([:=]|\?\?=|\|\|=)\s*(?:"([^"\\\r\n]{16,256})"|'([^'\\\r\n]{16,256})'|`([^`\\$\r\n]{16,256})`)/g
|
|
156
157
|
|
|
157
158
|
/** Whether a match's name is what the literal is assigned to. A quoted name must be the whole string,
|
|
158
159
|
* not the tail of one (`? "new-password" : "current-password"`), and a key is never preceded by a
|
|
@@ -168,9 +169,33 @@ function namesTheTarget(text: string, start: number, quote: string, operator: st
|
|
|
168
169
|
return text[before] !== "?" && text[before] !== "."
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
/** A quoted name and a literal written as a pair: `headers.set("x-api-key", "...")`,
|
|
173
|
+
* `new Headers([["x-api-key", "..."]])`. The last group is what follows the literal. */
|
|
174
|
+
const PAIRED =
|
|
175
|
+
/(["'])([A-Za-z_$][\w$-]*)\1\s*,\s*(?:"([^"\\\r\n]{16,256})"|'([^'\\\r\n]{16,256})'|`([^`\\$\r\n]{16,256})`)\s*([\]),]?)/g
|
|
176
|
+
|
|
177
|
+
/** A value spelled as words or as a constant's name: another field (`["password",
|
|
178
|
+
* "passwordConfirmation"]`), an environment variable, a stand-in token (`"Bearer not-a-token"`). */
|
|
179
|
+
const NAME_LIKE =
|
|
180
|
+
/^(?:[A-Za-z][a-z]*(?:[A-Z][a-z]+|[_.-][A-Za-z][a-z]*)+|[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)+)$/
|
|
181
|
+
|
|
182
|
+
/** Whether a pair is a whole two-element array, or the first two arguments of `.set(` or `.append(`:
|
|
183
|
+
* an element in a longer list is not a pair. */
|
|
184
|
+
function opensAPair(text: string, quoteAt: number, after: string): boolean {
|
|
185
|
+
let before = quoteAt - 1
|
|
186
|
+
while (before >= 0 && /\s/.test(text[before] ?? "")) before--
|
|
187
|
+
if (text[before] === "[") return after === "]"
|
|
188
|
+
if (text[before] !== "(" || (after !== ")" && after !== ",")) return false
|
|
189
|
+
return /\.(?:set|append)$/.test(text.slice(Math.max(0, before - 7), before))
|
|
190
|
+
}
|
|
191
|
+
|
|
171
192
|
const EXTRA_SECRET_NAME =
|
|
172
193
|
/(?:accesskey|signingkey|encryptionkey|masterkey|serviceaccountkey|connectionstring)$/
|
|
173
194
|
|
|
195
|
+
/** Header names whose value puts a scheme before the credential: `"Bearer <token>"`. */
|
|
196
|
+
const AUTHORIZATION_NAME = /^(?:proxy)?authorization$/
|
|
197
|
+
const AUTHORIZATION_VALUE = /^(?:(bearer)|basic) +(\S+)$/i
|
|
198
|
+
|
|
174
199
|
/** Values that look like keys but are public by design. */
|
|
175
200
|
const PUBLIC_VALUE =
|
|
176
201
|
/^(?:AIza[0-9A-Za-z_-]{35}|pk_(?:live|test)_[0-9A-Za-z]+|6L[0-9A-Za-z_-]{38}|G-[A-Z0-9]{6,}|UA-\d+-\d+)$/
|
|
@@ -200,6 +225,32 @@ function looksRandom(value: string, minLength: number): boolean {
|
|
|
200
225
|
return classes >= 2 && entropy(value) >= 3.5
|
|
201
226
|
}
|
|
202
227
|
|
|
228
|
+
/** A password the scanned text spells as a placeholder or a dev default. */
|
|
229
|
+
function placeholderPassword(user: string, password: string): boolean {
|
|
230
|
+
// nifra-expect NF-S002: a password equal to its user name is a placeholder in the text being
|
|
231
|
+
// scanned; nothing is authenticated here.
|
|
232
|
+
return PLACEHOLDER.test(password) || password === user || /[${}<>*]/.test(password)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The credential an `Authorization` literal carries: a Bearer token, or a Basic `user:password`
|
|
236
|
+
* that is not a placeholder. */
|
|
237
|
+
function authorizationCredential(value: string): string | undefined {
|
|
238
|
+
const found = AUTHORIZATION_VALUE.exec(value)
|
|
239
|
+
if (found === null) return undefined
|
|
240
|
+
const credential = found[2] ?? ""
|
|
241
|
+
if (found[1] !== undefined) return NAME_LIKE.test(credential) ? undefined : credential
|
|
242
|
+
let decoded: string
|
|
243
|
+
try {
|
|
244
|
+
decoded = atob(credential)
|
|
245
|
+
} catch {
|
|
246
|
+
return undefined
|
|
247
|
+
}
|
|
248
|
+
const colon = decoded.indexOf(":")
|
|
249
|
+
if (colon === -1 || placeholderPassword(decoded.slice(0, colon), decoded.slice(colon + 1)))
|
|
250
|
+
return undefined
|
|
251
|
+
return credential
|
|
252
|
+
}
|
|
253
|
+
|
|
203
254
|
function jwtRole(payload: string): unknown {
|
|
204
255
|
try {
|
|
205
256
|
const json = atob(payload.replaceAll("-", "+").replaceAll("_", "/"))
|
|
@@ -231,11 +282,7 @@ function formatMatches(text: string): Match[] {
|
|
|
231
282
|
matches.push({ rule: "private-key", index: found.index, text: found[0], what: "private key" })
|
|
232
283
|
}
|
|
233
284
|
for (const found of text.matchAll(CREDENTIAL_URL)) {
|
|
234
|
-
|
|
235
|
-
const password = found[2] ?? ""
|
|
236
|
-
// @nifra-gate-reviewed: a password equal to its user name is a placeholder in the text being
|
|
237
|
-
// scanned; nothing is authenticated here.
|
|
238
|
-
if (PLACEHOLDER.test(password) || password === user || /[${}<>*]/.test(password)) continue
|
|
285
|
+
if (placeholderPassword(found[1] ?? "", found[2] ?? "")) continue
|
|
239
286
|
matches.push({
|
|
240
287
|
rule: "credential-url",
|
|
241
288
|
index: found.index,
|
|
@@ -246,21 +293,146 @@ function formatMatches(text: string): Match[] {
|
|
|
246
293
|
return matches
|
|
247
294
|
}
|
|
248
295
|
|
|
296
|
+
/** Words that mark a whole `_`/`-` segment of a value as a stand-in: `your_signing_secret`. */
|
|
297
|
+
const PLACEHOLDER_SEGMENT =
|
|
298
|
+
/^(?:your|example|placeholder|sample|dummy|changeme|replace|generated|x{3,})$/
|
|
299
|
+
|
|
300
|
+
const placeholderSegments = (value: string): boolean =>
|
|
301
|
+
value
|
|
302
|
+
.toLowerCase()
|
|
303
|
+
.split(/[_-]/)
|
|
304
|
+
.some((segment) => PLACEHOLDER_SEGMENT.test(segment))
|
|
305
|
+
|
|
306
|
+
/** A word-cased label: letters and marks, hyphen-joined words, an acronym. No digits, no runs of
|
|
307
|
+
* mixed case, no credential punctuation. */
|
|
308
|
+
const LABEL = /^(?:\p{Lu}{1,4}|\p{Lu}?[\p{Ll}\p{M}]+)(?:-(?:\p{Lu}{1,4}|\p{Lu}?[\p{Ll}\p{M}]+))*$/u
|
|
309
|
+
|
|
310
|
+
/** How many string values a catalog holds at least, and the share of them that is prose. */
|
|
311
|
+
const CATALOG_VALUES = 10
|
|
312
|
+
|
|
313
|
+
/** Where a `/` starts a regular expression: after anything but a value's end. */
|
|
314
|
+
const VALUE_END = /[\w$)\]}"'`]/
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The spans of top-level object literals that read as message catalogs: at least
|
|
318
|
+
* {@link CATALOG_VALUES} string property values, most of them prose with whitespace. A lexer, not a
|
|
319
|
+
* parser, so a build scans without TypeScript; strings, comments and regular expressions are skipped.
|
|
320
|
+
*/
|
|
321
|
+
function catalogSpans(text: string): Array<readonly [number, number]> {
|
|
322
|
+
const spans: Array<readonly [number, number]> = []
|
|
323
|
+
let depth = 0
|
|
324
|
+
let start = 0
|
|
325
|
+
let values = 0
|
|
326
|
+
let prose = 0
|
|
327
|
+
// Whether the open top-level brace starts an object literal, not a block such as a function body.
|
|
328
|
+
let literal = false
|
|
329
|
+
let last = ""
|
|
330
|
+
let i = 0
|
|
331
|
+
while (i < text.length) {
|
|
332
|
+
const c = text.charAt(i)
|
|
333
|
+
if (c === "/" && text[i + 1] === "/") {
|
|
334
|
+
const end = text.indexOf("\n", i)
|
|
335
|
+
i = end === -1 ? text.length : end
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
339
|
+
const end = text.indexOf("*/", i + 2)
|
|
340
|
+
i = end === -1 ? text.length : end + 2
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
if (c === '"' || c === "'" || c === "`" || (c === "/" && !VALUE_END.test(last))) {
|
|
344
|
+
let j = i + 1
|
|
345
|
+
let inClass = false
|
|
346
|
+
for (; j < text.length; j++) {
|
|
347
|
+
const d = text[j]
|
|
348
|
+
if (d === "\\") {
|
|
349
|
+
j++
|
|
350
|
+
continue
|
|
351
|
+
}
|
|
352
|
+
if (c !== "`" && d === "\n") break
|
|
353
|
+
if (c !== "/") {
|
|
354
|
+
if (d === c) break
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
if (d === "[") inClass = true
|
|
358
|
+
else if (d === "]") inClass = false
|
|
359
|
+
else if (d === "/" && !inClass) break
|
|
360
|
+
}
|
|
361
|
+
// A string after `:` is a property's value.
|
|
362
|
+
if (depth > 0 && c !== "/" && c !== "`" && last === ":") {
|
|
363
|
+
values++
|
|
364
|
+
if (/\s/.test(text.slice(i + 1, j))) prose++
|
|
365
|
+
}
|
|
366
|
+
last = c
|
|
367
|
+
i = j + 1
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
if (c === "{") {
|
|
371
|
+
if (depth === 0) {
|
|
372
|
+
start = i
|
|
373
|
+
values = 0
|
|
374
|
+
prose = 0
|
|
375
|
+
const word = /([\w$]+)\s*$/.exec(text.slice(Math.max(0, i - 16), i))?.[1]
|
|
376
|
+
literal = last === "" || "=(,:[?".includes(last) || word === "default"
|
|
377
|
+
}
|
|
378
|
+
depth++
|
|
379
|
+
} else if (c === "}" && depth > 0 && --depth === 0) {
|
|
380
|
+
if (literal && values >= CATALOG_VALUES && prose * 2 > values) spans.push([start, i + 1])
|
|
381
|
+
}
|
|
382
|
+
if (!/\s/.test(c)) last = c
|
|
383
|
+
i++
|
|
384
|
+
}
|
|
385
|
+
return spans
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The credential a literal given to `name` carries, when the name is secret-like and the literal
|
|
389
|
+
* looks random. */
|
|
390
|
+
function credentialOf(name: string, value: string): string | undefined {
|
|
391
|
+
const key = name.toLowerCase().replace(/[-_]/g, "")
|
|
392
|
+
const header = AUTHORIZATION_NAME.test(key)
|
|
393
|
+
if (!header && !isSensitiveFieldName(name) && !EXTRA_SECRET_NAME.test(key)) return undefined
|
|
394
|
+
const secret = header ? authorizationCredential(value) : value
|
|
395
|
+
if (
|
|
396
|
+
secret === undefined ||
|
|
397
|
+
PUBLIC_VALUE.test(secret) ||
|
|
398
|
+
PLACEHOLDER.test(secret) ||
|
|
399
|
+
placeholderSegments(secret) ||
|
|
400
|
+
!looksRandom(secret, 16)
|
|
401
|
+
)
|
|
402
|
+
return undefined
|
|
403
|
+
return secret
|
|
404
|
+
}
|
|
405
|
+
|
|
249
406
|
function assignedMatches(text: string): Match[] {
|
|
250
407
|
const matches: Match[] = []
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!
|
|
255
|
-
|
|
256
|
-
|
|
408
|
+
let catalogs: ReadonlyArray<readonly [number, number]> | undefined
|
|
409
|
+
/** A label in a message catalog: `password: "Lykilorð"` names the field in another language. */
|
|
410
|
+
const catalogLabel = (value: string, valueAt: number): boolean => {
|
|
411
|
+
if (!LABEL.test(value)) return false
|
|
412
|
+
catalogs ??= catalogSpans(text)
|
|
413
|
+
return catalogs.some(([start, end]) => valueAt > start && valueAt < end)
|
|
414
|
+
}
|
|
415
|
+
const add = (name: string, value: string, valueAt: number): void => {
|
|
416
|
+
const secret = credentialOf(name, value)
|
|
417
|
+
if (secret === undefined || catalogLabel(value, valueAt)) return
|
|
257
418
|
matches.push({
|
|
258
419
|
rule: "assigned-secret",
|
|
259
|
-
|
|
260
|
-
|
|
420
|
+
// The credential is the value's tail: a header value's scheme is not part of it.
|
|
421
|
+
index: valueAt + value.length - secret.length,
|
|
422
|
+
text: secret,
|
|
261
423
|
what: `literal assigned to "${name}"`,
|
|
262
424
|
})
|
|
263
425
|
}
|
|
426
|
+
for (const found of text.matchAll(ASSIGNED)) {
|
|
427
|
+
if (!namesTheTarget(text, found.index, found[2] ?? "", found[3] ?? "")) continue
|
|
428
|
+
const value = found[4] ?? found[5] ?? found[6] ?? ""
|
|
429
|
+
add(found[1] ?? "", value, found.index + found[0].lastIndexOf(value))
|
|
430
|
+
}
|
|
431
|
+
for (const found of text.matchAll(PAIRED)) {
|
|
432
|
+
const value = found[3] ?? found[4] ?? found[5] ?? ""
|
|
433
|
+
if (NAME_LIKE.test(value) || !opensAPair(text, found.index, found[6] ?? "")) continue
|
|
434
|
+
add(found[2] ?? "", value, found.index + found[0].lastIndexOf(value))
|
|
435
|
+
}
|
|
264
436
|
return matches
|
|
265
437
|
}
|
|
266
438
|
|
|
@@ -508,7 +680,7 @@ export function createRedactor(
|
|
|
508
680
|
for (const { what, pattern } of KEY_FORMATS) out = out.replace(pattern, `[redacted:${what}]`)
|
|
509
681
|
out = out.replace(JWT, "[redacted:JWT]")
|
|
510
682
|
out = out.replace(CREDENTIAL_URL, (match, user: string, password: string) =>
|
|
511
|
-
//
|
|
683
|
+
// nifra-expect NF-S002: the same placeholder test as the scan; nothing is authenticated here.
|
|
512
684
|
PLACEHOLDER.test(password) || password === user
|
|
513
685
|
? match
|
|
514
686
|
: match.replace(`:${password}@`, ":[redacted]@"),
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
export const SERVER_FN_MODULE = /\.fn(\.[cm]?[jt]sx?)?$/
|
|
15
15
|
|
|
16
16
|
import { jsStringLiteral } from "./js-string.ts"
|
|
17
|
+
import { DATE_TAG, REVIVE_DATES_SOURCE } from "./revive-dates.ts"
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------------------------------
|
|
19
20
|
// `.fn` - turn a server-function module into the client stubs that call it.
|
|
@@ -50,15 +51,18 @@ const DECLARATION =
|
|
|
50
51
|
const ANY_CALL = /\bserverFn\s*[<(]/g
|
|
51
52
|
/** Strip line and block comments by forward index scan, so prose cannot register as code. Not a
|
|
52
53
|
* regex: every regex shape for comment stripping either backtracks polynomially on adversarial
|
|
53
|
-
* source or is opaque enough that reviewers cannot tell.
|
|
54
|
-
*
|
|
55
|
-
* swallows the rest of the source, matching a real parser. */
|
|
54
|
+
* source or is opaque enough that reviewers cannot tell. String literals are stepped over, so a
|
|
55
|
+
* `"image/*"` cannot open a comment; a `//` preceded by `:` is kept (a URL outside a string); an
|
|
56
|
+
* unterminated block comment swallows the rest of the source, matching a real parser. */
|
|
56
57
|
function stripComments(source: string): string {
|
|
57
58
|
const parts: string[] = []
|
|
58
59
|
let keepFrom = 0
|
|
59
60
|
let i = 0
|
|
60
61
|
while (i < source.length) {
|
|
61
|
-
|
|
62
|
+
const char = source[i]
|
|
63
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
64
|
+
i = stringEnd(source, i)
|
|
65
|
+
} else if (source.startsWith("/*", i)) {
|
|
62
66
|
const close = source.indexOf("*/", i + 2)
|
|
63
67
|
parts.push(source.slice(keepFrom, i))
|
|
64
68
|
i = close === -1 ? source.length : close + 2
|
|
@@ -76,6 +80,21 @@ function stripComments(source: string): string {
|
|
|
76
80
|
return parts.join("")
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/** The index just past the string literal opening at `start`. A quote or apostrophe string also ends
|
|
84
|
+
* at a line break, so a stray one (JSX text, a regex literal) misreads one line at most. */
|
|
85
|
+
function stringEnd(source: string, start: number): number {
|
|
86
|
+
const quote = source[start]
|
|
87
|
+
let i = start + 1
|
|
88
|
+
while (i < source.length) {
|
|
89
|
+
const char = source[i]
|
|
90
|
+
if (char === "\\") i += 2
|
|
91
|
+
else if (char === quote) return i + 1
|
|
92
|
+
else if (char === "\n" && quote !== "`") return i
|
|
93
|
+
else i++
|
|
94
|
+
}
|
|
95
|
+
return source.length
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
/**
|
|
80
99
|
* The namespace a `*.fn.ts` module is mounted under: its filename without the `.fn` suffix.
|
|
81
100
|
*
|
|
@@ -143,9 +162,14 @@ async function __nifraCall(name, input) {
|
|
|
143
162
|
}
|
|
144
163
|
throw new Error(\`[nifra/fn] \${name} failed: \${response.status}\${detail ? \` \${detail}\` : ""}\`)
|
|
145
164
|
}
|
|
146
|
-
|
|
165
|
+
if (response.status === 204) return undefined
|
|
166
|
+
const text = await response.text()
|
|
167
|
+
const value = JSON.parse(text)
|
|
168
|
+
return text.includes('"${DATE_TAG}"') ? __nifraDates(value) : value
|
|
147
169
|
}
|
|
148
170
|
|
|
171
|
+
${REVIVE_DATES_SOURCE}
|
|
172
|
+
|
|
149
173
|
${calls}
|
|
150
174
|
`
|
|
151
175
|
}
|
package/src/internal/web-app.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { emitRequestErrorLog } from "@nifrajs/core/edge-kit"
|
|
2
|
+
import { jsonLogger } from "@nifrajs/core/logger"
|
|
1
3
|
import {
|
|
2
4
|
type BackendEvidenceProvider,
|
|
3
5
|
type BackendMount,
|
|
@@ -16,12 +18,15 @@ import { CACHE_CHANNEL } from "../isr.ts"
|
|
|
16
18
|
import { generateLlmsTxt } from "../llms-txt.ts"
|
|
17
19
|
import type { Manifest } from "../manifest.ts"
|
|
18
20
|
import type { RenderAdapter } from "../render-seam.ts"
|
|
19
|
-
import { PRERENDERED_LIST_PATH } from "../router.ts"
|
|
21
|
+
import { createMatcher, PRERENDERED_LIST_PATH } from "../router.ts"
|
|
22
|
+
import type { EvidenceAncestry } from "./evidence-traversal.ts"
|
|
20
23
|
import { formatShadowedPages, shadowedPages } from "./mount-shadow.ts"
|
|
21
24
|
import { createPageRequestExecutor, type NonceResolver } from "./page-execution.ts"
|
|
25
|
+
import { requestPattern } from "./route-path.ts"
|
|
22
26
|
|
|
23
27
|
export type { NonceResolver } from "./page-execution.ts"
|
|
24
28
|
export interface CreateWebAppOptions<Env = unknown> {
|
|
29
|
+
readonly serverComponents?: import("../server-components/types.ts").ServerComponentRuntime
|
|
25
30
|
readonly adapter: RenderAdapter
|
|
26
31
|
readonly manifest: Manifest
|
|
27
32
|
/** URL of the built client entry (module script) injected into every page. */
|
|
@@ -78,9 +83,10 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
78
83
|
*
|
|
79
84
|
* **Why the timing matters.** `beforeHandle`, `afterHandle`, `around`, `derive`/`decorate`, and
|
|
80
85
|
* `onError` are snapshotted into each route AS IT IS DECLARED, and `createWebApp` declares every
|
|
81
|
-
* page (plus the `/*` catch-all) before it returns - so a caller's `app.use(…)` afterwards
|
|
82
|
-
* them to
|
|
83
|
-
*
|
|
86
|
+
* page (plus the `/*` catch-all) before it returns - so a caller's `app.use(…)` afterwards would
|
|
87
|
+
* bind them to no page, and outside production it throws (`HOOK_AFTER_PAGES`). `requestId()` is
|
|
88
|
+
* exactly that case: it is a `derive`, so applied late it would leave every page without
|
|
89
|
+
* `c.requestId`. A `group()` is its own scope and stays allowed. Route assurance evidence is order-scoped the same
|
|
84
90
|
* way, so a late `securityHeaders()` leaves the pages unable to PROVE the header to `nifra assure`
|
|
85
91
|
* even though it does still set it.
|
|
86
92
|
*
|
|
@@ -101,10 +107,11 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
101
107
|
* per-route via `@nifrajs/client`'s `createRoutes`). Opaque to the core.
|
|
102
108
|
*
|
|
103
109
|
* **Auto-mount.** Every `inProcessClient(backend)` exposes the symbol-keyed platform-aware backend
|
|
104
|
-
* mount interface from `@nifrajs/core/mount`; `createWebApp` also serves
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
110
|
+
* mount interface from `@nifrajs/core/mount`; given an {@link apiPrefix}, `createWebApp` also serves
|
|
111
|
+
* that backend over HTTP there: a request whose pathname starts with the prefix is dispatched before
|
|
112
|
+
* page routing with the same `env`/`waitUntil` platform context, and the backend's `Response` is
|
|
113
|
+
* returned untouched. The mount runs in `nifra dev` too. Without `apiPrefix` the backend is
|
|
114
|
+
* loader-only.
|
|
108
115
|
*
|
|
109
116
|
* **Request-scoped loader calls.** While a page renders, its loaders, actions and boundaries get a
|
|
110
117
|
* `ctx.api` bound to that request's platform identity: a backend handler reached through it sees the
|
|
@@ -114,12 +121,12 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
114
121
|
* does not lose it. Headers do NOT travel: the page request's `cookie` and `authorization` are never
|
|
115
122
|
* copied, so a loader call is anonymous unless the loader passes headers on the call itself. */
|
|
116
123
|
readonly api?: unknown
|
|
117
|
-
/** HTTP path prefix the {@link api} backend is
|
|
124
|
+
/** HTTP path prefix the {@link api} backend is mounted at, such as `"/api"`. A request whose
|
|
118
125
|
* pathname is exactly the prefix or starts with `prefix + "/"` is dispatched to the backend before
|
|
119
126
|
* page routing; the backend therefore defines its routes at the **full** path (`server().post("/api/
|
|
120
|
-
* sync", …)`), matching the in-process `inProcessClient` call sites.
|
|
121
|
-
*
|
|
122
|
-
*
|
|
127
|
+
* sync", …)`), matching the in-process `inProcessClient` call sites. Unset or `""`, nothing is
|
|
128
|
+
* mounted: the app serves pages only and `api` stays a loader-only `ctx.api`. Mounting is also a
|
|
129
|
+
* no-op when `api` does not expose the symbol mount.
|
|
123
130
|
*
|
|
124
131
|
* A page file under the prefix (`routes/api/report.tsx`) could never render - the backend answers
|
|
125
132
|
* first and its 404 is final - so `createWebApp` throws, naming the file. The same holds for every
|
|
@@ -146,7 +153,8 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
146
153
|
* Tried longest-path-first and BEFORE the `api` mount, so a mount at `/api/auth` wins over a backend
|
|
147
154
|
* at `/api` no matter which was declared first. `stripPrefix` is the per-mount form of {@link apiStrip}:
|
|
148
155
|
* leave it off to pass the full path through. `opaque` states why a mount whose routes nifra cannot
|
|
149
|
-
* read (better-auth's handler) is outside assurance; it is then listed as a known gap
|
|
156
|
+
* read (better-auth's handler) is outside assurance; it is then listed as a known gap, with any
|
|
157
|
+
* routes it declares reported as declared.
|
|
150
158
|
*/
|
|
151
159
|
readonly mounts?: ReadonlyArray<{
|
|
152
160
|
readonly path: string
|
|
@@ -154,7 +162,7 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
154
162
|
readonly stripPrefix?: boolean
|
|
155
163
|
readonly priority?: number
|
|
156
164
|
readonly fallbackOn?: 404
|
|
157
|
-
readonly opaque?:
|
|
165
|
+
readonly opaque?: MountOptions<Env>["opaque"]
|
|
158
166
|
}>
|
|
159
167
|
/** Secret for **draft / preview mode** (see `enableDraft`). When set, a request carrying a valid
|
|
160
168
|
* signed `__nifra_draft` cookie gets `ctx.draft === true` in loaders/actions (else always `false`).
|
|
@@ -191,8 +199,10 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
191
199
|
* team - unreleased feature names, internal hostnames, and "don't touch X yet" notes live there
|
|
192
200
|
* routinely. Turn it on only for a repo whose guidelines you would publish as a page. */
|
|
193
201
|
readonly publishLocalGuidelines?: boolean
|
|
194
|
-
/** Serve `/llms.txt` and `/llms-full.txt
|
|
195
|
-
*
|
|
202
|
+
/** Serve `/llms.txt` and `/llms-full.txt`: the page routes, and the backend routes when the backend
|
|
203
|
+
* is mounted over HTTP at {@link apiPrefix}, with their request and response schemas in the full
|
|
204
|
+
* text. **Off by default**: both are public and unauthenticated, and they list every route, admin
|
|
205
|
+
* and internal ones included. Turn it on for an app whose whole surface you would publish. */
|
|
196
206
|
readonly llmsTxt?: boolean
|
|
197
207
|
/** SSG: per dynamic route pattern, its `getStaticPaths` `fallback` (from `enumerateStaticRoutes` or
|
|
198
208
|
* the build's `prerendered.json`). A route mapped to `"404"` rejects any path NOT in
|
|
@@ -204,8 +214,9 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
204
214
|
readonly staticBoundaryCache?: import("../boundary.ts").StaticBoundaryCache
|
|
205
215
|
/** Observe every loader/action failure - for error-reporting plugins (Sentry-style). Called for
|
|
206
216
|
* real throws (not control-flow `Response`s like `redirect`), **before** the nearest `_error`
|
|
207
|
-
* boundary renders / a soft-nav 500 / a rethrow
|
|
208
|
-
*
|
|
217
|
+
* boundary renders / a soft-nav 500 / a rethrow. A failure a boundary answers is also written to the
|
|
218
|
+
* server's `logger` (under its `errorLogDetail`) whether or not this is set, exactly as an unhandled
|
|
219
|
+
* error is. Observation only; its own throws are swallowed so a faulty reporter can't break
|
|
209
220
|
* rendering. (`beforeLoader` is intentionally omitted - the core HTTP hooks already cover
|
|
210
221
|
* pre-request work.) */
|
|
211
222
|
readonly onLoaderError?: (
|
|
@@ -258,10 +269,14 @@ function backendMountOf<Env>(api: unknown): MountableApp<Env> | undefined {
|
|
|
258
269
|
return mount
|
|
259
270
|
}
|
|
260
271
|
|
|
261
|
-
|
|
272
|
+
/** A provider a web app composes: it also takes the chain of web providers above it, which a
|
|
273
|
+
* provider that composes nothing ignores. */
|
|
274
|
+
type ComposedEvidenceProvider = (ancestry?: EvidenceAncestry) => ReturnType<BackendEvidenceProvider>
|
|
275
|
+
|
|
276
|
+
function evidenceProviderOf(value: unknown): ComposedEvidenceProvider | undefined {
|
|
262
277
|
if ((typeof value !== "object" && typeof value !== "function") || value === null) return undefined
|
|
263
278
|
const provider = (value as { [NIFRA_BACKEND_EVIDENCE]?: unknown })[NIFRA_BACKEND_EVIDENCE]
|
|
264
|
-
return typeof provider === "function" ? (provider as
|
|
279
|
+
return typeof provider === "function" ? (provider as ComposedEvidenceProvider) : undefined
|
|
265
280
|
}
|
|
266
281
|
|
|
267
282
|
function mountPathPrefix(path: string): string {
|
|
@@ -274,6 +289,15 @@ function mountPathPrefix(path: string): string {
|
|
|
274
289
|
return withoutWildcard.slice(0, end)
|
|
275
290
|
}
|
|
276
291
|
|
|
292
|
+
/** The server's own error log, for page failures an `_error` boundary answers before it sees them. */
|
|
293
|
+
function boundaryErrorLog(
|
|
294
|
+
options: ServerOptions | undefined,
|
|
295
|
+
): (error: unknown, request: Request) => void {
|
|
296
|
+
const logger = options?.logger ?? jsonLogger()
|
|
297
|
+
const detail = options?.errorLogDetail ?? "full"
|
|
298
|
+
return (error, request) => emitRequestErrorLog(logger, detail, error, { req: request })
|
|
299
|
+
}
|
|
300
|
+
|
|
277
301
|
/**
|
|
278
302
|
* Build a nifra app from a route manifest: every route SSRs its layout chain via `renderPage`,
|
|
279
303
|
* and a wildcard catch-all renders `_404` (or a plain 404). Reuses @nifrajs/core's router +
|
|
@@ -306,6 +330,38 @@ export function createWebApp<Env = unknown>(
|
|
|
306
330
|
// `beforeHandle`, assurance evidence) actually bind to them - see the `use` option. A caller
|
|
307
331
|
// cannot do this after the fact: by the time this function returns, the routes are declared.
|
|
308
332
|
options.use?.(app)
|
|
333
|
+
if (options.serverComponents !== undefined) {
|
|
334
|
+
const runtime = options.serverComponents
|
|
335
|
+
const match = createMatcher(
|
|
336
|
+
manifest.routes
|
|
337
|
+
.filter((route) => runtime.owns(route.id))
|
|
338
|
+
.map((route) => ({ routeId: route.id, pattern: route.pattern })),
|
|
339
|
+
)
|
|
340
|
+
const privateRoute = (request: Request): boolean =>
|
|
341
|
+
request.headers.has("x-nifra-component") || match(new URL(request.url).pathname) !== undefined
|
|
342
|
+
Object.defineProperty(app, Symbol.for("nifra.web.private-route"), { value: privateRoute })
|
|
343
|
+
app.onResponse((response, request) => {
|
|
344
|
+
if (!privateRoute(request)) return response
|
|
345
|
+
const headers = new Headers(response.headers)
|
|
346
|
+
headers.set("cache-control", "private, no-store")
|
|
347
|
+
const vary = new Set(
|
|
348
|
+
(headers.get("vary") ?? "")
|
|
349
|
+
.split(",")
|
|
350
|
+
.map((name) => name.trim().toLowerCase())
|
|
351
|
+
.filter(Boolean),
|
|
352
|
+
)
|
|
353
|
+
vary.add("x-nifra-component")
|
|
354
|
+
vary.add("x-nifra-data")
|
|
355
|
+
headers.set("vary", [...vary].join(", "))
|
|
356
|
+
headers.delete("x-nifra-isr-revalidate")
|
|
357
|
+
headers.delete("x-nifra-isr-tags")
|
|
358
|
+
return new Response(response.body, {
|
|
359
|
+
status: response.status,
|
|
360
|
+
statusText: response.statusText,
|
|
361
|
+
headers,
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
}
|
|
309
365
|
// A resolver created by `createNonceResolver({ header })` carries a paired response hook. Register it
|
|
310
366
|
// after caller middleware so its request-specific CSP value is the last default applied to a page.
|
|
311
367
|
const nonceResponse = options.nonce?.onResponse
|
|
@@ -322,15 +378,15 @@ export function createWebApp<Env = unknown>(
|
|
|
322
378
|
cacheChannel.enabled = true
|
|
323
379
|
},
|
|
324
380
|
})
|
|
325
|
-
//
|
|
381
|
+
// Mount the in-process backend over HTTP at `apiPrefix`, when one is given, BEFORE page routing.
|
|
326
382
|
// Core's pre-route mount seam runs before the page wildcard `/*`; parent request hooks run first,
|
|
327
383
|
// child request hooks run inside the mount, and parent response hooks still wrap the result.
|
|
328
384
|
// The mount handles every HTTP method and preserves backend 404 responses instead of letting the
|
|
329
385
|
// page catch-all render them. We dispatch the same `Request` object (its body unread), so
|
|
330
386
|
// streamed/large bodies pass through untouched. The backend defines full prefixed paths
|
|
331
|
-
// (`server().post("/api/sync", …)`) unless `apiStrip` is enabled.
|
|
332
|
-
//
|
|
333
|
-
const apiPrefix = options.apiPrefix ?? "
|
|
387
|
+
// (`server().post("/api/sync", …)`) unless `apiStrip` is enabled. Without a prefix, or with a
|
|
388
|
+
// non-mountable `api`, the backend is loader-only and pages-only apps keep the no-mount fast path.
|
|
389
|
+
const apiPrefix = options.apiPrefix ?? ""
|
|
334
390
|
const apiStrip = options.apiStrip === true
|
|
335
391
|
const mountedApi = backendMountOf<Env>(api)
|
|
336
392
|
// Longest path first, so a more specific mount (`/api/auth`) is tried before a broader one (`/api`)
|
|
@@ -361,6 +417,22 @@ export function createWebApp<Env = unknown>(
|
|
|
361
417
|
? undefined
|
|
362
418
|
: servedPrerenderedList(options.prerenderedPaths)
|
|
363
419
|
const pageExecutor = createPageRequestExecutor<Env>({
|
|
420
|
+
...(options.serverComponents === undefined
|
|
421
|
+
? {}
|
|
422
|
+
: { serverComponents: options.serverComponents }),
|
|
423
|
+
...(options.serverComponents === undefined
|
|
424
|
+
? {}
|
|
425
|
+
: {
|
|
426
|
+
rerenderOwnerGET: async (
|
|
427
|
+
request: Request,
|
|
428
|
+
context: import("./page-execution.ts").PageRouteContext<Env>,
|
|
429
|
+
) =>
|
|
430
|
+
app.fetch(request, {
|
|
431
|
+
env: context.env,
|
|
432
|
+
...(context.waitUntil === undefined ? {} : { waitUntil: context.waitUntil }),
|
|
433
|
+
...(context.clientIp === undefined ? {} : { clientIp: context.clientIp }),
|
|
434
|
+
}),
|
|
435
|
+
}),
|
|
364
436
|
adapter,
|
|
365
437
|
manifest,
|
|
366
438
|
clientEntry,
|
|
@@ -384,18 +456,32 @@ export function createWebApp<Env = unknown>(
|
|
|
384
456
|
? {}
|
|
385
457
|
: { nonce: cspNonceResolver(options.csp) as NonceResolver<Env>, csp: options.csp }),
|
|
386
458
|
...(options.onLoaderError === undefined ? {} : { onLoaderError: options.onLoaderError }),
|
|
459
|
+
logBoundaryError: boundaryErrorLog(options.server),
|
|
387
460
|
cacheChannel,
|
|
388
461
|
})
|
|
389
462
|
|
|
463
|
+
let ownsRootCatchAll = false
|
|
390
464
|
for (const route of manifest.routes) {
|
|
391
|
-
|
|
392
|
-
|
|
465
|
+
const pattern = requestPattern(route.pattern)
|
|
466
|
+
if (pattern.startsWith("/*")) ownsRootCatchAll = true
|
|
467
|
+
app.register(
|
|
468
|
+
"GET",
|
|
469
|
+
pattern,
|
|
470
|
+
options.serverComponents?.owns(route.id)
|
|
471
|
+
? { capabilities: options.serverComponents.capabilities(route.id) }
|
|
472
|
+
: undefined,
|
|
473
|
+
pageExecutor.get(route),
|
|
474
|
+
)
|
|
475
|
+
app.register("POST", pattern, undefined, pageExecutor.post(route))
|
|
393
476
|
}
|
|
394
477
|
// llms.txt & llms-full.txt describe what a visitor can reach: the pages, and the backend only when it
|
|
395
478
|
// is served over HTTP - a backend the loaders call in-process has no public surface to describe. An
|
|
396
479
|
// app route at either path takes precedence, and each text is built once per app.
|
|
397
|
-
if (options.llmsTxt
|
|
398
|
-
const llmsOptions = {
|
|
480
|
+
if (options.llmsTxt === true) {
|
|
481
|
+
const llmsOptions = {
|
|
482
|
+
includeLocalGuidelines: options.publishLocalGuidelines === true,
|
|
483
|
+
apiPathPrefix: apiStrip ? mountPathPrefix(apiPrefix) : "",
|
|
484
|
+
}
|
|
399
485
|
const publicApi = mountedApi !== undefined && apiPrefix !== "" ? api : undefined
|
|
400
486
|
for (const [path, full] of [
|
|
401
487
|
["/llms.txt", false],
|
|
@@ -436,16 +522,13 @@ export function createWebApp<Env = unknown>(
|
|
|
436
522
|
}
|
|
437
523
|
|
|
438
524
|
// Wildcard catch-all: unmatched paths render the nearest `_404` (404), or a plain text 404 if
|
|
439
|
-
// absent.
|
|
440
|
-
|
|
525
|
+
// absent. A root catch-all page (`[...slug].tsx`) already answers every path, the root included,
|
|
526
|
+
// and the router refuses a second `/*` under another param name.
|
|
527
|
+
if (!ownsRootCatchAll) app.register("GET", "/*", undefined, pageExecutor.fallback)
|
|
441
528
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
throw new Error("[nifra/web] cyclic composed assurance evidence mount")
|
|
446
|
-
}
|
|
447
|
-
evidenceComposing = true
|
|
448
|
-
try {
|
|
529
|
+
const evidenceProvider: ComposedEvidenceProvider = async (passed) => {
|
|
530
|
+
const { traverseEvidence } = await import("./evidence-traversal.ts")
|
|
531
|
+
return traverseEvidence(evidenceProvider, passed, async (ancestry) => {
|
|
449
532
|
const { composeProjectEvidence, snapshotProjectEvidence } = await import(
|
|
450
533
|
"@nifrajs/core/evidence"
|
|
451
534
|
)
|
|
@@ -464,7 +547,7 @@ export function createWebApp<Env = unknown>(
|
|
|
464
547
|
)
|
|
465
548
|
}
|
|
466
549
|
parts.push({
|
|
467
|
-
evidence: await provider.call(mount.app),
|
|
550
|
+
evidence: await provider.call(mount.app, ancestry),
|
|
468
551
|
...(mount.stripPrefix === true ? { pathPrefix: mountPathPrefix(mount.path) } : {}),
|
|
469
552
|
})
|
|
470
553
|
}
|
|
@@ -476,14 +559,13 @@ export function createWebApp<Env = unknown>(
|
|
|
476
559
|
)
|
|
477
560
|
}
|
|
478
561
|
parts.push({
|
|
479
|
-
evidence: await provider.call(api),
|
|
562
|
+
evidence: await provider.call(api, ancestry),
|
|
563
|
+
// biome-ignore lint/plugin/noConditionalEmptyObjectSpread: Evidence paths acquire a prefix only when the mounted backend strips it.
|
|
480
564
|
...(apiStrip ? { pathPrefix: mountPathPrefix(apiPrefix) } : {}),
|
|
481
565
|
})
|
|
482
566
|
}
|
|
483
567
|
return composeProjectEvidence(parts)
|
|
484
|
-
}
|
|
485
|
-
evidenceComposing = false
|
|
486
|
-
}
|
|
568
|
+
})
|
|
487
569
|
}
|
|
488
570
|
Object.defineProperty(app, NIFRA_BACKEND_EVIDENCE, {
|
|
489
571
|
configurable: false,
|
|
@@ -491,6 +573,8 @@ export function createWebApp<Env = unknown>(
|
|
|
491
573
|
value: evidenceProvider,
|
|
492
574
|
writable: false,
|
|
493
575
|
})
|
|
576
|
+
// Core's dev-time hook audit reads this: an order-scoped hook added from here on skips every page.
|
|
577
|
+
Object.defineProperty(app, Symbol.for("nifra.web.pagesDeclared"), { value: true })
|
|
494
578
|
|
|
495
579
|
return app
|
|
496
580
|
}
|