@nifrajs/web 5.5.1 → 5.6.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 +154 -0
- 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 +9 -0
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +140 -12
- 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 +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- 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 +65 -8
- 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 +179 -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 +96 -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 +14 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +415 -31
- package/dist/internal/page-execution.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 +32 -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 +1407 -0
- package/dist/internal/ppr/server.js.map +1 -0
- package/dist/internal/ppr/shell-store.d.ts +38 -0
- package/dist/internal/ppr/shell-store.d.ts.map +1 -0
- package/dist/internal/ppr/shell-store.js +90 -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/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 +28 -1
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +83 -18
- 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/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 +25 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +78 -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 +1069 -0
- package/dist/keep-alive.js.map +1 -0
- package/dist/live/bus.d.ts +56 -0
- package/dist/live/bus.d.ts.map +1 -0
- package/dist/live/bus.js +32 -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 +181 -0
- package/dist/live/hub.d.ts.map +1 -0
- package/dist/live/hub.js +539 -0
- package/dist/live/hub.js.map +1 -0
- package/dist/live/keyring.d.ts +87 -0
- package/dist/live/keyring.d.ts.map +1 -0
- package/dist/live/keyring.js +260 -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 +840 -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 +48 -0
- package/dist/live-redis.d.ts.map +1 -0
- package/dist/live-redis.js +201 -0
- package/dist/live-redis.js.map +1 -0
- package/dist/live.d.ts +94 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +869 -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 +30 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +5 -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 +374 -99
- 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/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 +86 -28
- 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/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 +19 -1
- package/src/client.ts +147 -10
- 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 +19 -0
- package/src/internal/cdn/cloudflare.ts +2 -1
- package/src/internal/certified-components.ts +1 -1
- package/src/internal/codegen.ts +87 -8
- package/src/internal/dev-websocket.ts +230 -0
- package/src/internal/hole-runtime.ts +286 -0
- package/src/internal/identity-epoch.ts +235 -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 +106 -0
- package/src/internal/live-queries.ts +60 -0
- package/src/internal/page-events.ts +27 -0
- package/src/internal/page-execution.ts +579 -40
- 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 +36 -0
- package/src/internal/ppr/server.ts +1764 -0
- package/src/internal/ppr/shell-store.ts +139 -0
- package/src/internal/prerendered-pages.ts +7 -4
- 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 +149 -22
- 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/store-signature.ts +149 -0
- package/src/internal/vite-plugin-compat.ts +2 -2
- package/src/internal/web-app.ts +121 -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 +1315 -0
- package/src/live/bus.ts +77 -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 +615 -0
- package/src/live/keyring.ts +379 -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 +973 -0
- package/src/live-hub.ts +658 -0
- package/src/live-redis.ts +259 -0
- package/src/live.ts +1096 -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 +35 -0
- package/src/route-types.ts +3 -1
- package/src/router.ts +494 -101
- package/src/server-components/build.ts +9 -3
- package/src/server-components/client.ts +89 -5
- 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 +112 -30
- package/src/server-components/state.ts +66 -44
- package/src/server-components/types.ts +16 -0
- 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
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live keyring: AES-256-GCM grants under hourly per-process subkeys, and the HMAC tags and subject
|
|
3
|
+
* tags derived from the same secrets. Everything here is WebCrypto, so it runs unchanged on Bun, Node,
|
|
4
|
+
* Deno and Workers.
|
|
5
|
+
*
|
|
6
|
+
* Token: `base64url(kid || nonce || AES-256-GCM(JSON claims))`, with the 16-byte `kid` authenticated as
|
|
7
|
+
* associated data. `kid = secretId (4) || processId (8) || hour (4)`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TEXT = new TextEncoder()
|
|
11
|
+
const UTF8 = new TextDecoder("utf-8", { fatal: true })
|
|
12
|
+
|
|
13
|
+
export const MIN_SECRET_BYTES = 32
|
|
14
|
+
export const MAX_SECRETS = 3
|
|
15
|
+
/** A grant token longer than this is refused before decoding, and never issued. */
|
|
16
|
+
export const MAX_GRANT_CHARS = 10_240
|
|
17
|
+
export const MAX_KEYS_PER_FRAME = 64
|
|
18
|
+
/** The cache-tag charset: a letter, then up to 127 of `A-Z a-z 0-9 . _ : / -`. */
|
|
19
|
+
export const KEY_PATTERN = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/
|
|
20
|
+
export const SID_PATTERN = /^[0-9a-f]{32}$/
|
|
21
|
+
|
|
22
|
+
const KID_BYTES = 16
|
|
23
|
+
const NONCE_BYTES = 12
|
|
24
|
+
const GCM_TAG_BYTES = 16
|
|
25
|
+
const HOUR_MS = 3_600_000
|
|
26
|
+
const HOUR_SECONDS = 3600
|
|
27
|
+
const IAT_SKEW_SECONDS = 60
|
|
28
|
+
const SUBKEY_CACHE_SIZE = 1024
|
|
29
|
+
const TAG_BYTES = 16
|
|
30
|
+
/**
|
|
31
|
+
* Seals under one subkey before it refuses more. Random 96-bit nonces stay safe for about 2^32
|
|
32
|
+
* encryptions per key; an hourly per-process subkey at 10,000 grants a second sees 3.6e7.
|
|
33
|
+
*/
|
|
34
|
+
export const MAX_SEALS_PER_SUBKEY = 2 ** 31
|
|
35
|
+
|
|
36
|
+
const INFO_SECRET_ID = TEXT.encode("nifra live secret id v1")
|
|
37
|
+
const INFO_GRANT = TEXT.encode("nifra live grant v1")
|
|
38
|
+
const INFO_TAG = TEXT.encode("nifra live tag v1")
|
|
39
|
+
const INFO_SUBJECT = TEXT.encode("nifra live subject v1")
|
|
40
|
+
const NO_SALT = new Uint8Array(0)
|
|
41
|
+
|
|
42
|
+
export interface GrantOwner {
|
|
43
|
+
/** The keys the owner's loader declared. */
|
|
44
|
+
readonly k: readonly string[]
|
|
45
|
+
/** The bus seq read before the owner's data was loaded. */
|
|
46
|
+
readonly w: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GrantClaims {
|
|
50
|
+
readonly v: 1
|
|
51
|
+
readonly sid: string
|
|
52
|
+
readonly sub: string | null
|
|
53
|
+
readonly path: string
|
|
54
|
+
readonly frame: string
|
|
55
|
+
readonly owners: Readonly<Record<string, GrantOwner>>
|
|
56
|
+
/** The seq read at issuance: revocations above it refuse the grant. */
|
|
57
|
+
readonly s: number
|
|
58
|
+
/** Renewals re-signed without loaders since the last load that ran them all. */
|
|
59
|
+
readonly r: number
|
|
60
|
+
readonly iat: number
|
|
61
|
+
readonly exp: number
|
|
62
|
+
readonly aud: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type GrantInput = Omit<GrantClaims, "v" | "iat" | "exp" | "aud">
|
|
66
|
+
|
|
67
|
+
export interface OpenedGrant {
|
|
68
|
+
readonly claims: GrantClaims
|
|
69
|
+
/** Index of the secret that decrypted it; that secret also derives the grant's tags. */
|
|
70
|
+
readonly secret: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Keyring {
|
|
74
|
+
readonly appId: string
|
|
75
|
+
/** Grant lifetime in seconds. */
|
|
76
|
+
readonly ttl: number
|
|
77
|
+
readonly secretCount: number
|
|
78
|
+
/** Each secret's public 32-bit id, newest first: a token names its secret by it. */
|
|
79
|
+
readonly secretIds: readonly number[]
|
|
80
|
+
/**
|
|
81
|
+
* Encrypt under the first secret; `undefined` when the token would exceed {@link MAX_GRANT_CHARS} or
|
|
82
|
+
* the hour's subkey reached {@link MAX_SEALS_PER_SUBKEY}.
|
|
83
|
+
*/
|
|
84
|
+
seal(input: GrantInput, nowMs: number): Promise<string | undefined>
|
|
85
|
+
/** Authenticate and decrypt, or `undefined` for anything not issued by this keyring and still valid. */
|
|
86
|
+
open(token: unknown, nowMs: number): Promise<OpenedGrant | undefined>
|
|
87
|
+
/** The 128-bit wire tag of a key under one secret (default: the first). */
|
|
88
|
+
tag(key: string, secret?: number): Promise<string>
|
|
89
|
+
/**
|
|
90
|
+
* The subject's stable opaque tag under each listed secret, newest first. Across a rotation the
|
|
91
|
+
* same viewer keeps one tag in common, whichever process served the load.
|
|
92
|
+
*/
|
|
93
|
+
subjectTags(subject: string | null): Promise<string[]>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A rotation list: newest first, never empty. */
|
|
97
|
+
export type LiveSecrets = readonly [string, ...string[]]
|
|
98
|
+
|
|
99
|
+
export interface KeyringOptions {
|
|
100
|
+
readonly secrets: LiveSecrets
|
|
101
|
+
readonly appId: string
|
|
102
|
+
readonly ttl: number
|
|
103
|
+
/** Fixed per process; random when omitted. Tests pin it. */
|
|
104
|
+
readonly processId?: Uint8Array
|
|
105
|
+
/** Tests lower it; see {@link MAX_SEALS_PER_SUBKEY}. */
|
|
106
|
+
readonly maxSealsPerSubkey?: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface SecretEntry {
|
|
110
|
+
readonly index: number
|
|
111
|
+
readonly base: CryptoKey
|
|
112
|
+
readonly id: Uint8Array<ArrayBuffer>
|
|
113
|
+
readonly idKey: number
|
|
114
|
+
readonly tagKey: CryptoKey
|
|
115
|
+
readonly subjectKey: CryptoKey
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Synchronous boot checks: count and strength. Duplicate ids need derivation, see {@link createKeyring}. */
|
|
119
|
+
export function assertLiveSecrets(secrets: readonly unknown[]): asserts secrets is LiveSecrets {
|
|
120
|
+
if (secrets.length === 0) throw new TypeError("[nifra/web] live: `secret` lists no secret")
|
|
121
|
+
if (secrets.length > MAX_SECRETS) {
|
|
122
|
+
throw new TypeError(
|
|
123
|
+
`[nifra/web] live: \`secret\` lists ${secrets.length} secrets; at most ${MAX_SECRETS} are allowed, since every publish goes out under each one. Remove a demoted secret once grantTtl plus the renewal window has passed.`,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
for (const secret of secrets) {
|
|
127
|
+
const length = typeof secret === "string" ? TEXT.encode(secret).length : -1
|
|
128
|
+
if (length < MIN_SECRET_BYTES) {
|
|
129
|
+
throw new TypeError(
|
|
130
|
+
`[nifra/web] live: every secret must be a string of at least ${MIN_SECRET_BYTES} bytes (256-bit). Generate one with: openssl rand -base64 32`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Derive every key a secret needs. Rejects when two secrets share a `secretId`. */
|
|
137
|
+
export async function createKeyring(options: KeyringOptions): Promise<Keyring> {
|
|
138
|
+
assertLiveSecrets(options.secrets)
|
|
139
|
+
const appSalt = TEXT.encode(options.appId)
|
|
140
|
+
const processId = options.processId ?? crypto.getRandomValues(new Uint8Array(8))
|
|
141
|
+
if (processId.length !== 8) throw new TypeError("[nifra/web] live: processId must be 8 bytes")
|
|
142
|
+
const derive = async (secret: string, index: number): Promise<SecretEntry> => {
|
|
143
|
+
const base = await crypto.subtle.importKey("raw", TEXT.encode(secret), "HKDF", false, [
|
|
144
|
+
"deriveBits",
|
|
145
|
+
"deriveKey",
|
|
146
|
+
])
|
|
147
|
+
const id = new Uint8Array(await hkdf(base, NO_SALT, INFO_SECRET_ID, 32)).slice(0, 4)
|
|
148
|
+
const tagKey = await hmacKey(new Uint8Array(await hkdf(base, appSalt, INFO_TAG, 32)))
|
|
149
|
+
const subjectKey = await hmacKey(new Uint8Array(await hkdf(base, appSalt, INFO_SUBJECT, 32)))
|
|
150
|
+
return { index, base, id, idKey: readU32(id, 0), tagKey, subjectKey }
|
|
151
|
+
}
|
|
152
|
+
const [head, ...rest] = options.secrets
|
|
153
|
+
const first = await derive(head, 0)
|
|
154
|
+
const entries = [first, ...(await Promise.all(rest.map((secret, i) => derive(secret, i + 1))))]
|
|
155
|
+
const byId = new Map<number, SecretEntry>()
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
if (byId.has(entry.idKey)) {
|
|
158
|
+
throw new TypeError(
|
|
159
|
+
"[nifra/web] live: two configured secrets share a secret id; replace one of them (a repeated secret does this too)",
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
byId.set(entry.idKey, entry)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Least recently used first: a hit is moved to the end. Only a kid that sealed or opened a grant
|
|
166
|
+
// is kept, so forged kids cannot push out the subkeys real grants use.
|
|
167
|
+
const subkeys = new Map<string, Promise<CryptoKey>>()
|
|
168
|
+
const cachedSubkey = (name: string): Promise<CryptoKey> | undefined => {
|
|
169
|
+
const cached = subkeys.get(name)
|
|
170
|
+
if (cached !== undefined) {
|
|
171
|
+
subkeys.delete(name)
|
|
172
|
+
subkeys.set(name, cached)
|
|
173
|
+
}
|
|
174
|
+
return cached
|
|
175
|
+
}
|
|
176
|
+
const remember = (name: string, key: Promise<CryptoKey>): void => {
|
|
177
|
+
if (subkeys.has(name)) return
|
|
178
|
+
if (subkeys.size >= SUBKEY_CACHE_SIZE) {
|
|
179
|
+
const oldest = subkeys.keys().next().value
|
|
180
|
+
if (oldest !== undefined) subkeys.delete(oldest)
|
|
181
|
+
}
|
|
182
|
+
subkeys.set(name, key)
|
|
183
|
+
}
|
|
184
|
+
const deriveSubkey = (entry: SecretEntry, kid: Uint8Array<ArrayBuffer>): Promise<CryptoKey> =>
|
|
185
|
+
crypto.subtle.deriveKey(
|
|
186
|
+
{ name: "HKDF", hash: "SHA-256", salt: kid, info: INFO_GRANT },
|
|
187
|
+
entry.base,
|
|
188
|
+
{ name: "AES-GCM", length: 256 },
|
|
189
|
+
false,
|
|
190
|
+
["encrypt", "decrypt"],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
const ttl = options.ttl
|
|
194
|
+
const appId = options.appId
|
|
195
|
+
const maxSeals = options.maxSealsPerSubkey ?? MAX_SEALS_PER_SUBKEY
|
|
196
|
+
// The subkey `seal` uses changes with the hour, and only `seal` encrypts.
|
|
197
|
+
let sealHour = -1
|
|
198
|
+
let seals = 0
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
appId,
|
|
202
|
+
ttl,
|
|
203
|
+
secretCount: entries.length,
|
|
204
|
+
secretIds: entries.map((entry) => entry.idKey),
|
|
205
|
+
async seal(input, nowMs) {
|
|
206
|
+
const iat = Math.floor(nowMs / 1000)
|
|
207
|
+
const hour = Math.floor(iat / HOUR_SECONDS)
|
|
208
|
+
if (hour !== sealHour) {
|
|
209
|
+
sealHour = hour
|
|
210
|
+
seals = 0
|
|
211
|
+
}
|
|
212
|
+
if (seals >= maxSeals) return undefined
|
|
213
|
+
seals++
|
|
214
|
+
const claims: GrantClaims = { v: 1, ...input, iat, exp: iat + ttl, aud: appId }
|
|
215
|
+
const kid = new Uint8Array(KID_BYTES)
|
|
216
|
+
kid.set(first.id, 0)
|
|
217
|
+
kid.set(processId, 4)
|
|
218
|
+
writeU32(kid, 12, hour)
|
|
219
|
+
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES))
|
|
220
|
+
const name = latin1(kid)
|
|
221
|
+
let key = cachedSubkey(name)
|
|
222
|
+
if (key === undefined) {
|
|
223
|
+
key = deriveSubkey(first, kid)
|
|
224
|
+
remember(name, key)
|
|
225
|
+
}
|
|
226
|
+
const sealed = new Uint8Array(
|
|
227
|
+
await crypto.subtle.encrypt(
|
|
228
|
+
{ name: "AES-GCM", iv: nonce, additionalData: kid, tagLength: GCM_TAG_BYTES * 8 },
|
|
229
|
+
await key,
|
|
230
|
+
TEXT.encode(JSON.stringify(claims)),
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
const token = new Uint8Array(KID_BYTES + NONCE_BYTES + sealed.length)
|
|
234
|
+
token.set(kid, 0)
|
|
235
|
+
token.set(nonce, KID_BYTES)
|
|
236
|
+
token.set(sealed, KID_BYTES + NONCE_BYTES)
|
|
237
|
+
const encoded = toBase64Url(token)
|
|
238
|
+
return encoded.length > MAX_GRANT_CHARS ? undefined : encoded
|
|
239
|
+
},
|
|
240
|
+
async open(token, nowMs) {
|
|
241
|
+
// Size and framing first, then the secret id and hour: none of these refusals costs any crypto.
|
|
242
|
+
if (typeof token !== "string" || token.length > MAX_GRANT_CHARS) return undefined
|
|
243
|
+
const bytes = fromBase64Url(token)
|
|
244
|
+
if (bytes === null || bytes.length < KID_BYTES + NONCE_BYTES + GCM_TAG_BYTES) return undefined
|
|
245
|
+
const entry = byId.get(readU32(bytes, 0))
|
|
246
|
+
if (entry === undefined) return undefined
|
|
247
|
+
const hour = readU32(bytes, 12)
|
|
248
|
+
const nowHour = Math.floor(nowMs / HOUR_MS)
|
|
249
|
+
if (hour !== nowHour && hour !== nowHour - 1) return undefined
|
|
250
|
+
const kid = bytes.slice(0, KID_BYTES)
|
|
251
|
+
const name = latin1(kid)
|
|
252
|
+
const cached = cachedSubkey(name)
|
|
253
|
+
const key = cached ?? deriveSubkey(entry, kid)
|
|
254
|
+
let plain: ArrayBuffer
|
|
255
|
+
try {
|
|
256
|
+
plain = await crypto.subtle.decrypt(
|
|
257
|
+
{
|
|
258
|
+
name: "AES-GCM",
|
|
259
|
+
iv: bytes.slice(KID_BYTES, KID_BYTES + NONCE_BYTES),
|
|
260
|
+
additionalData: kid,
|
|
261
|
+
tagLength: GCM_TAG_BYTES * 8,
|
|
262
|
+
},
|
|
263
|
+
await key,
|
|
264
|
+
bytes.slice(KID_BYTES + NONCE_BYTES),
|
|
265
|
+
)
|
|
266
|
+
} catch {
|
|
267
|
+
return undefined
|
|
268
|
+
}
|
|
269
|
+
if (cached === undefined) remember(name, key)
|
|
270
|
+
let claims: unknown
|
|
271
|
+
try {
|
|
272
|
+
claims = JSON.parse(UTF8.decode(plain))
|
|
273
|
+
} catch {
|
|
274
|
+
return undefined
|
|
275
|
+
}
|
|
276
|
+
return validClaims(claims, appId, ttl, hour, Math.floor(nowMs / 1000))
|
|
277
|
+
? { claims, secret: entry.index }
|
|
278
|
+
: undefined
|
|
279
|
+
},
|
|
280
|
+
async tag(key, secret = 0) {
|
|
281
|
+
const tagKey = (entries[secret] ?? first).tagKey
|
|
282
|
+
const mac = await crypto.subtle.sign("HMAC", tagKey, TEXT.encode(key))
|
|
283
|
+
return toBase64Url(new Uint8Array(mac).slice(0, TAG_BYTES))
|
|
284
|
+
},
|
|
285
|
+
subjectTags(subject) {
|
|
286
|
+
// A prefix per kind: no subject string can collide with the anonymous viewer's input.
|
|
287
|
+
const input = TEXT.encode(subject === null ? "a" : `s${subject}`)
|
|
288
|
+
return Promise.all(
|
|
289
|
+
entries.map(async (entry) =>
|
|
290
|
+
toBase64Url(
|
|
291
|
+
new Uint8Array(await crypto.subtle.sign("HMAC", entry.subjectKey, input)).slice(
|
|
292
|
+
0,
|
|
293
|
+
TAG_BYTES,
|
|
294
|
+
),
|
|
295
|
+
),
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function validClaims(
|
|
303
|
+
value: unknown,
|
|
304
|
+
appId: string,
|
|
305
|
+
ttl: number,
|
|
306
|
+
hour: number,
|
|
307
|
+
nowSeconds: number,
|
|
308
|
+
): value is GrantClaims {
|
|
309
|
+
if (!isRecord(value)) return false
|
|
310
|
+
const { v, sid, sub, path, frame, owners, s, r, iat, exp, aud } = value
|
|
311
|
+
if (v !== 1 || aud !== appId) return false
|
|
312
|
+
if (typeof sid !== "string" || !SID_PATTERN.test(sid)) return false
|
|
313
|
+
if (sub !== null && typeof sub !== "string") return false
|
|
314
|
+
if (typeof path !== "string" || typeof frame !== "string") return false
|
|
315
|
+
if (!isCount(s) || !isCount(r) || !isInteger(iat) || !isInteger(exp)) return false
|
|
316
|
+
if (iat > nowSeconds + IAT_SKEW_SECONDS) return false
|
|
317
|
+
if (Math.floor(iat / HOUR_SECONDS) !== hour) return false
|
|
318
|
+
if (exp - iat !== ttl || exp <= nowSeconds) return false
|
|
319
|
+
if (!isRecord(owners)) return false
|
|
320
|
+
let keys = 0
|
|
321
|
+
for (const owner of Object.values(owners)) {
|
|
322
|
+
if (!isRecord(owner) || !isCount(owner.w) || !Array.isArray(owner.k)) return false
|
|
323
|
+
for (const key of owner.k) if (typeof key !== "string" || !KEY_PATTERN.test(key)) return false
|
|
324
|
+
keys += owner.k.length
|
|
325
|
+
}
|
|
326
|
+
return keys <= MAX_KEYS_PER_FRAME
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
330
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
331
|
+
|
|
332
|
+
const isInteger = (value: unknown): value is number =>
|
|
333
|
+
typeof value === "number" && Number.isSafeInteger(value)
|
|
334
|
+
|
|
335
|
+
const isCount = (value: unknown): value is number => isInteger(value) && value >= 0
|
|
336
|
+
|
|
337
|
+
const hkdf = (
|
|
338
|
+
base: CryptoKey,
|
|
339
|
+
salt: Uint8Array<ArrayBuffer>,
|
|
340
|
+
info: Uint8Array<ArrayBuffer>,
|
|
341
|
+
bytes: number,
|
|
342
|
+
): Promise<ArrayBuffer> =>
|
|
343
|
+
crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, base, bytes * 8)
|
|
344
|
+
|
|
345
|
+
const hmacKey = (raw: Uint8Array<ArrayBuffer>): Promise<CryptoKey> =>
|
|
346
|
+
crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-256" }, false, ["sign"])
|
|
347
|
+
|
|
348
|
+
const view = (bytes: Uint8Array): DataView =>
|
|
349
|
+
new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
350
|
+
|
|
351
|
+
const readU32 = (bytes: Uint8Array, at: number): number => view(bytes).getUint32(at)
|
|
352
|
+
|
|
353
|
+
const writeU32 = (bytes: Uint8Array, at: number, value: number): void =>
|
|
354
|
+
view(bytes).setUint32(at, value)
|
|
355
|
+
|
|
356
|
+
const latin1 = (bytes: Uint8Array): string => {
|
|
357
|
+
let out = ""
|
|
358
|
+
for (const byte of bytes) out += String.fromCharCode(byte)
|
|
359
|
+
return out
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export const toBase64Url = (bytes: Uint8Array): string =>
|
|
363
|
+
btoa(latin1(bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
|
364
|
+
|
|
365
|
+
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
366
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/
|
|
367
|
+
|
|
368
|
+
/** Canonical encodings only: a second spelling of the same bytes is refused. */
|
|
369
|
+
function fromBase64Url(s: string): Uint8Array<ArrayBuffer> | null {
|
|
370
|
+
if (s.length === 0 || s.length % 4 === 1 || !BASE64URL_PATTERN.test(s)) return null
|
|
371
|
+
const remainder = s.length % 4
|
|
372
|
+
const last = BASE64URL_ALPHABET.indexOf(s.charAt(s.length - 1))
|
|
373
|
+
if ((remainder === 2 && (last & 0x0f) !== 0) || (remainder === 3 && (last & 0x03) !== 0))
|
|
374
|
+
return null
|
|
375
|
+
const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - remainder) % 4))
|
|
376
|
+
const bytes = new Uint8Array(bin.length)
|
|
377
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
|
|
378
|
+
return bytes
|
|
379
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A request body as text, read only up to `max` bytes, or the status to refuse it with: 413 past the
|
|
3
|
+
* cap, declared or delivered; 400 for a malformed length or invalid UTF-8.
|
|
4
|
+
*/
|
|
5
|
+
export async function readCapped(req: Request, max: number): Promise<string | 400 | 413> {
|
|
6
|
+
const declared = req.headers.get("content-length")
|
|
7
|
+
if (declared !== null) {
|
|
8
|
+
if (!/^\d+$/.test(declared)) return 400
|
|
9
|
+
if (Number(declared) > max) return 413
|
|
10
|
+
}
|
|
11
|
+
if (req.body === null) return ""
|
|
12
|
+
const reader = req.body.getReader()
|
|
13
|
+
const chunks: Uint8Array[] = []
|
|
14
|
+
let total = 0
|
|
15
|
+
for (;;) {
|
|
16
|
+
const { done, value } = await reader.read()
|
|
17
|
+
if (done) break
|
|
18
|
+
total += value.byteLength
|
|
19
|
+
if (total > max) {
|
|
20
|
+
await Promise.allSettled([reader.cancel()])
|
|
21
|
+
return 413
|
|
22
|
+
}
|
|
23
|
+
chunks.push(value)
|
|
24
|
+
}
|
|
25
|
+
const bytes = new Uint8Array(total)
|
|
26
|
+
let offset = 0
|
|
27
|
+
for (const chunk of chunks) {
|
|
28
|
+
bytes.set(chunk, offset)
|
|
29
|
+
offset += chunk.byteLength
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes)
|
|
33
|
+
} catch {
|
|
34
|
+
return 400
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stored revocations, checked on every use of a grant. An entry is kept for one grant lifetime plus
|
|
3
|
+
* clock skew: by then every grant issued before it has expired.
|
|
4
|
+
*/
|
|
5
|
+
import type { LiveRevocation } from "./bus.ts"
|
|
6
|
+
import type { GrantClaims } from "./keyring.ts"
|
|
7
|
+
|
|
8
|
+
/** Past this many stored entries, one global revocation replaces them all. */
|
|
9
|
+
export const MAX_REVOCATIONS = 100_000
|
|
10
|
+
/** How far clocks may disagree; a revocation is kept this much past the grant lifetime. */
|
|
11
|
+
export const REVOCATION_SKEW_MS = 120_000
|
|
12
|
+
|
|
13
|
+
interface Stamp {
|
|
14
|
+
readonly seq: number
|
|
15
|
+
readonly at: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** What a load checks a grant against. */
|
|
19
|
+
export interface RevocationCheck {
|
|
20
|
+
grantRevoked(claims: GrantClaims): boolean
|
|
21
|
+
keyRevoked(key: string, claims: GrantClaims): boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The stored revocations that bear on a few subjects and keys: what the Workers hub tells a load. */
|
|
25
|
+
export interface RevocationSnapshot {
|
|
26
|
+
readonly floor: number
|
|
27
|
+
readonly subjects: Readonly<Record<string, number>>
|
|
28
|
+
readonly keys: Readonly<Record<string, number>>
|
|
29
|
+
/** By `key + "\0" + subject`. */
|
|
30
|
+
readonly subjectKeys: Readonly<Record<string, number>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Revocations implements RevocationCheck {
|
|
34
|
+
/** A grant issued below this seq is refused whole: `revokeAll`, or history this process lacks. */
|
|
35
|
+
floor: number
|
|
36
|
+
private readonly subjects = new Map<string, Stamp>()
|
|
37
|
+
private readonly keys = new Map<string, Stamp>()
|
|
38
|
+
/** `key + "\0" + subject`; the key charset has no NUL. */
|
|
39
|
+
private readonly subjectKeys = new Map<string, Stamp>()
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
head: number,
|
|
43
|
+
private readonly ttlMs: number,
|
|
44
|
+
private readonly limit = MAX_REVOCATIONS,
|
|
45
|
+
) {
|
|
46
|
+
this.floor = head
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
apply(event: LiveRevocation & { readonly seq: number }, nowMs: number): void {
|
|
50
|
+
this.prune(nowMs)
|
|
51
|
+
const stamp = { seq: event.seq, at: nowMs }
|
|
52
|
+
if (event.key === null) {
|
|
53
|
+
if (event.sub === null) this.raise(event.seq)
|
|
54
|
+
else renew(this.subjects, event.sub, stamp)
|
|
55
|
+
} else if (event.sub === null) {
|
|
56
|
+
renew(this.keys, event.key, stamp)
|
|
57
|
+
} else {
|
|
58
|
+
renew(this.subjectKeys, `${event.key}\0${event.sub}`, stamp)
|
|
59
|
+
}
|
|
60
|
+
if (this.subjects.size + this.keys.size + this.subjectKeys.size > this.limit) {
|
|
61
|
+
console.error(
|
|
62
|
+
`[nifra/web] live: more than ${this.limit} stored revocations; every grant issued before now is refused instead`,
|
|
63
|
+
)
|
|
64
|
+
this.raise(event.seq)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Refuse every grant issued below `seq`; the stored entries are all older, so they go. */
|
|
69
|
+
raise(seq: number): void {
|
|
70
|
+
if (seq > this.floor) this.floor = seq
|
|
71
|
+
this.subjects.clear()
|
|
72
|
+
this.keys.clear()
|
|
73
|
+
this.subjectKeys.clear()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
grantRevoked(claims: GrantClaims): boolean {
|
|
77
|
+
return (
|
|
78
|
+
claims.s < this.floor ||
|
|
79
|
+
(claims.sub !== null && above(this.subjects.get(claims.sub), claims.s))
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
keyRevoked(key: string, claims: GrantClaims): boolean {
|
|
84
|
+
return (
|
|
85
|
+
above(this.keys.get(key), claims.s) ||
|
|
86
|
+
(claims.sub !== null && above(this.subjectKeys.get(`${key}\0${claims.sub}`), claims.s))
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The entries for these subjects and keys, and every pairing of them. */
|
|
91
|
+
snapshot(subjects: readonly string[], keys: readonly string[]): RevocationSnapshot {
|
|
92
|
+
const pairs: string[] = []
|
|
93
|
+
for (const key of keys) for (const sub of subjects) pairs.push(`${key}\0${sub}`)
|
|
94
|
+
return {
|
|
95
|
+
floor: this.floor,
|
|
96
|
+
subjects: pick(this.subjects, subjects),
|
|
97
|
+
keys: pick(this.keys, keys),
|
|
98
|
+
subjectKeys: pick(this.subjectKeys, pairs),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private prune(nowMs: number): void {
|
|
103
|
+
const horizon = nowMs - this.ttlMs - REVOCATION_SKEW_MS
|
|
104
|
+
for (const map of [this.subjects, this.keys, this.subjectKeys]) {
|
|
105
|
+
// Insertion order is age order (see `renew`), so the stale entries are a prefix.
|
|
106
|
+
for (const [name, stamp] of map) {
|
|
107
|
+
if (stamp.at >= horizon) break
|
|
108
|
+
map.delete(name)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const above = (stamp: Stamp | undefined, s: number): boolean => stamp !== undefined && stamp.seq > s
|
|
115
|
+
|
|
116
|
+
/** Built from entries, so a name such as `__proto__` is an own property like any other. */
|
|
117
|
+
function pick(map: Map<string, Stamp>, names: readonly string[]): Record<string, number> {
|
|
118
|
+
const out: [string, number][] = []
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
const stamp = map.get(name)
|
|
121
|
+
if (stamp !== undefined) out.push([name, stamp.seq])
|
|
122
|
+
}
|
|
123
|
+
return Object.fromEntries(out)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Re-insert so the map stays in age order. */
|
|
127
|
+
function renew(map: Map<string, Stamp>, name: string, stamp: Stamp): void {
|
|
128
|
+
map.delete(name)
|
|
129
|
+
map.set(name, stamp)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** {@link RevocationCheck} over a {@link RevocationSnapshot}: the same answers for what it covers. */
|
|
133
|
+
export class RevocationView implements RevocationCheck {
|
|
134
|
+
constructor(private readonly snapshot: RevocationSnapshot) {}
|
|
135
|
+
|
|
136
|
+
grantRevoked(claims: GrantClaims): boolean {
|
|
137
|
+
return (
|
|
138
|
+
claims.s < this.snapshot.floor ||
|
|
139
|
+
(claims.sub !== null && stampAbove(this.snapshot.subjects, claims.sub, claims.s))
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
keyRevoked(key: string, claims: GrantClaims): boolean {
|
|
144
|
+
return (
|
|
145
|
+
stampAbove(this.snapshot.keys, key, claims.s) ||
|
|
146
|
+
(claims.sub !== null &&
|
|
147
|
+
stampAbove(this.snapshot.subjectKeys, `${key}\0${claims.sub}`, claims.s))
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function stampAbove(stamps: Readonly<Record<string, number>>, name: string, s: number): boolean {
|
|
153
|
+
const seq = Object.hasOwn(stamps, name) ? stamps[name] : undefined
|
|
154
|
+
return seq !== undefined && seq > s
|
|
155
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Option checks `live()` and `liveHub()` share: an app and its hub must read the same options alike. */
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_TTL_SECONDS = 600
|
|
4
|
+
export const DEFAULT_MAX_SOCKETS = 10_000
|
|
5
|
+
export const DEFAULT_RELOAD_RATE = 500
|
|
6
|
+
export const DEFAULT_APP_ID = "nifra"
|
|
7
|
+
|
|
8
|
+
export function positive(value: number | undefined, fallback: number, name: string): number {
|
|
9
|
+
const resolved = value ?? fallback
|
|
10
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1) {
|
|
11
|
+
throw new TypeError(`[nifra/web] live: \`${name}\` must be a positive whole number`)
|
|
12
|
+
}
|
|
13
|
+
return resolved
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* At most an hour: a grant's subkey names the hour it was sealed in, and only that hour and the next
|
|
18
|
+
* open it, so a longer lifetime would end early.
|
|
19
|
+
*/
|
|
20
|
+
export function grantTtlOf(value: number | undefined): number {
|
|
21
|
+
const ttl = value ?? DEFAULT_TTL_SECONDS
|
|
22
|
+
if (!Number.isSafeInteger(ttl) || ttl < 60 || ttl > 3600) {
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
"[nifra/web] live: `grantTtl` must be a whole number of seconds from 60 to 3600",
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
return ttl
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function spreadFloorOf(value: number | undefined): number {
|
|
31
|
+
const floor = value ?? 0
|
|
32
|
+
if (!Number.isSafeInteger(floor) || floor < 0 || floor > 5000) {
|
|
33
|
+
throw new TypeError("[nifra/web] live: `spreadFloor` must be a whole number of ms up to 5000")
|
|
34
|
+
}
|
|
35
|
+
return floor
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function appIdOf(value: string | undefined): string {
|
|
39
|
+
const appId = value ?? DEFAULT_APP_ID
|
|
40
|
+
if (typeof appId !== "string" || appId === "" || appId.length > 128) {
|
|
41
|
+
throw new TypeError(
|
|
42
|
+
"[nifra/web] live: `appId` must be a non-empty string of at most 128 characters",
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
return appId
|
|
46
|
+
}
|