@nifrajs/web 5.5.1 → 5.7.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/CHANGELOG.md +243 -0
- package/README.md +21 -11
- package/dist/boundary.d.ts +67 -7
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +6 -4
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +3 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +2 -0
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +9 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +10 -0
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +174 -23
- package/dist/client.js.map +1 -1
- package/dist/csp.d.ts +8 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +11 -2
- package/dist/csp.js.map +1 -1
- package/dist/deferred.d.ts +6 -4
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +10 -6
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +11 -2
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +13 -6
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +4 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +12 -5
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +4 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -1
- package/dist/diagnostic-catalog.js +48 -0
- package/dist/diagnostic-catalog.js.map +1 -1
- package/dist/diagnostic.d.ts +1 -1
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +1 -1
- package/dist/diagnostic.js.map +1 -1
- package/dist/fn.d.ts +5 -3
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +20 -5
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +13 -3
- package/dist/fs.js.map +1 -1
- package/dist/identity.d.ts +22 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +60 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +2 -1
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
- package/dist/internal/cdn/cloudflare.js +2 -1
- package/dist/internal/cdn/cloudflare.js.map +1 -1
- package/dist/internal/certified-components.d.ts +1 -1
- package/dist/internal/certified-components.js +1 -1
- package/dist/internal/codegen.d.ts +11 -10
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +73 -10
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-websocket.d.ts +73 -0
- package/dist/internal/dev-websocket.d.ts.map +1 -0
- package/dist/internal/dev-websocket.js +145 -0
- package/dist/internal/dev-websocket.js.map +1 -0
- package/dist/internal/hole-runtime.d.ts +66 -0
- package/dist/internal/hole-runtime.d.ts.map +1 -0
- package/dist/internal/hole-runtime.js +211 -0
- package/dist/internal/hole-runtime.js.map +1 -0
- package/dist/internal/identity-epoch.d.ts +50 -0
- package/dist/internal/identity-epoch.d.ts.map +1 -0
- package/dist/internal/identity-epoch.js +180 -0
- package/dist/internal/identity-epoch.js.map +1 -0
- package/dist/internal/invalidation-contract.d.ts +23 -0
- package/dist/internal/invalidation-contract.d.ts.map +1 -0
- package/dist/internal/invalidation-contract.js +11 -0
- package/dist/internal/invalidation-contract.js.map +1 -0
- package/dist/internal/invalidation.d.ts +62 -0
- package/dist/internal/invalidation.d.ts.map +1 -0
- package/dist/internal/invalidation.js +149 -0
- package/dist/internal/invalidation.js.map +1 -0
- package/dist/internal/island-bindings.d.ts +51 -0
- package/dist/internal/island-bindings.d.ts.map +1 -0
- package/dist/internal/island-bindings.js +147 -0
- package/dist/internal/island-bindings.js.map +1 -0
- package/dist/internal/island-list.d.ts +26 -0
- package/dist/internal/island-list.d.ts.map +1 -0
- package/dist/internal/island-list.js +78 -0
- package/dist/internal/island-list.js.map +1 -0
- package/dist/internal/island-mount.d.ts +4 -0
- package/dist/internal/island-mount.d.ts.map +1 -0
- package/dist/internal/island-mount.js +66 -0
- package/dist/internal/island-mount.js.map +1 -0
- package/dist/internal/island-owner.d.ts +5 -0
- package/dist/internal/island-owner.d.ts.map +1 -0
- package/dist/internal/island-owner.js +20 -0
- package/dist/internal/island-owner.js.map +1 -0
- package/dist/internal/island-resource.d.ts +30 -0
- package/dist/internal/island-resource.d.ts.map +1 -0
- package/dist/internal/island-resource.js +57 -0
- package/dist/internal/island-resource.js.map +1 -0
- package/dist/internal/island-signals.d.ts +51 -0
- package/dist/internal/island-signals.d.ts.map +1 -0
- package/dist/internal/island-signals.js +220 -0
- package/dist/internal/island-signals.js.map +1 -0
- package/dist/internal/keep-alive-table.d.ts +23 -0
- package/dist/internal/keep-alive-table.d.ts.map +1 -0
- package/dist/internal/keep-alive-table.js +53 -0
- package/dist/internal/keep-alive-table.js.map +1 -0
- package/dist/internal/live-contract.d.ts +101 -0
- package/dist/internal/live-contract.d.ts.map +1 -0
- package/dist/internal/live-contract.js +5 -0
- package/dist/internal/live-contract.js.map +1 -0
- package/dist/internal/live-queries.d.ts +22 -0
- package/dist/internal/live-queries.d.ts.map +1 -0
- package/dist/internal/live-queries.js +54 -0
- package/dist/internal/live-queries.js.map +1 -0
- package/dist/internal/page-events.d.ts +10 -0
- package/dist/internal/page-events.d.ts.map +1 -0
- package/dist/internal/page-events.js +26 -0
- package/dist/internal/page-events.js.map +1 -0
- package/dist/internal/page-execution.d.ts +16 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +432 -31
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts +3 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +28 -5
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/ppr/admission.d.ts +83 -0
- package/dist/internal/ppr/admission.d.ts.map +1 -0
- package/dist/internal/ppr/admission.js +221 -0
- package/dist/internal/ppr/admission.js.map +1 -0
- package/dist/internal/ppr/canary.d.ts +24 -0
- package/dist/internal/ppr/canary.d.ts.map +1 -0
- package/dist/internal/ppr/canary.js +102 -0
- package/dist/internal/ppr/canary.js.map +1 -0
- package/dist/internal/ppr/compile-shell.d.ts +39 -0
- package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
- package/dist/internal/ppr/compile-shell.js +298 -0
- package/dist/internal/ppr/compile-shell.js.map +1 -0
- package/dist/internal/ppr/contract.d.ts +177 -0
- package/dist/internal/ppr/contract.d.ts.map +1 -0
- package/dist/internal/ppr/contract.js +80 -0
- package/dist/internal/ppr/contract.js.map +1 -0
- package/dist/internal/ppr/frames.d.ts +71 -0
- package/dist/internal/ppr/frames.d.ts.map +1 -0
- package/dist/internal/ppr/frames.js +119 -0
- package/dist/internal/ppr/frames.js.map +1 -0
- package/dist/internal/ppr/integration.d.ts +19 -0
- package/dist/internal/ppr/integration.d.ts.map +1 -0
- package/dist/internal/ppr/integration.js +106 -0
- package/dist/internal/ppr/integration.js.map +1 -0
- package/dist/internal/ppr/options.d.ts +96 -0
- package/dist/internal/ppr/options.d.ts.map +1 -0
- package/dist/internal/ppr/options.js +125 -0
- package/dist/internal/ppr/options.js.map +1 -0
- package/dist/internal/ppr/route-rules.d.ts +12 -0
- package/dist/internal/ppr/route-rules.d.ts.map +1 -0
- package/dist/internal/ppr/route-rules.js +53 -0
- package/dist/internal/ppr/route-rules.js.map +1 -0
- package/dist/internal/ppr/runtime.d.ts +29 -0
- package/dist/internal/ppr/runtime.d.ts.map +1 -0
- package/dist/internal/ppr/runtime.js +35 -0
- package/dist/internal/ppr/runtime.js.map +1 -0
- package/dist/internal/ppr/server.d.ts +29 -0
- package/dist/internal/ppr/server.d.ts.map +1 -0
- package/dist/internal/ppr/server.js +1409 -0
- package/dist/internal/ppr/server.js.map +1 -0
- package/dist/internal/ppr/shell-store.d.ts +39 -0
- package/dist/internal/ppr/shell-store.d.ts.map +1 -0
- package/dist/internal/ppr/shell-store.js +106 -0
- package/dist/internal/ppr/shell-store.js.map +1 -0
- package/dist/internal/prerendered-pages.d.ts +1 -1
- package/dist/internal/prerendered-pages.d.ts.map +1 -1
- package/dist/internal/prerendered-pages.js +7 -4
- package/dist/internal/prerendered-pages.js.map +1 -1
- package/dist/internal/present.d.ts +11 -0
- package/dist/internal/present.d.ts.map +1 -0
- package/dist/internal/present.js +13 -0
- package/dist/internal/present.js.map +1 -0
- package/dist/internal/query-digest.d.ts +9 -0
- package/dist/internal/query-digest.d.ts.map +1 -0
- package/dist/internal/query-digest.js +23 -0
- package/dist/internal/query-digest.js.map +1 -0
- package/dist/internal/query-runtime.d.ts +6 -1
- package/dist/internal/query-runtime.d.ts.map +1 -1
- package/dist/internal/query-runtime.js +9 -0
- package/dist/internal/query-runtime.js.map +1 -1
- package/dist/internal/query-seam.d.ts +29 -0
- package/dist/internal/query-seam.d.ts.map +1 -0
- package/dist/internal/query-seam.js +23 -0
- package/dist/internal/query-seam.js.map +1 -0
- package/dist/internal/render-document.d.ts +34 -64
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +82 -64
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-abort.d.ts +8 -0
- package/dist/internal/request-abort.d.ts.map +1 -0
- package/dist/internal/request-abort.js +22 -0
- package/dist/internal/request-abort.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +2 -0
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +8 -1
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/share-data.d.ts +10 -0
- package/dist/internal/share-data.d.ts.map +1 -0
- package/dist/internal/share-data.js +135 -0
- package/dist/internal/share-data.js.map +1 -0
- package/dist/internal/status-page.d.ts +68 -0
- package/dist/internal/status-page.d.ts.map +1 -0
- package/dist/internal/status-page.js +66 -0
- package/dist/internal/status-page.js.map +1 -0
- package/dist/internal/store-signature.d.ts +15 -0
- package/dist/internal/store-signature.d.ts.map +1 -0
- package/dist/internal/store-signature.js +114 -0
- package/dist/internal/store-signature.js.map +1 -0
- package/dist/internal/vite-plugin-compat.d.ts +2 -2
- package/dist/internal/vite-plugin-compat.js +2 -2
- package/dist/internal/web-app.d.ts +26 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +79 -3
- package/dist/internal/web-app.js.map +1 -1
- package/dist/islands-swap.d.ts +41 -0
- package/dist/islands-swap.d.ts.map +1 -0
- package/dist/islands-swap.js +270 -0
- package/dist/islands-swap.js.map +1 -0
- package/dist/islands.d.ts +86 -24
- package/dist/islands.d.ts.map +1 -1
- package/dist/islands.js +138 -78
- package/dist/islands.js.map +1 -1
- package/dist/isr.d.ts +67 -13
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +247 -99
- package/dist/isr.js.map +1 -1
- package/dist/keep-alive.d.ts +147 -0
- package/dist/keep-alive.d.ts.map +1 -0
- package/dist/keep-alive.js +1102 -0
- package/dist/keep-alive.js.map +1 -0
- package/dist/live/bus.d.ts +70 -0
- package/dist/live/bus.d.ts.map +1 -0
- package/dist/live/bus.js +55 -0
- package/dist/live/bus.js.map +1 -0
- package/dist/live/core.d.ts +25 -0
- package/dist/live/core.d.ts.map +1 -0
- package/dist/live/core.js +29 -0
- package/dist/live/core.js.map +1 -0
- package/dist/live/history.d.ts +38 -0
- package/dist/live/history.d.ts.map +1 -0
- package/dist/live/history.js +104 -0
- package/dist/live/history.js.map +1 -0
- package/dist/live/hub-protocol.d.ts +46 -0
- package/dist/live/hub-protocol.d.ts.map +1 -0
- package/dist/live/hub-protocol.js +108 -0
- package/dist/live/hub-protocol.js.map +1 -0
- package/dist/live/hub.d.ts +185 -0
- package/dist/live/hub.d.ts.map +1 -0
- package/dist/live/hub.js +544 -0
- package/dist/live/hub.js.map +1 -0
- package/dist/live/keyring.d.ts +92 -0
- package/dist/live/keyring.d.ts.map +1 -0
- package/dist/live/keyring.js +265 -0
- package/dist/live/keyring.js.map +1 -0
- package/dist/live/read-capped.d.ts +6 -0
- package/dist/live/read-capped.d.ts.map +1 -0
- package/dist/live/read-capped.js +42 -0
- package/dist/live/read-capped.js.map +1 -0
- package/dist/live/revocations.d.ts +52 -0
- package/dist/live/revocations.d.ts.map +1 -0
- package/dist/live/revocations.js +116 -0
- package/dist/live/revocations.js.map +1 -0
- package/dist/live/settings.d.ts +14 -0
- package/dist/live/settings.d.ts.map +1 -0
- package/dist/live/settings.js +38 -0
- package/dist/live/settings.js.map +1 -0
- package/dist/live-client.d.ts +34 -0
- package/dist/live-client.d.ts.map +1 -0
- package/dist/live-client.js +878 -0
- package/dist/live-client.js.map +1 -0
- package/dist/live-hub.d.ts +130 -0
- package/dist/live-hub.d.ts.map +1 -0
- package/dist/live-hub.js +507 -0
- package/dist/live-hub.js.map +1 -0
- package/dist/live-redis.d.ts +60 -0
- package/dist/live-redis.d.ts.map +1 -0
- package/dist/live-redis.js +212 -0
- package/dist/live-redis.js.map +1 -0
- package/dist/live.d.ts +150 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +972 -0
- package/dist/live.js.map +1 -0
- package/dist/manifest.d.ts +113 -2
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +109 -5
- package/dist/manifest.js.map +1 -1
- package/dist/navigation.d.ts +9 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +13 -3
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +6 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +8 -2
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/ppr.d.ts +73 -0
- package/dist/ppr.d.ts.map +1 -0
- package/dist/ppr.js +76 -0
- package/dist/ppr.js.map +1 -0
- package/dist/query.d.ts +8 -13
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +112 -18
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +32 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +7 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +2 -2
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +72 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +416 -129
- package/dist/router.js.map +1 -1
- package/dist/server-components/build.d.ts.map +1 -1
- package/dist/server-components/build.js +3 -3
- package/dist/server-components/build.js.map +1 -1
- package/dist/server-components/client.d.ts +23 -1
- package/dist/server-components/client.d.ts.map +1 -1
- package/dist/server-components/client.js +92 -5
- package/dist/server-components/client.js.map +1 -1
- package/dist/server-components/definition.d.ts.map +1 -1
- package/dist/server-components/definition.js +2 -1
- package/dist/server-components/definition.js.map +1 -1
- package/dist/server-components/hole.d.ts +8 -0
- package/dist/server-components/hole.d.ts.map +1 -0
- package/dist/server-components/hole.js +20 -0
- package/dist/server-components/hole.js.map +1 -0
- package/dist/server-components/html-policy.d.ts.map +1 -1
- package/dist/server-components/html-policy.js +62 -4
- package/dist/server-components/html-policy.js.map +1 -1
- package/dist/server-components/html-scan.d.ts +17 -0
- package/dist/server-components/html-scan.d.ts.map +1 -0
- package/dist/server-components/html-scan.js +180 -0
- package/dist/server-components/html-scan.js.map +1 -0
- package/dist/server-components/html.d.ts.map +1 -1
- package/dist/server-components/html.js +19 -4
- package/dist/server-components/html.js.map +1 -1
- package/dist/server-components/router-integration.d.ts +4 -1
- package/dist/server-components/router-integration.d.ts.map +1 -1
- package/dist/server-components/router-integration.js +23 -2
- package/dist/server-components/router-integration.js.map +1 -1
- package/dist/server-components/runtime.d.ts +6 -1
- package/dist/server-components/runtime.d.ts.map +1 -1
- package/dist/server-components/runtime.js +88 -29
- package/dist/server-components/runtime.js.map +1 -1
- package/dist/server-components/state.d.ts +3 -1
- package/dist/server-components/state.d.ts.map +1 -1
- package/dist/server-components/state.js +50 -38
- package/dist/server-components/state.js.map +1 -1
- package/dist/server-components/types.d.ts +9 -0
- package/dist/server-components/types.d.ts.map +1 -1
- package/dist/server-components/vite.d.ts.map +1 -1
- package/dist/server-components/vite.js +26 -9
- package/dist/server-components/vite.js.map +1 -1
- package/dist/vite.d.ts +5 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +58 -4
- package/dist/vite.js.map +1 -1
- package/package.json +48 -8
- package/src/boundary.ts +77 -5
- package/src/build-plan.ts +3 -0
- package/src/build-vite.ts +2 -0
- package/src/build.ts +20 -1
- package/src/client.ts +183 -23
- package/src/csp.ts +16 -2
- package/src/deferred.ts +11 -5
- package/src/dev-feed.ts +11 -2
- package/src/dev-session.ts +22 -11
- package/src/dev.ts +29 -8
- package/src/diagnostic-catalog.ts +53 -0
- package/src/diagnostic.ts +1 -0
- package/src/fn.ts +24 -7
- package/src/fs.ts +22 -8
- package/src/identity.ts +61 -0
- package/src/index.ts +20 -4
- package/src/internal/cdn/cloudflare.ts +2 -1
- package/src/internal/certified-components.ts +1 -1
- package/src/internal/codegen.ts +95 -9
- package/src/internal/dev-websocket.ts +230 -0
- package/src/internal/hole-runtime.ts +286 -0
- package/src/internal/identity-epoch.ts +239 -0
- package/src/internal/invalidation-contract.ts +35 -0
- package/src/internal/invalidation.ts +213 -0
- package/src/internal/island-bindings.ts +191 -0
- package/src/internal/island-list.ts +92 -0
- package/src/internal/island-mount.ts +68 -0
- package/src/internal/island-owner.ts +20 -0
- package/src/internal/island-resource.ts +76 -0
- package/src/internal/island-signals.ts +249 -0
- package/src/internal/keep-alive-table.ts +72 -0
- package/src/internal/live-contract.ts +111 -0
- package/src/internal/live-queries.ts +60 -0
- package/src/internal/page-events.ts +27 -0
- package/src/internal/page-execution.ts +605 -45
- package/src/internal/parity.ts +35 -6
- package/src/internal/ppr/admission.ts +300 -0
- package/src/internal/ppr/canary.ts +122 -0
- package/src/internal/ppr/compile-shell.ts +379 -0
- package/src/internal/ppr/contract.ts +265 -0
- package/src/internal/ppr/frames.ts +170 -0
- package/src/internal/ppr/integration.ts +137 -0
- package/src/internal/ppr/options.ts +221 -0
- package/src/internal/ppr/route-rules.ts +77 -0
- package/src/internal/ppr/runtime.ts +39 -0
- package/src/internal/ppr/server.ts +1766 -0
- package/src/internal/ppr/shell-store.ts +152 -0
- package/src/internal/prerendered-pages.ts +7 -4
- package/src/internal/present.ts +20 -0
- package/src/internal/query-digest.ts +34 -0
- package/src/internal/query-runtime.ts +14 -1
- package/src/internal/query-seam.ts +48 -0
- package/src/internal/render-document.ts +158 -113
- package/src/internal/request-abort.ts +26 -0
- package/src/internal/server-boundary.ts +10 -1
- package/src/internal/share-data.ts +136 -0
- package/src/internal/status-page.ts +95 -0
- package/src/internal/store-signature.ts +149 -0
- package/src/internal/vite-plugin-compat.ts +2 -2
- package/src/internal/web-app.ts +123 -3
- package/src/islands-swap.ts +283 -0
- package/src/islands.ts +169 -72
- package/src/isr.ts +348 -106
- package/src/keep-alive.ts +1354 -0
- package/src/live/bus.ts +104 -0
- package/src/live/core.ts +52 -0
- package/src/live/history.ts +109 -0
- package/src/live/hub-protocol.ts +133 -0
- package/src/live/hub.ts +620 -0
- package/src/live/keyring.ts +384 -0
- package/src/live/read-capped.ts +36 -0
- package/src/live/revocations.ts +155 -0
- package/src/live/settings.ts +46 -0
- package/src/live-client.ts +1011 -0
- package/src/live-hub.ts +658 -0
- package/src/live-redis.ts +284 -0
- package/src/live.ts +1257 -0
- package/src/manifest.ts +274 -6
- package/src/navigation.ts +14 -2
- package/src/plugins/vite-zone-guard.ts +15 -2
- package/src/ppr.ts +153 -0
- package/src/query.ts +147 -20
- package/src/render-seam.ts +37 -0
- package/src/route-types.ts +3 -1
- package/src/router.ts +534 -132
- package/src/server-components/build.ts +9 -3
- package/src/server-components/client.ts +89 -5
- package/src/server-components/definition.ts +2 -1
- package/src/server-components/hole.ts +23 -0
- package/src/server-components/html-policy.ts +63 -4
- package/src/server-components/html-scan.ts +159 -0
- package/src/server-components/html.ts +18 -4
- package/src/server-components/router-integration.ts +21 -2
- package/src/server-components/runtime.ts +114 -31
- package/src/server-components/state.ts +66 -44
- package/src/server-components/types.ts +16 -0
- package/src/server-components/vite.ts +33 -14
- package/src/vite.ts +75 -6
- package/dist/nano.d.ts +0 -125
- package/dist/nano.d.ts.map +0 -1
- package/dist/nano.js +0 -190
- package/dist/nano.js.map +0 -1
- package/src/nano.ts +0 -246
package/src/internal/parity.ts
CHANGED
|
@@ -99,6 +99,9 @@ export interface IdentityParityFinding {
|
|
|
99
99
|
* before anything loads. Reported, never fatal - see `SingleCopyCoverage`.
|
|
100
100
|
*/
|
|
101
101
|
readonly deduplicated: boolean
|
|
102
|
+
/** Unbundled commands that still load both copies of a deduplicated package, because bunfig.toml
|
|
103
|
+
* does not preload the registrar for them. Absent when both are covered. */
|
|
104
|
+
readonly unenforced?: readonly ("bun run" | "bun test")[]
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
/**
|
|
@@ -743,11 +746,34 @@ const describeProvenance = (copies: readonly IdentityParityCopy[]): string | und
|
|
|
743
746
|
return `reached through ${one ? "a symlink" : "symlinks"} that ${one ? "points" : "point"} outside the importer's install: ${lines.join(", ")}. Unless the importer declares ${one ? "it" : "them"} as a \`link:\` dependency, ${one ? "the link was" : "the links were"} planted by hand, by another project, or by \`bun link\` - remove ${one ? "it" : "them"} and reinstall there, so the importer resolves its own install's copy.`
|
|
744
747
|
}
|
|
745
748
|
|
|
746
|
-
/**
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
749
|
+
/**
|
|
750
|
+
* What is left to do about a duplicate the declaration already covers. Bun reads the top-level
|
|
751
|
+
* `preload` for `bun run` (and the nifra CLI, which runs under the app's bunfig) and `[test] preload`
|
|
752
|
+
* for `bun test`, never one for the other, so each half is reported on its own.
|
|
753
|
+
*/
|
|
754
|
+
const deduplicatedRemediation = (registration: SingleCopyRegistration): string => {
|
|
755
|
+
if (registration.run && registration.test)
|
|
756
|
+
return "Declared single-copy and enforced: every duplicate is rewritten to this app's copy, so one module instance loads."
|
|
757
|
+
const preload = `preload = ["${SINGLE_COPY_REGISTER_SPECIFIER}"]`
|
|
758
|
+
const ignored =
|
|
759
|
+
registration.ignored === undefined
|
|
760
|
+
? ""
|
|
761
|
+
: ` The ${preload} under [${registration.ignored.join("], [")}] belongs to that table, so Bun never runs it.`
|
|
762
|
+
if (registration.test)
|
|
763
|
+
return `Declared single-copy and enforced under \`bun test\` only: \`bun run\`, \`nifra dev\` and the CLI's app imports still load both copies until bunfig.toml carries ${preload} at the top level, above the first [table].${ignored}`
|
|
764
|
+
if (registration.run)
|
|
765
|
+
return `Declared single-copy and enforced under \`bun run\` only: \`bun test\` still loads both copies until bunfig.toml carries ${preload} under [test].`
|
|
766
|
+
return `Declared single-copy, so bundled output is deduplicated by the build. Unbundled runs are NOT: Bun's runtime never offers a bare specifier to a resolver, so add ${preload} to bunfig.toml above the first [table], and again under [test], or \`bun run\` and \`bun test\` still load both copies.${ignored}`
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const unenforcedCommands = (
|
|
770
|
+
registration: SingleCopyRegistration,
|
|
771
|
+
): readonly ("bun run" | "bun test")[] => {
|
|
772
|
+
const commands: ("bun run" | "bun test")[] = []
|
|
773
|
+
if (!registration.run) commands.push("bun run")
|
|
774
|
+
if (!registration.test) commands.push("bun test")
|
|
775
|
+
return commands
|
|
776
|
+
}
|
|
751
777
|
|
|
752
778
|
/**
|
|
753
779
|
* Read the declaration and its runtime proof, preferring the directory the caller is in.
|
|
@@ -861,6 +887,7 @@ export async function collectIdentityParity(
|
|
|
861
887
|
// so the skew stays fatal and the ranges have to be fixed.
|
|
862
888
|
const covered =
|
|
863
889
|
cause === "duplicate-path" && matchesSingleCopyDeclaration(singleCopy.declared, name)
|
|
890
|
+
const unenforced = covered ? unenforcedCommands(singleCopy.registration) : []
|
|
864
891
|
const finding: IdentityParityFinding = {
|
|
865
892
|
package: name,
|
|
866
893
|
copies: resolvedCopies,
|
|
@@ -880,7 +907,9 @@ export async function collectIdentityParity(
|
|
|
880
907
|
...(provenance === undefined ? {} : { provenance }),
|
|
881
908
|
deduplicated: covered,
|
|
882
909
|
}
|
|
883
|
-
;(covered ? deduplicated : findings).push(
|
|
910
|
+
;(covered ? deduplicated : findings).push(
|
|
911
|
+
unenforced.length === 0 ? finding : { ...finding, unenforced },
|
|
912
|
+
)
|
|
884
913
|
}
|
|
885
914
|
return {
|
|
886
915
|
workspaceRoot: scanRoot,
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type { ResolvedPprOptions } from "./options.ts"
|
|
2
|
+
|
|
3
|
+
/** Which admission level denied an invocation, checked in this order. */
|
|
4
|
+
export type DenialLevel = "stop" | "process" | "boundary"
|
|
5
|
+
|
|
6
|
+
/** How one hole invocation ended, as its response sees it. */
|
|
7
|
+
export type HoleRun<T> =
|
|
8
|
+
| { readonly ok: true; readonly value: T }
|
|
9
|
+
| {
|
|
10
|
+
readonly ok: false
|
|
11
|
+
readonly reason: "denied" | "failed" | "aborted"
|
|
12
|
+
readonly error?: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PprAdmissionStats {
|
|
16
|
+
/** Responses holding a hole-work slot. */
|
|
17
|
+
readonly slotsInUse: number
|
|
18
|
+
/** Every invocation whose promise has not settled, abandoned ones included (`ppr.inflight_loaders`). */
|
|
19
|
+
readonly pending: number
|
|
20
|
+
/** The abandoned subset of {@link pending} (`ppr.abandoned_inflight`). */
|
|
21
|
+
readonly abandoned: number
|
|
22
|
+
readonly pendingBy: Readonly<Record<string, number>>
|
|
23
|
+
readonly abandonedBy: Readonly<Record<string, number>>
|
|
24
|
+
readonly denied: Readonly<Record<DenialLevel, number>>
|
|
25
|
+
/** Responses that found no free slot and invoked no hole (`ppr.shed`). */
|
|
26
|
+
readonly shed: number
|
|
27
|
+
/** True while the global admission stop holds (`ppr.admission_stopped`). */
|
|
28
|
+
readonly admissionStopped: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type AdmissionOptions = Pick<
|
|
32
|
+
ResolvedPprOptions,
|
|
33
|
+
| "boundaryAbandonedTrip"
|
|
34
|
+
| "processAbandonedTrip"
|
|
35
|
+
| "admissionStop"
|
|
36
|
+
| "abandonGraceMs"
|
|
37
|
+
| "maxHoleWork"
|
|
38
|
+
>
|
|
39
|
+
|
|
40
|
+
/** The clock and timer admission reads; injected by tests so no test waits on the wall clock. */
|
|
41
|
+
export interface AdmissionTimers {
|
|
42
|
+
now(): number
|
|
43
|
+
setTimeout(callback: () => void, ms: number): unknown
|
|
44
|
+
clearTimeout(handle: unknown): void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SYSTEM_TIMERS: AdmissionTimers = {
|
|
48
|
+
now: () => performance.now(),
|
|
49
|
+
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
50
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the handle came from this object's own setTimeout.
|
|
51
|
+
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const watchers = new WeakMap<AbortSignal, Set<() => void>>()
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Calls `onAbort` when `signal` aborts, until the returned function is called. A signal gets one
|
|
58
|
+
* native listener that every watcher shares: adding and removing a native listener per hole
|
|
59
|
+
* invocation cost more under load than the invocation's own bookkeeping.
|
|
60
|
+
*/
|
|
61
|
+
export function watchAbort(signal: AbortSignal, onAbort: () => void): () => void {
|
|
62
|
+
let watching = watchers.get(signal)
|
|
63
|
+
if (watching === undefined) {
|
|
64
|
+
const created = new Set<() => void>()
|
|
65
|
+
watching = created
|
|
66
|
+
watchers.set(signal, created)
|
|
67
|
+
signal.addEventListener(
|
|
68
|
+
"abort",
|
|
69
|
+
() => {
|
|
70
|
+
for (const watcher of created) watcher()
|
|
71
|
+
},
|
|
72
|
+
{ once: true },
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
const set = watching
|
|
76
|
+
set.add(onAbort)
|
|
77
|
+
return () => {
|
|
78
|
+
set.delete(onAbort)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A boundary's count, kept once made: an invocation changes its own count with no map lookup. */
|
|
83
|
+
interface Count {
|
|
84
|
+
n: number
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const countOf = (counts: Map<string, Count>, key: string): Count => {
|
|
88
|
+
let count = counts.get(key)
|
|
89
|
+
if (count === undefined) {
|
|
90
|
+
count = { n: 0 }
|
|
91
|
+
counts.set(key, count)
|
|
92
|
+
}
|
|
93
|
+
return count
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const nonZero = (counts: Map<string, Count>): Record<string, number> => {
|
|
97
|
+
const out: Record<string, number> = {}
|
|
98
|
+
for (const [key, count] of counts) if (count.n !== 0) out[key] = count.n
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Bounds the hole work a process runs without letting one bad loader starve the rest.
|
|
104
|
+
*
|
|
105
|
+
* An invocation is abandoned once its signal aborted more than `abandonGraceMs` ago and its promise
|
|
106
|
+
* is still pending; it stays counted until the promise settles. Only abandoned invocations count
|
|
107
|
+
* toward a denial level, so a well-behaved boundary is denied only at the global stop. Each response
|
|
108
|
+
* holds one hole-work slot from before its first invocation until every invocation has settled,
|
|
109
|
+
* been denied or been abandoned, which bounds live invocations by `32 * maxHoleWork`.
|
|
110
|
+
*/
|
|
111
|
+
export class HoleAdmission {
|
|
112
|
+
readonly #options: AdmissionOptions
|
|
113
|
+
readonly #onLateSettle: ((key: string, msAfterAbort: number) => void) | undefined
|
|
114
|
+
readonly #timers: AdmissionTimers
|
|
115
|
+
readonly #pendingBy = new Map<string, Count>()
|
|
116
|
+
readonly #abandonedBy = new Map<string, Count>()
|
|
117
|
+
readonly #denied: Record<DenialLevel, number> = { stop: 0, process: 0, boundary: 0 }
|
|
118
|
+
#pending = 0
|
|
119
|
+
#abandoned = 0
|
|
120
|
+
#slotsInUse = 0
|
|
121
|
+
#shed = 0
|
|
122
|
+
|
|
123
|
+
constructor(
|
|
124
|
+
options: AdmissionOptions,
|
|
125
|
+
onLateSettle?: (key: string, msAfterAbort: number) => void,
|
|
126
|
+
timers: AdmissionTimers = SYSTEM_TIMERS,
|
|
127
|
+
) {
|
|
128
|
+
this.#options = options
|
|
129
|
+
this.#onLateSettle = onLateSettle
|
|
130
|
+
this.#timers = timers
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A hole-work slot for one response, or `undefined` when none is free (the response is shed). */
|
|
134
|
+
slot(): HoleSlot | undefined {
|
|
135
|
+
if (this.#slotsInUse >= this.#options.maxHoleWork) {
|
|
136
|
+
this.#shed++
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
this.#slotsInUse++
|
|
140
|
+
return new HoleSlot(this, () => this.#slotsInUse--)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The level that denies boundary `key` now, or `undefined` when it is admitted. */
|
|
144
|
+
denial(key: string): DenialLevel | undefined {
|
|
145
|
+
if (this.#abandoned >= this.#options.admissionStop) return "stop"
|
|
146
|
+
const mine = this.#abandonedBy.get(key)?.n ?? 0
|
|
147
|
+
if (this.#abandoned >= this.#options.processAbandonedTrip && mine > 0) return "process"
|
|
148
|
+
if (mine >= this.#options.boundaryAbandonedTrip) return "boundary"
|
|
149
|
+
return undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @internal Counts one denial. */
|
|
153
|
+
countDenied(level: DenialLevel): void {
|
|
154
|
+
this.#denied[level]++
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @internal Runs one admitted invocation, counted until its promise settles. Once its signal has
|
|
159
|
+
* been aborted for `abandonGraceMs` it is abandoned and `onDone` fires; `onDone` fires again when
|
|
160
|
+
* it settles. The result settles with the loader, or at once when the signal aborts first: a
|
|
161
|
+
* response never waits on a loader that ignores its signal. One promise and one abort watcher per
|
|
162
|
+
* invocation; a chain of them cost more than the loaders the bench page runs.
|
|
163
|
+
*/
|
|
164
|
+
invoke<T>(
|
|
165
|
+
key: string,
|
|
166
|
+
signal: AbortSignal,
|
|
167
|
+
load: () => T | Promise<T>,
|
|
168
|
+
onDone: () => void,
|
|
169
|
+
watch: (onAbort: () => void) => () => void,
|
|
170
|
+
): Promise<HoleRun<T>> {
|
|
171
|
+
this.#pending++
|
|
172
|
+
const pending = countOf(this.#pendingBy, key)
|
|
173
|
+
pending.n++
|
|
174
|
+
return new Promise<HoleRun<T>>((resolve) => {
|
|
175
|
+
let answered = false
|
|
176
|
+
const answer = (run: HoleRun<T>): void => {
|
|
177
|
+
if (answered) return
|
|
178
|
+
answered = true
|
|
179
|
+
resolve(run)
|
|
180
|
+
}
|
|
181
|
+
let abandoned: Count | undefined
|
|
182
|
+
let abortedAt: number | undefined
|
|
183
|
+
let timer: unknown
|
|
184
|
+
const onAbort = (): void => {
|
|
185
|
+
abortedAt = this.#timers.now()
|
|
186
|
+
timer = this.#timers.setTimeout(() => {
|
|
187
|
+
abandoned = countOf(this.#abandonedBy, key)
|
|
188
|
+
abandoned.n++
|
|
189
|
+
this.#abandoned++
|
|
190
|
+
onDone()
|
|
191
|
+
}, this.#options.abandonGraceMs)
|
|
192
|
+
answer({ ok: false, reason: "aborted", error: signal.reason })
|
|
193
|
+
}
|
|
194
|
+
let unwatch: (() => void) | undefined
|
|
195
|
+
if (signal.aborted) onAbort()
|
|
196
|
+
else unwatch = watch(onAbort)
|
|
197
|
+
const settle = (): void => {
|
|
198
|
+
unwatch?.()
|
|
199
|
+
if (timer !== undefined) this.#timers.clearTimeout(timer)
|
|
200
|
+
this.#pending--
|
|
201
|
+
pending.n--
|
|
202
|
+
if (abandoned !== undefined) {
|
|
203
|
+
this.#abandoned--
|
|
204
|
+
abandoned.n--
|
|
205
|
+
}
|
|
206
|
+
if (abortedAt !== undefined) {
|
|
207
|
+
const late = this.#timers.now() - abortedAt
|
|
208
|
+
if (late > this.#options.abandonGraceMs) this.#onLateSettle?.(key, late)
|
|
209
|
+
}
|
|
210
|
+
onDone()
|
|
211
|
+
}
|
|
212
|
+
// `then` turns a synchronous throw into this invocation's rejection.
|
|
213
|
+
Promise.resolve()
|
|
214
|
+
.then(load)
|
|
215
|
+
.then(
|
|
216
|
+
(value) => {
|
|
217
|
+
settle()
|
|
218
|
+
answer({ ok: true, value })
|
|
219
|
+
},
|
|
220
|
+
(error: unknown) => {
|
|
221
|
+
settle()
|
|
222
|
+
answer({ ok: false, reason: signal.aborted ? "aborted" : "failed", error })
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
stats(): PprAdmissionStats {
|
|
229
|
+
return {
|
|
230
|
+
slotsInUse: this.#slotsInUse,
|
|
231
|
+
pending: this.#pending,
|
|
232
|
+
abandoned: this.#abandoned,
|
|
233
|
+
pendingBy: nonZero(this.#pendingBy),
|
|
234
|
+
abandonedBy: nonZero(this.#abandonedBy),
|
|
235
|
+
denied: { ...this.#denied },
|
|
236
|
+
shed: this.#shed,
|
|
237
|
+
admissionStopped: this.#abandoned >= this.#options.admissionStop,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** One response's hole-work slot. Released exactly once. */
|
|
243
|
+
export class HoleSlot {
|
|
244
|
+
readonly #admission: HoleAdmission
|
|
245
|
+
readonly #release: () => void
|
|
246
|
+
#outstanding = 0
|
|
247
|
+
#sealed = false
|
|
248
|
+
#released = false
|
|
249
|
+
|
|
250
|
+
constructor(admission: HoleAdmission, release: () => void) {
|
|
251
|
+
this.#admission = admission
|
|
252
|
+
this.#release = release
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Invoke one hole loader under admission. Denied, it is never invoked. Otherwise the result
|
|
257
|
+
* settles with the loader, or at once when `signal` aborts while the loader is still pending.
|
|
258
|
+
* `watch` reports that abort; the caller that aborts the signal itself passes its own.
|
|
259
|
+
*/
|
|
260
|
+
run<T>(
|
|
261
|
+
key: string,
|
|
262
|
+
signal: AbortSignal,
|
|
263
|
+
load: () => T | Promise<T>,
|
|
264
|
+
watch: (onAbort: () => void) => () => void = (onAbort) => watchAbort(signal, onAbort),
|
|
265
|
+
): Promise<HoleRun<T>> {
|
|
266
|
+
if (this.#sealed) throw new Error("[nifra/web] ppr: a sealed hole slot cannot start a hole")
|
|
267
|
+
if (signal.aborted) return Promise.resolve({ ok: false, reason: "aborted" })
|
|
268
|
+
const level = this.#admission.denial(key)
|
|
269
|
+
if (level !== undefined) {
|
|
270
|
+
this.#admission.countDenied(level)
|
|
271
|
+
return Promise.resolve({ ok: false, reason: "denied" })
|
|
272
|
+
}
|
|
273
|
+
this.#outstanding++
|
|
274
|
+
let done = false
|
|
275
|
+
return this.#admission.invoke(
|
|
276
|
+
key,
|
|
277
|
+
signal,
|
|
278
|
+
load,
|
|
279
|
+
() => {
|
|
280
|
+
if (done) return
|
|
281
|
+
done = true
|
|
282
|
+
this.#outstanding--
|
|
283
|
+
this.#maybeRelease()
|
|
284
|
+
},
|
|
285
|
+
watch,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** No more holes will start for this response; the slot frees once the started ones are done. */
|
|
290
|
+
seal(): void {
|
|
291
|
+
this.#sealed = true
|
|
292
|
+
this.#maybeRelease()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#maybeRelease(): void {
|
|
296
|
+
if (this.#released || !this.#sealed || this.#outstanding > 0) return
|
|
297
|
+
this.#released = true
|
|
298
|
+
this.#release()
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { MAX_SHELL_AFTER_REQUESTS } from "./options.ts"
|
|
2
|
+
|
|
3
|
+
/** Where a canary value went into the request whose shell carried it. */
|
|
4
|
+
export type CanaryCarrier = "cookie" | "authorization" | "header" | "query"
|
|
5
|
+
|
|
6
|
+
export interface ShellCanaryFinding {
|
|
7
|
+
readonly path: string
|
|
8
|
+
readonly via: CanaryCarrier
|
|
9
|
+
/** The shared hole whose frame carried it; absent when the shell did. */
|
|
10
|
+
readonly hole?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ShellCanaryOptions {
|
|
14
|
+
/** The origin the requests are made against. Default `http://canary.invalid`. */
|
|
15
|
+
readonly origin?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CARRIERS: readonly CanaryCarrier[] = ["cookie", "authorization", "header", "query"]
|
|
19
|
+
|
|
20
|
+
/** Where a streamed document's per-response part starts: everything before it is the shared shell. */
|
|
21
|
+
const REQUEST_SCRIPT = '<script type="application/json" id="nifra-ppr-req">'
|
|
22
|
+
const SCRIPT_END = "</script>"
|
|
23
|
+
|
|
24
|
+
/** Requests per path: enough for a shell to be built, then for each shared hole's entry. */
|
|
25
|
+
const MAX_ATTEMPTS = 2 * MAX_SHELL_AFTER_REQUESTS
|
|
26
|
+
|
|
27
|
+
/** The JSON text of the inert script element opened by `opener`, or `undefined` without one. */
|
|
28
|
+
const scriptText = (html: string, opener: string): string | undefined => {
|
|
29
|
+
const start = html.indexOf(opener)
|
|
30
|
+
if (start === -1) return undefined
|
|
31
|
+
const end = html.indexOf(SCRIPT_END, start + opener.length)
|
|
32
|
+
return end === -1 ? undefined : html.slice(start + opener.length, end)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parsed = (text: string | undefined): unknown => {
|
|
36
|
+
if (text === undefined) return undefined
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(text)
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The shared holes' frames a response carries, by name, when every one carries its entry. */
|
|
45
|
+
function sharedFrames(html: string): ReadonlyMap<string, string> | undefined {
|
|
46
|
+
const request = parsed(scriptText(html, REQUEST_SCRIPT))
|
|
47
|
+
const names =
|
|
48
|
+
typeof request === "object" && request !== null ? Reflect.get(request, "shared") : undefined
|
|
49
|
+
const frames = new Map<string, string>()
|
|
50
|
+
if (!Array.isArray(names)) return frames
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
if (typeof name !== "string") return undefined
|
|
53
|
+
const text = scriptText(html, `<script type="application/json" id="nifra-ppr:${name}">`)
|
|
54
|
+
const frame = parsed(text)
|
|
55
|
+
// An error frame holds nothing built: the entry is not there yet.
|
|
56
|
+
if (text === undefined || typeof frame !== "object" || frame === null) return undefined
|
|
57
|
+
if (Reflect.get(frame, "error") === true) return undefined
|
|
58
|
+
frames.set(name, text)
|
|
59
|
+
}
|
|
60
|
+
return frames
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const randomHex = (): string =>
|
|
64
|
+
Array.from(crypto.getRandomValues(new Uint8Array(12)), (byte) =>
|
|
65
|
+
byte.toString(16).padStart(2, "0"),
|
|
66
|
+
).join("")
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The canary check. Each path is requested through the app's own middleware and gates with a
|
|
70
|
+
* unique value in a cookie, an `Authorization` header, a custom header and the query, until a
|
|
71
|
+
* response carries a shell (`shellAfterRequests` decides which request builds it) and a built frame
|
|
72
|
+
* for each shared hole, and those are scanned for every value the path's requests carried. It
|
|
73
|
+
* catches visitor data the app keeps in module state, which no import check sees. Run it against a
|
|
74
|
+
* fresh app before it serves traffic: a shell or frame already cached was built without the canaries.
|
|
75
|
+
*/
|
|
76
|
+
export async function checkShellCanaries(
|
|
77
|
+
app: { fetch(request: Request): Response | Promise<Response> },
|
|
78
|
+
paths: readonly string[],
|
|
79
|
+
options: ShellCanaryOptions = {},
|
|
80
|
+
): Promise<readonly ShellCanaryFinding[]> {
|
|
81
|
+
const findings: ShellCanaryFinding[] = []
|
|
82
|
+
for (const path of paths) {
|
|
83
|
+
const sent: Array<Record<CanaryCarrier, string>> = []
|
|
84
|
+
// A route that renders whole every time reports nothing.
|
|
85
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
86
|
+
const values: Record<CanaryCarrier, string> = {
|
|
87
|
+
cookie: `nifra-canary-${randomHex()}`,
|
|
88
|
+
authorization: `nifra-canary-${randomHex()}`,
|
|
89
|
+
header: `nifra-canary-${randomHex()}`,
|
|
90
|
+
query: `nifra-canary-${randomHex()}`,
|
|
91
|
+
}
|
|
92
|
+
sent.push(values)
|
|
93
|
+
const url = new URL(path, options.origin ?? "http://canary.invalid")
|
|
94
|
+
url.searchParams.set("nifra_canary", values.query)
|
|
95
|
+
const response = await app.fetch(
|
|
96
|
+
new Request(url.href, {
|
|
97
|
+
headers: {
|
|
98
|
+
cookie: `nifra_canary=${values.cookie}`,
|
|
99
|
+
authorization: `Bearer ${values.authorization}`,
|
|
100
|
+
"x-nifra-canary": values.header,
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
const html = await response.text()
|
|
105
|
+
const end = html.indexOf(REQUEST_SCRIPT)
|
|
106
|
+
// No shared shell yet: this request rendered whole, so nothing it saw is shared.
|
|
107
|
+
if (end === -1) continue
|
|
108
|
+
const frames = sharedFrames(html)
|
|
109
|
+
// A shared hole whose entry is still being built: a later request gets it.
|
|
110
|
+
if (frames === undefined && attempt < MAX_ATTEMPTS - 1) continue
|
|
111
|
+
const shell = html.slice(0, end)
|
|
112
|
+
const carried = (text: string, via: CanaryCarrier): boolean =>
|
|
113
|
+
sent.some((earlier) => text.includes(earlier[via]))
|
|
114
|
+
for (const via of CARRIERS) if (carried(shell, via)) findings.push({ path, via })
|
|
115
|
+
for (const [hole, frame] of frames ?? []) {
|
|
116
|
+
for (const via of CARRIERS) if (carried(frame, via)) findings.push({ path, via, hole })
|
|
117
|
+
}
|
|
118
|
+
break
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return findings
|
|
122
|
+
}
|