@nimbus-sh/worker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -0
- package/dist/_shared/bytes.d.ts +18 -0
- package/dist/_shared/bytes.d.ts.map +1 -0
- package/dist/_shared/bytes.js +17 -0
- package/dist/_shared/cache-stats.d.ts +146 -0
- package/dist/_shared/cache-stats.d.ts.map +1 -0
- package/dist/_shared/cache-stats.js +162 -0
- package/dist/_shared/exports-resolver.d.ts +76 -0
- package/dist/_shared/exports-resolver.d.ts.map +1 -0
- package/dist/_shared/exports-resolver.js +284 -0
- package/dist/_shared/install-phase.d.ts +47 -0
- package/dist/_shared/install-phase.d.ts.map +1 -0
- package/dist/_shared/install-phase.js +1 -0
- package/dist/_shared/real-node-imports.d.ts +38 -0
- package/dist/_shared/real-node-imports.d.ts.map +1 -0
- package/dist/_shared/real-node-imports.js +46 -0
- package/dist/_shared/retry.d.ts +93 -0
- package/dist/_shared/retry.d.ts.map +1 -0
- package/dist/_shared/retry.js +161 -0
- package/dist/_shared/session-id.d.ts +39 -0
- package/dist/_shared/session-id.d.ts.map +1 -0
- package/dist/_shared/session-id.js +127 -0
- package/dist/_shared/session-router.d.ts +104 -0
- package/dist/_shared/session-router.d.ts.map +1 -0
- package/dist/_shared/session-router.js +165 -0
- package/dist/_shared/w7-frame.d.ts +93 -0
- package/dist/_shared/w7-frame.d.ts.map +1 -0
- package/dist/_shared/w7-frame.js +402 -0
- package/dist/auth/index.d.ts +16 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +15 -0
- package/dist/auth/middleware.d.ts +85 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +150 -0
- package/dist/auth/token.d.ts +96 -0
- package/dist/auth/token.d.ts.map +1 -0
- package/dist/auth/token.js +226 -0
- package/dist/auth/types.d.ts +141 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +114 -0
- package/dist/bindings/d1.d.ts +102 -0
- package/dist/bindings/d1.d.ts.map +1 -0
- package/dist/bindings/d1.js +549 -0
- package/dist/bindings/kv.d.ts +82 -0
- package/dist/bindings/kv.d.ts.map +1 -0
- package/dist/bindings/kv.js +290 -0
- package/dist/bindings/r2.d.ts +137 -0
- package/dist/bindings/r2.d.ts.map +1 -0
- package/dist/bindings/r2.js +379 -0
- package/dist/cirrus-npm-cjs.generated.d.ts +17 -0
- package/dist/cirrus-npm-cjs.generated.d.ts.map +1 -0
- package/dist/cirrus-npm-cjs.generated.js +41 -0
- package/dist/cirrus-plugin-react.generated.d.ts +34 -0
- package/dist/cirrus-plugin-react.generated.d.ts.map +1 -0
- package/dist/cirrus-plugin-react.generated.js +33 -0
- package/dist/constants.d.ts +34 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +116 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts +41 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -0
- package/dist/esbuild-wasm-bundle.generated.js +40 -0
- package/dist/facets/cirrus-real.d.ts +147 -0
- package/dist/facets/cirrus-real.d.ts.map +1 -0
- package/dist/facets/cirrus-real.js +981 -0
- package/dist/facets/inner-do-registry.d.ts +41 -0
- package/dist/facets/inner-do-registry.d.ts.map +1 -0
- package/dist/facets/inner-do-registry.js +51 -0
- package/dist/facets/manager.d.ts +314 -0
- package/dist/facets/manager.d.ts.map +1 -0
- package/dist/facets/manager.js +2681 -0
- package/dist/facets/process.d.ts +318 -0
- package/dist/facets/process.d.ts.map +1 -0
- package/dist/facets/process.js +567 -0
- package/dist/facets/real-vite-fs-shim.d.ts +151 -0
- package/dist/facets/real-vite-fs-shim.d.ts.map +1 -0
- package/dist/facets/real-vite-fs-shim.js +1279 -0
- package/dist/facets/real-vite-hmr.d.ts +142 -0
- package/dist/facets/real-vite-hmr.d.ts.map +1 -0
- package/dist/facets/real-vite-hmr.js +476 -0
- package/dist/facets/vite-dev-server.d.ts +293 -0
- package/dist/facets/vite-dev-server.d.ts.map +1 -0
- package/dist/facets/vite-dev-server.js +2580 -0
- package/dist/facets/wasm-swap-registry.d.ts +261 -0
- package/dist/facets/wasm-swap-registry.d.ts.map +1 -0
- package/dist/facets/wasm-swap-registry.js +551 -0
- package/dist/facets/ws-terminal.d.ts +107 -0
- package/dist/facets/ws-terminal.d.ts.map +1 -0
- package/dist/facets/ws-terminal.js +207 -0
- package/dist/git/commands.d.ts +13 -0
- package/dist/git/commands.d.ts.map +1 -0
- package/dist/git/commands.js +633 -0
- package/dist/git/network-facet.d.ts +61 -0
- package/dist/git/network-facet.d.ts.map +1 -0
- package/dist/git/network-facet.js +700 -0
- package/dist/git-bundle.generated.d.ts +16 -0
- package/dist/git-bundle.generated.d.ts.map +1 -0
- package/dist/git-bundle.generated.js +15 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/loaders/child-process/spawn-facet.d.ts +90 -0
- package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -0
- package/dist/loaders/child-process/spawn-facet.js +103 -0
- package/dist/loaders/child-process/spawn-pool.d.ts +110 -0
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -0
- package/dist/loaders/child-process/spawn-pool.js +161 -0
- package/dist/loaders/fanout-pool.d.ts +195 -0
- package/dist/loaders/fanout-pool.d.ts.map +1 -0
- package/dist/loaders/fanout-pool.js +326 -0
- package/dist/loaders/generated-workers.d.ts +26 -0
- package/dist/loaders/generated-workers.d.ts.map +1 -0
- package/dist/loaders/generated-workers.js +25 -0
- package/dist/loaders/index.d.ts +15 -0
- package/dist/loaders/index.d.ts.map +1 -0
- package/dist/loaders/index.js +13 -0
- package/dist/loaders/loader-pool.d.ts +273 -0
- package/dist/loaders/loader-pool.d.ts.map +1 -0
- package/dist/loaders/loader-pool.js +624 -0
- package/dist/loaders/npm-resolve-preamble.d.ts +34 -0
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -0
- package/dist/loaders/npm-resolve-preamble.js +237 -0
- package/dist/loaders/pre-bundle-preamble.d.ts +41 -0
- package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -0
- package/dist/loaders/pre-bundle-preamble.js +114 -0
- package/dist/loaders/vendor/codegen.d.ts +24 -0
- package/dist/loaders/vendor/codegen.d.ts.map +1 -0
- package/dist/loaders/vendor/codegen.js +44 -0
- package/dist/loaders/vendor/errors.d.ts +24 -0
- package/dist/loaders/vendor/errors.d.ts.map +1 -0
- package/dist/loaders/vendor/errors.js +46 -0
- package/dist/loaders/vendor/index.d.ts +16 -0
- package/dist/loaders/vendor/index.d.ts.map +1 -0
- package/dist/loaders/vendor/index.js +11 -0
- package/dist/loaders/vendor/pool.d.ts +88 -0
- package/dist/loaders/vendor/pool.d.ts.map +1 -0
- package/dist/loaders/vendor/pool.js +353 -0
- package/dist/loaders/vendor/primitives.d.ts +11 -0
- package/dist/loaders/vendor/primitives.d.ts.map +1 -0
- package/dist/loaders/vendor/primitives.js +33 -0
- package/dist/loaders/vendor/serialize.d.ts +3 -0
- package/dist/loaders/vendor/serialize.d.ts.map +1 -0
- package/dist/loaders/vendor/serialize.js +25 -0
- package/dist/loaders/vendor/types.d.ts +60 -0
- package/dist/loaders/vendor/types.d.ts.map +1 -0
- package/dist/loaders/vendor/types.js +4 -0
- package/dist/npm/cache.d.ts +136 -0
- package/dist/npm/cache.d.ts.map +1 -0
- package/dist/npm/cache.js +429 -0
- package/dist/npm/install-batch-facet.d.ts +129 -0
- package/dist/npm/install-batch-facet.d.ts.map +1 -0
- package/dist/npm/install-batch-facet.js +598 -0
- package/dist/npm/install-facet.d.ts +29 -0
- package/dist/npm/install-facet.d.ts.map +1 -0
- package/dist/npm/install-facet.js +9 -0
- package/dist/npm/installer.d.ts +239 -0
- package/dist/npm/installer.d.ts.map +1 -0
- package/dist/npm/installer.js +2022 -0
- package/dist/npm/npx-install.d.ts +74 -0
- package/dist/npm/npx-install.d.ts.map +1 -0
- package/dist/npm/npx-install.js +302 -0
- package/dist/npm/pre-bundle-facet.d.ts +168 -0
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -0
- package/dist/npm/pre-bundle-facet.js +653 -0
- package/dist/npm/r2-cache.d.ts +286 -0
- package/dist/npm/r2-cache.d.ts.map +1 -0
- package/dist/npm/r2-cache.js +506 -0
- package/dist/npm/resolve-facet.d.ts +202 -0
- package/dist/npm/resolve-facet.d.ts.map +1 -0
- package/dist/npm/resolve-facet.js +833 -0
- package/dist/npm/resolve-one-facet.d.ts +204 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -0
- package/dist/npm/resolve-one-facet.js +437 -0
- package/dist/npm/resolver.d.ts +109 -0
- package/dist/npm/resolver.d.ts.map +1 -0
- package/dist/npm/resolver.js +899 -0
- package/dist/npm/tarball-stream.d.ts +85 -0
- package/dist/npm/tarball-stream.d.ts.map +1 -0
- package/dist/npm/tarball-stream.js +205 -0
- package/dist/npm/tarball.d.ts +51 -0
- package/dist/npm/tarball.d.ts.map +1 -0
- package/dist/npm/tarball.js +185 -0
- package/dist/observability/diag-counters.d.ts +292 -0
- package/dist/observability/diag-counters.d.ts.map +1 -0
- package/dist/observability/diag-counters.js +302 -0
- package/dist/observability/heap-estimate.d.ts +166 -0
- package/dist/observability/heap-estimate.d.ts.map +1 -0
- package/dist/observability/heap-estimate.js +192 -0
- package/dist/observability/heavy-alloc-coord.d.ts +95 -0
- package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
- package/dist/observability/heavy-alloc-coord.js +149 -0
- package/dist/observability/oom-classify.d.ts +56 -0
- package/dist/observability/oom-classify.d.ts.map +1 -0
- package/dist/observability/oom-classify.js +118 -0
- package/dist/observability/oom-discriminator.d.ts +192 -0
- package/dist/observability/oom-discriminator.d.ts.map +1 -0
- package/dist/observability/oom-discriminator.js +233 -0
- package/dist/real-vite-bundle.generated.d.ts +46 -0
- package/dist/real-vite-bundle.generated.d.ts.map +1 -0
- package/dist/real-vite-bundle.generated.js +53 -0
- package/dist/replica/routing.d.ts +171 -0
- package/dist/replica/routing.d.ts.map +1 -0
- package/dist/replica/routing.js +288 -0
- package/dist/replica/suspension.d.ts +36 -0
- package/dist/replica/suspension.d.ts.map +1 -0
- package/dist/replica/suspension.js +49 -0
- package/dist/router/index.d.ts +156 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +220 -0
- package/dist/runtime/assets-loader.d.ts +71 -0
- package/dist/runtime/assets-loader.d.ts.map +1 -0
- package/dist/runtime/assets-loader.js +128 -0
- package/dist/runtime/barrel-detect.d.ts +46 -0
- package/dist/runtime/barrel-detect.d.ts.map +1 -0
- package/dist/runtime/barrel-detect.js +73 -0
- package/dist/runtime/barrel-synthesizer.d.ts +181 -0
- package/dist/runtime/barrel-synthesizer.d.ts.map +1 -0
- package/dist/runtime/barrel-synthesizer.js +541 -0
- package/dist/runtime/bun-repl.d.ts +46 -0
- package/dist/runtime/bun-repl.d.ts.map +1 -0
- package/dist/runtime/bun-repl.js +326 -0
- package/dist/runtime/bun-runner.d.ts +66 -0
- package/dist/runtime/bun-runner.d.ts.map +1 -0
- package/dist/runtime/bun-runner.js +219 -0
- package/dist/runtime/clang-runner.d.ts +38 -0
- package/dist/runtime/clang-runner.d.ts.map +1 -0
- package/dist/runtime/clang-runner.js +1033 -0
- package/dist/runtime/comment-strip.d.ts +37 -0
- package/dist/runtime/comment-strip.d.ts.map +1 -0
- package/dist/runtime/comment-strip.js +74 -0
- package/dist/runtime/esbuild-service.d.ts +201 -0
- package/dist/runtime/esbuild-service.d.ts.map +1 -0
- package/dist/runtime/esbuild-service.js +1444 -0
- package/dist/runtime/esbuild-wasm-bytes.d.ts +87 -0
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -0
- package/dist/runtime/esbuild-wasm-bytes.js +131 -0
- package/dist/runtime/framework-detect.d.ts +71 -0
- package/dist/runtime/framework-detect.d.ts.map +1 -0
- package/dist/runtime/framework-detect.js +178 -0
- package/dist/runtime/long-running-handle.d.ts +133 -0
- package/dist/runtime/long-running-handle.d.ts.map +1 -0
- package/dist/runtime/long-running-handle.js +192 -0
- package/dist/runtime/node-repl.d.ts +36 -0
- package/dist/runtime/node-repl.d.ts.map +1 -0
- package/dist/runtime/node-repl.js +293 -0
- package/dist/runtime/node-runner.d.ts +111 -0
- package/dist/runtime/node-runner.d.ts.map +1 -0
- package/dist/runtime/node-runner.js +194 -0
- package/dist/runtime/node-shims.d.ts +24 -0
- package/dist/runtime/node-shims.d.ts.map +1 -0
- package/dist/runtime/node-shims.js +3019 -0
- package/dist/runtime/package-manager.d.ts +118 -0
- package/dist/runtime/package-manager.d.ts.map +1 -0
- package/dist/runtime/package-manager.js +420 -0
- package/dist/runtime/port-registry.d.ts +80 -0
- package/dist/runtime/port-registry.d.ts.map +1 -0
- package/dist/runtime/port-registry.js +166 -0
- package/dist/runtime/process-logs-api.d.ts +102 -0
- package/dist/runtime/process-logs-api.d.ts.map +1 -0
- package/dist/runtime/process-logs-api.js +253 -0
- package/dist/runtime/process-logs.d.ts +311 -0
- package/dist/runtime/process-logs.d.ts.map +1 -0
- package/dist/runtime/process-logs.js +656 -0
- package/dist/runtime/process-table.d.ts +65 -0
- package/dist/runtime/process-table.d.ts.map +1 -0
- package/dist/runtime/process-table.js +107 -0
- package/dist/runtime/project-detect.d.ts +21 -0
- package/dist/runtime/project-detect.d.ts.map +1 -0
- package/dist/runtime/project-detect.js +46 -0
- package/dist/runtime/python-repl.d.ts +66 -0
- package/dist/runtime/python-repl.d.ts.map +1 -0
- package/dist/runtime/python-repl.js +563 -0
- package/dist/runtime/python-runner.d.ts +50 -0
- package/dist/runtime/python-runner.d.ts.map +1 -0
- package/dist/runtime/python-runner.js +870 -0
- package/dist/runtime/repl-session.d.ts +160 -0
- package/dist/runtime/repl-session.d.ts.map +1 -0
- package/dist/runtime/repl-session.js +393 -0
- package/dist/runtime/require-resolver.d.ts +52 -0
- package/dist/runtime/require-resolver.d.ts.map +1 -0
- package/dist/runtime/require-resolver.js +690 -0
- package/dist/runtime/router-basename.d.ts +36 -0
- package/dist/runtime/router-basename.d.ts.map +1 -0
- package/dist/runtime/router-basename.js +310 -0
- package/dist/runtime/ruby-repl.d.ts +43 -0
- package/dist/runtime/ruby-repl.d.ts.map +1 -0
- package/dist/runtime/ruby-repl.js +229 -0
- package/dist/runtime/ruby-runner.d.ts +62 -0
- package/dist/runtime/ruby-runner.d.ts.map +1 -0
- package/dist/runtime/ruby-runner.js +737 -0
- package/dist/runtime/runtime-catalog.d.ts +99 -0
- package/dist/runtime/runtime-catalog.d.ts.map +1 -0
- package/dist/runtime/runtime-catalog.js +183 -0
- package/dist/runtime/runtime-registry.d.ts +136 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -0
- package/dist/runtime/runtime-registry.js +318 -0
- package/dist/runtime/streams.d.ts +13 -0
- package/dist/runtime/streams.d.ts.map +1 -0
- package/dist/runtime/streams.js +402 -0
- package/dist/runtime/wasi-instance.d.ts +190 -0
- package/dist/runtime/wasi-instance.d.ts.map +1 -0
- package/dist/runtime/wasi-instance.js +2044 -0
- package/dist/runtime/wasm-runner.d.ts +117 -0
- package/dist/runtime/wasm-runner.d.ts.map +1 -0
- package/dist/runtime/wasm-runner.js +705 -0
- package/dist/session/bindings.d.ts +168 -0
- package/dist/session/bindings.d.ts.map +1 -0
- package/dist/session/bindings.js +524 -0
- package/dist/session/ctx-exports.d.ts +16 -0
- package/dist/session/ctx-exports.d.ts.map +1 -0
- package/dist/session/ctx-exports.js +22 -0
- package/dist/session/diag.d.ts +62 -0
- package/dist/session/diag.d.ts.map +1 -0
- package/dist/session/diag.js +118 -0
- package/dist/session/fs-watch.d.ts +132 -0
- package/dist/session/fs-watch.d.ts.map +1 -0
- package/dist/session/fs-watch.js +255 -0
- package/dist/session/helpers.d.ts +150 -0
- package/dist/session/helpers.d.ts.map +1 -0
- package/dist/session/helpers.js +472 -0
- package/dist/session/hibernation.d.ts +145 -0
- package/dist/session/hibernation.d.ts.map +1 -0
- package/dist/session/hibernation.js +385 -0
- package/dist/session/init-phases.d.ts +87 -0
- package/dist/session/init-phases.d.ts.map +1 -0
- package/dist/session/init-phases.js +137 -0
- package/dist/session/init.d.ts +46 -0
- package/dist/session/init.d.ts.map +1 -0
- package/dist/session/init.js +3579 -0
- package/dist/session/keys.d.ts +56 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +55 -0
- package/dist/session/nimbus-session.d.ts +434 -0
- package/dist/session/nimbus-session.d.ts.map +1 -0
- package/dist/session/nimbus-session.js +1084 -0
- package/dist/session/programmatic.d.ts +105 -0
- package/dist/session/programmatic.d.ts.map +1 -0
- package/dist/session/programmatic.js +326 -0
- package/dist/session/replica-routes.d.ts +68 -0
- package/dist/session/replica-routes.d.ts.map +1 -0
- package/dist/session/replica-routes.js +77 -0
- package/dist/session/routes.d.ts +29 -0
- package/dist/session/routes.d.ts.map +1 -0
- package/dist/session/routes.js +1331 -0
- package/dist/session/rpc.d.ts +323 -0
- package/dist/session/rpc.d.ts.map +1 -0
- package/dist/session/rpc.js +808 -0
- package/dist/session/state-store.d.ts +210 -0
- package/dist/session/state-store.d.ts.map +1 -0
- package/dist/session/state-store.js +459 -0
- package/dist/session/supervisor-rpc.d.ts +269 -0
- package/dist/session/supervisor-rpc.d.ts.map +1 -0
- package/dist/session/supervisor-rpc.js +437 -0
- package/dist/session/ws-hibernation-config.d.ts +64 -0
- package/dist/session/ws-hibernation-config.d.ts.map +1 -0
- package/dist/session/ws-hibernation-config.js +89 -0
- package/dist/session/ws.d.ts +95 -0
- package/dist/session/ws.d.ts.map +1 -0
- package/dist/session/ws.js +395 -0
- package/dist/shell/features.d.ts +410 -0
- package/dist/shell/features.d.ts.map +1 -0
- package/dist/shell/features.js +1885 -0
- package/dist/shell/unix-commands.d.ts +15 -0
- package/dist/shell/unix-commands.d.ts.map +1 -0
- package/dist/shell/unix-commands.js +3703 -0
- package/dist/stubs/lifo-ui.d.ts +10 -0
- package/dist/stubs/lifo-ui.d.ts.map +1 -0
- package/dist/stubs/lifo-ui.js +1 -0
- package/dist/tailwind-play.generated.d.ts +19 -0
- package/dist/tailwind-play.generated.d.ts.map +1 -0
- package/dist/tailwind-play.generated.js +18 -0
- package/dist/vfs/dev-provider.d.ts +64 -0
- package/dist/vfs/dev-provider.d.ts.map +1 -0
- package/dist/vfs/dev-provider.js +191 -0
- package/dist/vfs/events.d.ts +51 -0
- package/dist/vfs/events.d.ts.map +1 -0
- package/dist/vfs/events.js +127 -0
- package/dist/vfs/path.d.ts +30 -0
- package/dist/vfs/path.d.ts.map +1 -0
- package/dist/vfs/path.js +41 -0
- package/dist/vfs/seed-project.d.ts +62 -0
- package/dist/vfs/seed-project.d.ts.map +1 -0
- package/dist/vfs/seed-project.js +1045 -0
- package/dist/vfs/sqlite-vfs.d.ts +435 -0
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
- package/dist/vfs/sqlite-vfs.js +1670 -0
- package/dist/vfs/symlink-registry.d.ts +64 -0
- package/dist/vfs/symlink-registry.d.ts.map +1 -0
- package/dist/vfs/symlink-registry.js +137 -0
- package/dist/wrangler/nimbus-wrangler.d.ts +157 -0
- package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -0
- package/dist/wrangler/nimbus-wrangler.js +931 -0
- package/package.json +77 -0
- package/public/_assets/cirrus-plugin-react.bundle.js +70740 -0
- package/public/_assets/esbuild-0.24.2.wasm +0 -0
- package/public/_assets/real-vite-bundle.js +101478 -0
- package/public/_assets/rollup.wasm +0 -0
- package/public/favicon.svg +8 -0
- package/public/index.html +565 -0
- package/public/s/index.html +2379 -0
- package/scripts/bundle-esbuild-wasm.mjs +244 -0
- package/scripts/bundle-facet-workers.mjs +137 -0
- package/scripts/bundle-git.mjs +118 -0
- package/scripts/bundle-npm-cjs.mjs +363 -0
- package/scripts/bundle-plugin-react.mjs +480 -0
- package/scripts/bundle-real-vite.mjs +959 -0
- package/scripts/bundle-runtime.mjs +600 -0
- package/scripts/bundle-tailwind-play.mjs +130 -0
- package/scripts/patch-lifo-ui.mjs +131 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* retry.ts — small, dependency-free retry helper for flaky HTTP GETs.
|
|
3
|
+
*
|
|
4
|
+
* The npm path (resolver packument fetches + tarball fetches) talks to
|
|
5
|
+
* registry.npmjs.org via Cloudflare's edge. Either hop can produce a
|
|
6
|
+
* transient 5xx — we've seen HTTP 503 on a single tarball out of 456
|
|
7
|
+
* kill a whole install. npm's client retries 5xx by default; we didn't,
|
|
8
|
+
* until this file. Any single transient failure across hundreds of
|
|
9
|
+
* packages = whole install dead.
|
|
10
|
+
*
|
|
11
|
+
* Scope:
|
|
12
|
+
* - Retries on 5xx (500/502/503/504 especially) and network errors
|
|
13
|
+
* (fetch rejection, AbortError timeout, etc.).
|
|
14
|
+
* - Does NOT retry on 4xx (those are hard: 404 means the package or
|
|
15
|
+
* version genuinely doesn't exist; retrying is wrong and wastes time).
|
|
16
|
+
* - Budget: 3 retries with jittered exponential backoff (500/1500/4500 ms
|
|
17
|
+
* ±25% jitter). Worst-case added latency per package ≈ 6.5 s in the
|
|
18
|
+
* fully-degraded path. Tighter than npm's ~5×60s because dev iteration
|
|
19
|
+
* speed matters more than absolute resilience here.
|
|
20
|
+
*
|
|
21
|
+
* Intentionally NOT exported to the facet isolate: the facet serializes
|
|
22
|
+
* its task function via fn.toString() and can't cross a module boundary,
|
|
23
|
+
* so `src/npm-install-facet.ts` duplicates this retry loop inline inside
|
|
24
|
+
* `fetchAndStagePackage`. Keep the two in sync if behaviour changes.
|
|
25
|
+
*/
|
|
26
|
+
/** Default retry count AFTER the first attempt (3 = up to 4 total attempts). */
|
|
27
|
+
export const DEFAULT_RETRIES = 3;
|
|
28
|
+
/** Base backoff delays in ms. Index = retry attempt (0-indexed). */
|
|
29
|
+
export const BACKOFF_MS = [500, 1500, 4500];
|
|
30
|
+
/** Jitter ±25% of the base delay, uniformly distributed. */
|
|
31
|
+
export function jittered(baseMs) {
|
|
32
|
+
const span = baseMs * 0.25;
|
|
33
|
+
// Math.random is fine here — cryptographic randomness is not required;
|
|
34
|
+
// the goal is to break up synchronized retries across concurrent fetches
|
|
35
|
+
// after a shared upstream hiccup.
|
|
36
|
+
return Math.max(0, Math.round(baseMs + (Math.random() * 2 - 1) * span));
|
|
37
|
+
}
|
|
38
|
+
/** True if an HTTP status should trigger a retry. */
|
|
39
|
+
export function isRetriableStatus(status) {
|
|
40
|
+
// 5xx only. 429 (Too Many Requests) is arguably retriable too, but
|
|
41
|
+
// registry.npmjs.org is fronted by Cloudflare which generally rate-
|
|
42
|
+
// limits per-IP silently rather than with 429, and if we ever DO see
|
|
43
|
+
// 429 the right handling is Retry-After header honor, which we don't
|
|
44
|
+
// implement here. Keep the surface minimal.
|
|
45
|
+
return status >= 500 && status <= 599;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* fetch() wrapper with 5xx + network-error retry and jittered backoff.
|
|
49
|
+
*
|
|
50
|
+
* On a 4xx response, returns the Response object as-is (caller decides
|
|
51
|
+
* whether that's an error). On a 5xx or network-level failure, retries
|
|
52
|
+
* up to `retries` times with BACKOFF_MS[attempt] ± 25% jitter.
|
|
53
|
+
*
|
|
54
|
+
* When retries are exhausted, either:
|
|
55
|
+
* - Returns the last (5xx) Response, so the caller can read .status for
|
|
56
|
+
* error reporting, OR
|
|
57
|
+
* - Re-throws the last network error (caller never sees a Response).
|
|
58
|
+
*
|
|
59
|
+
* Callers that previously did `if (!resp.ok) throw ...` retain that shape;
|
|
60
|
+
* the only change is that transient failures are recovered before the
|
|
61
|
+
* throw instead of killing the whole install on the first hiccup.
|
|
62
|
+
*/
|
|
63
|
+
export async function retryableFetch(url, init, opts) {
|
|
64
|
+
const totalRetries = Math.max(0, opts?.retries ?? DEFAULT_RETRIES);
|
|
65
|
+
const name = opts?.name ?? url;
|
|
66
|
+
const doFetch = opts?.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
67
|
+
let lastError;
|
|
68
|
+
let lastResp;
|
|
69
|
+
for (let attempt = 0; attempt <= totalRetries; attempt++) {
|
|
70
|
+
// Per-attempt timeout via a fresh AbortController so a single slow
|
|
71
|
+
// response can't consume the whole retry budget. We merge the caller's
|
|
72
|
+
// signal (if any) by forwarding its abort to ours.
|
|
73
|
+
let timer;
|
|
74
|
+
let attemptInit = init;
|
|
75
|
+
const perAttemptMs = opts?.perAttemptTimeoutMs;
|
|
76
|
+
let localController;
|
|
77
|
+
if (perAttemptMs && perAttemptMs > 0) {
|
|
78
|
+
localController = new AbortController();
|
|
79
|
+
timer = setTimeout(() => localController.abort(), perAttemptMs);
|
|
80
|
+
const callerSignal = init?.signal;
|
|
81
|
+
if (callerSignal) {
|
|
82
|
+
if (callerSignal.aborted)
|
|
83
|
+
localController.abort();
|
|
84
|
+
else
|
|
85
|
+
callerSignal.addEventListener('abort', () => localController.abort(), { once: true });
|
|
86
|
+
}
|
|
87
|
+
attemptInit = { ...(init ?? {}), signal: localController.signal };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const resp = await doFetch(url, attemptInit);
|
|
91
|
+
if (timer)
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
if (resp.ok || !isRetriableStatus(resp.status)) {
|
|
94
|
+
// Success or 4xx — don't retry. Return as-is; caller checks .ok.
|
|
95
|
+
return resp;
|
|
96
|
+
}
|
|
97
|
+
// 5xx: drain the body to release the underlying stream.
|
|
98
|
+
try {
|
|
99
|
+
await resp.body?.cancel();
|
|
100
|
+
}
|
|
101
|
+
catch { /* best-effort */ }
|
|
102
|
+
lastResp = resp;
|
|
103
|
+
lastError = undefined;
|
|
104
|
+
if (attempt === totalRetries) {
|
|
105
|
+
// Out of retries — return the 5xx Response so the caller can
|
|
106
|
+
// surface the final status. Caller (e.g. npm-resolver.ts) is
|
|
107
|
+
// responsible for disposing the stub via Symbol.dispose after
|
|
108
|
+
// reading status. (Matches the previous !resp.ok throw shape if
|
|
109
|
+
// the caller checks .ok next.)
|
|
110
|
+
return resp;
|
|
111
|
+
}
|
|
112
|
+
// We're going to retry — dispose the stub NOW so it doesn't leak.
|
|
113
|
+
//
|
|
114
|
+
// When `doFetch` is the supervisor's fetch-proxy entrypoint
|
|
115
|
+
// (src/nimbus-session.ts:1729-1747), `resp` is an RPC stub returned
|
|
116
|
+
// from `entrypoint.fetch(...)`. Workerd auto-disposes such stubs
|
|
117
|
+
// only at the end of the enclosing event-handler context — which
|
|
118
|
+
// for `npm install` means "until the whole install completes".
|
|
119
|
+
// Cancelling `resp.body` (above) releases the underlying stream
|
|
120
|
+
// but does NOT release the stub itself. Across an install with N
|
|
121
|
+
// retries fired, that's N leaked stubs accumulating alongside the
|
|
122
|
+
// resolver's own per-packument stubs (already disposed in
|
|
123
|
+
// npm-resolver.ts:307-317). Each leaked stub keeps its server-side
|
|
124
|
+
// counterpart pinned until the deferred-destruction queue runs at
|
|
125
|
+
// request end, contributing to the queueState != ACTIVE fatal
|
|
126
|
+
// documented in WORKERD-CRASH.md.
|
|
127
|
+
//
|
|
128
|
+
// Same disposer pattern as src/npm-resolver.ts:312-316.
|
|
129
|
+
try {
|
|
130
|
+
const disposerKey = Symbol.dispose;
|
|
131
|
+
const disposer = disposerKey ? resp?.[disposerKey] : undefined;
|
|
132
|
+
if (typeof disposer === 'function')
|
|
133
|
+
disposer.call(resp);
|
|
134
|
+
}
|
|
135
|
+
catch { /* best-effort */ }
|
|
136
|
+
const delayMs = jittered(BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]);
|
|
137
|
+
opts?.onRetry?.(attempt + 1, totalRetries, delayMs, `HTTP ${resp.status}`);
|
|
138
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
if (timer)
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
// Network-level failure (fetch rejection, timeout via AbortError,
|
|
144
|
+
// DNS, connection reset). Retry the same way as a 5xx.
|
|
145
|
+
lastError = e;
|
|
146
|
+
lastResp = undefined;
|
|
147
|
+
if (attempt === totalRetries) {
|
|
148
|
+
throw e;
|
|
149
|
+
}
|
|
150
|
+
const delayMs = jittered(BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]);
|
|
151
|
+
const reason = e?.name === 'AbortError' ? 'timeout' : (e?.message || String(e));
|
|
152
|
+
opts?.onRetry?.(attempt + 1, totalRetries, delayMs, reason);
|
|
153
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Unreachable: the loop either returns (on success / non-retriable /
|
|
157
|
+
// final 5xx) or throws (on final network error).
|
|
158
|
+
if (lastResp)
|
|
159
|
+
return lastResp;
|
|
160
|
+
throw lastError ?? new Error(`retryableFetch: exhausted for ${name}`);
|
|
161
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-id.ts — Friendly human-readable session identifiers.
|
|
3
|
+
*
|
|
4
|
+
* Format: `adjective-noun-NNNN` (e.g. `nimble-otter-4271`).
|
|
5
|
+
*
|
|
6
|
+
* Why friendly IDs rather than opaque crypto strings?
|
|
7
|
+
* - Shareable over voice/chat without character-by-character dictation.
|
|
8
|
+
* - Memorable enough to bookmark a sandbox and return to it.
|
|
9
|
+
* - Same UX convention as Vercel preview URLs / StackBlitz sandboxes.
|
|
10
|
+
*
|
|
11
|
+
* DO mapping: `env.NIMBUS_SESSION.idFromName(sessionId)` — deterministic.
|
|
12
|
+
* No backing store required; the session ID IS the key.
|
|
13
|
+
*
|
|
14
|
+
* Entropy budget: ~200 adjectives × ~200 nouns × 10,000 numeric suffixes
|
|
15
|
+
* ≈ 4 × 10⁸ combinations. Birthday-collision probability at 1M live
|
|
16
|
+
* sessions ≈ 0.12%. Acceptable for v1. If we ever push past that scale,
|
|
17
|
+
* widen the suffix to 6 digits (≈ 4 × 10¹⁰).
|
|
18
|
+
*
|
|
19
|
+
* Validation is strict: lowercase ASCII only, exact shape enforced.
|
|
20
|
+
* `idFromName` is case-sensitive, so accepting mixed-case would fragment
|
|
21
|
+
* sessions. Reject anything non-conforming with 400 before touching a DO.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Validate a session ID before touching a DO. Returns true iff the string
|
|
25
|
+
* matches the exact `adjective-noun-NNNN` shape. Length bounds on each part
|
|
26
|
+
* are deliberately looser than our own word lists so future list growth
|
|
27
|
+
* doesn't invalidate bookmarked URLs.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isValidSessionId(id: string | null | undefined): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Generate a fresh session ID. Uses crypto.getRandomValues for uniform
|
|
32
|
+
* sampling — NOT Math.random (which on Workers is seeded at isolate start
|
|
33
|
+
* and can repeat across concurrent requests that share an isolate).
|
|
34
|
+
*
|
|
35
|
+
* Suffix is a 4-digit zero-padded decimal (0000–9999), so parseInt can
|
|
36
|
+
* round-trip it cleanly if anyone ever needs to sort/compare.
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateSessionId(): string;
|
|
39
|
+
//# sourceMappingURL=session-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-id.d.ts","sourceRoot":"","sources":["../../src/_shared/session-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAiFH;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAIvE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAO1C"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-id.ts — Friendly human-readable session identifiers.
|
|
3
|
+
*
|
|
4
|
+
* Format: `adjective-noun-NNNN` (e.g. `nimble-otter-4271`).
|
|
5
|
+
*
|
|
6
|
+
* Why friendly IDs rather than opaque crypto strings?
|
|
7
|
+
* - Shareable over voice/chat without character-by-character dictation.
|
|
8
|
+
* - Memorable enough to bookmark a sandbox and return to it.
|
|
9
|
+
* - Same UX convention as Vercel preview URLs / StackBlitz sandboxes.
|
|
10
|
+
*
|
|
11
|
+
* DO mapping: `env.NIMBUS_SESSION.idFromName(sessionId)` — deterministic.
|
|
12
|
+
* No backing store required; the session ID IS the key.
|
|
13
|
+
*
|
|
14
|
+
* Entropy budget: ~200 adjectives × ~200 nouns × 10,000 numeric suffixes
|
|
15
|
+
* ≈ 4 × 10⁸ combinations. Birthday-collision probability at 1M live
|
|
16
|
+
* sessions ≈ 0.12%. Acceptable for v1. If we ever push past that scale,
|
|
17
|
+
* widen the suffix to 6 digits (≈ 4 × 10¹⁰).
|
|
18
|
+
*
|
|
19
|
+
* Validation is strict: lowercase ASCII only, exact shape enforced.
|
|
20
|
+
* `idFromName` is case-sensitive, so accepting mixed-case would fragment
|
|
21
|
+
* sessions. Reject anything non-conforming with 400 before touching a DO.
|
|
22
|
+
*/
|
|
23
|
+
// Curated single-word adjectives. Lowercase ASCII, no profanity, no numbers,
|
|
24
|
+
// no compounds. Sort order doesn't matter — we index randomly.
|
|
25
|
+
const ADJECTIVES = [
|
|
26
|
+
'agile', 'alert', 'amber', 'ancient', 'arctic', 'ardent', 'autumn', 'azure',
|
|
27
|
+
'balmy', 'bashful', 'bitter', 'blissful', 'bold', 'bouncy', 'brave', 'breezy',
|
|
28
|
+
'bright', 'brisk', 'bronze', 'burly', 'busy', 'calm', 'candid', 'carbon',
|
|
29
|
+
'cheerful', 'chilly', 'classic', 'clever', 'cloudy', 'coastal', 'cobalt', 'cold',
|
|
30
|
+
'cool', 'copper', 'cosmic', 'cosy', 'crimson', 'crisp', 'curious', 'daring',
|
|
31
|
+
'dashing', 'dawn', 'dazzling', 'deep', 'dewy', 'distant', 'divine', 'dotted',
|
|
32
|
+
'dreamy', 'dry', 'dusty', 'eager', 'early', 'earnest', 'eastern', 'easy',
|
|
33
|
+
'elated', 'electric', 'elegant', 'emerald', 'endless', 'epic', 'even', 'fair',
|
|
34
|
+
'fancy', 'faithful', 'faint', 'falling', 'famous', 'fancy', 'fast', 'fearless',
|
|
35
|
+
'feisty', 'fiery', 'fine', 'firm', 'flat', 'fleeting', 'fluffy', 'flying',
|
|
36
|
+
'focused', 'fond', 'foggy', 'forgiving', 'frosty', 'gentle', 'giddy', 'glad',
|
|
37
|
+
'gleaming', 'glowing', 'golden', 'good', 'graceful', 'grand', 'grateful', 'green',
|
|
38
|
+
'gusty', 'handy', 'happy', 'hardy', 'harmonic', 'hasty', 'hazy', 'healthy',
|
|
39
|
+
'helpful', 'heroic', 'honest', 'hopeful', 'humble', 'icy', 'idle', 'indigo',
|
|
40
|
+
'ironclad', 'ivory', 'jade', 'jolly', 'joyful', 'keen', 'kind', 'lively',
|
|
41
|
+
'loyal', 'lucky', 'lunar', 'merry', 'mighty', 'mild', 'misty', 'modern',
|
|
42
|
+
'modest', 'mossy', 'muted', 'mystic', 'nimble', 'noble', 'northern', 'noisy',
|
|
43
|
+
'obsidian', 'orchid', 'patient', 'peachy', 'pearly', 'peppy', 'plucky', 'plum',
|
|
44
|
+
'polar', 'polite', 'proud', 'pure', 'quick', 'quiet', 'quirky', 'radiant',
|
|
45
|
+
'rapid', 'rare', 'ready', 'regal', 'resolute', 'rich', 'ripe', 'rosy',
|
|
46
|
+
'royal', 'ruby', 'rustic', 'sage', 'salty', 'sandy', 'scarlet', 'serene',
|
|
47
|
+
'sharp', 'shiny', 'silent', 'silken', 'silver', 'simple', 'sleepy', 'sleek',
|
|
48
|
+
'slender', 'smart', 'smooth', 'snappy', 'snowy', 'solar', 'solemn', 'solid',
|
|
49
|
+
'sonic', 'spicy', 'spirited', 'spry', 'stable', 'steady', 'stellar', 'steep',
|
|
50
|
+
'stormy', 'stout', 'sturdy', 'subtle', 'summer', 'sunny', 'super', 'supple',
|
|
51
|
+
'swift', 'tame', 'tangerine', 'tart', 'tender', 'thankful', 'tidy', 'tiny',
|
|
52
|
+
'tireless', 'topaz', 'tranquil', 'trusty', 'twilight', 'twinkling', 'upbeat',
|
|
53
|
+
'valiant', 'velvet', 'vibrant', 'vigilant', 'violet', 'vivid', 'wandering',
|
|
54
|
+
'warm', 'wary', 'whimsical', 'wild', 'windy', 'winsome', 'witty', 'wise',
|
|
55
|
+
'woolly', 'youthful', 'zealous', 'zen', 'zesty',
|
|
56
|
+
];
|
|
57
|
+
// Curated single-word concrete nouns (mostly animals + natural features).
|
|
58
|
+
// Avoid anything culturally loaded, branded, or with negative connotations.
|
|
59
|
+
const NOUNS = [
|
|
60
|
+
'acorn', 'alder', 'anchor', 'aspen', 'aster', 'axis', 'badger', 'bamboo',
|
|
61
|
+
'basin', 'basil', 'beacon', 'beagle', 'bear', 'beaver', 'beetle', 'birch',
|
|
62
|
+
'bison', 'blossom', 'bobcat', 'boulder', 'branch', 'breeze', 'briar', 'brook',
|
|
63
|
+
'bubble', 'buffalo', 'butler', 'cactus', 'camel', 'canyon', 'capybara',
|
|
64
|
+
'cardinal', 'caribou', 'cascade', 'castle', 'cavern', 'cedar', 'channel',
|
|
65
|
+
'cheetah', 'cherry', 'chestnut', 'chipmunk', 'cinnamon', 'citrus', 'clam',
|
|
66
|
+
'cliff', 'clover', 'coast', 'cobra', 'comet', 'coral', 'cosmos', 'cougar',
|
|
67
|
+
'coyote', 'crane', 'creek', 'crocus', 'crystal', 'cub', 'cypress', 'dahlia',
|
|
68
|
+
'daisy', 'deer', 'delta', 'diamond', 'dolphin', 'dove', 'dragon', 'dune',
|
|
69
|
+
'eagle', 'ember', 'emu', 'falcon', 'fawn', 'fennel', 'fern', 'ferret',
|
|
70
|
+
'finch', 'firefly', 'fjord', 'flame', 'flint', 'foal', 'forest', 'fossil',
|
|
71
|
+
'fox', 'galaxy', 'garden', 'gazelle', 'gecko', 'geyser', 'gibbon', 'giraffe',
|
|
72
|
+
'glacier', 'granite', 'grove', 'gull', 'harbor', 'harp', 'harvest', 'hawk',
|
|
73
|
+
'hedge', 'heron', 'hickory', 'horizon', 'hornet', 'hummingbird', 'husky',
|
|
74
|
+
'iguana', 'iris', 'island', 'jackal', 'jasmine', 'juniper', 'kestrel',
|
|
75
|
+
'kingfisher', 'kite', 'koala', 'lagoon', 'lantern', 'lark', 'laurel',
|
|
76
|
+
'lavender', 'lemur', 'leopard', 'lichen', 'lighthouse', 'lilac', 'lily',
|
|
77
|
+
'lion', 'llama', 'lobster', 'lotus', 'lynx', 'magnolia', 'mallard', 'mango',
|
|
78
|
+
'maple', 'marble', 'marlin', 'marmot', 'marsh', 'meadow', 'medallion',
|
|
79
|
+
'meridian', 'meteor', 'mink', 'mint', 'mist', 'mockingbird', 'moose', 'moss',
|
|
80
|
+
'mouse', 'mulberry', 'mustang', 'narwhal', 'nebula', 'nectar', 'newt',
|
|
81
|
+
'nightingale', 'oak', 'oasis', 'ocean', 'ocelot', 'opal', 'orchid', 'oriole',
|
|
82
|
+
'otter', 'owl', 'oyster', 'palm', 'panda', 'pangolin', 'panther', 'parrot',
|
|
83
|
+
'peak', 'pebble', 'peony', 'petal', 'phoenix', 'pigeon', 'pine', 'piper',
|
|
84
|
+
'plateau', 'poppy', 'prairie', 'puffin', 'puma', 'quail', 'quartz', 'quokka',
|
|
85
|
+
'rabbit', 'raccoon', 'rapids', 'raven', 'reef', 'reindeer', 'ribbon', 'ridge',
|
|
86
|
+
'river', 'robin', 'rose', 'sable', 'sage', 'salmon', 'sapphire', 'savanna',
|
|
87
|
+
'seal', 'sequoia', 'shark', 'shrub', 'silo', 'skink', 'sky', 'snail',
|
|
88
|
+
'snowflake', 'sparrow', 'spider', 'spire', 'spruce', 'squirrel', 'starling',
|
|
89
|
+
'stingray', 'stone', 'stork', 'stream', 'summit', 'sunflower', 'swan',
|
|
90
|
+
'swift', 'talon', 'tamarind', 'tapir', 'teal', 'tern', 'thicket', 'thistle',
|
|
91
|
+
'thrush', 'tiger', 'toad', 'topaz', 'tortoise', 'toucan', 'tree', 'trout',
|
|
92
|
+
'tulip', 'tundra', 'turtle', 'valley', 'vine', 'violet', 'viper', 'vista',
|
|
93
|
+
'vole', 'walnut', 'warbler', 'waterfall', 'wave', 'weasel', 'whale', 'wharf',
|
|
94
|
+
'wheat', 'whippet', 'willow', 'wisp', 'wolf', 'wombat', 'woodland',
|
|
95
|
+
'woodpecker', 'yak', 'yew', 'zebra', 'zinnia',
|
|
96
|
+
];
|
|
97
|
+
/** Strict shape check. Rejects empty, uppercase, missing-parts, extra-parts. */
|
|
98
|
+
const SESSION_ID_RE = /^[a-z]{3,14}-[a-z]{3,14}-\d{4}$/;
|
|
99
|
+
/**
|
|
100
|
+
* Validate a session ID before touching a DO. Returns true iff the string
|
|
101
|
+
* matches the exact `adjective-noun-NNNN` shape. Length bounds on each part
|
|
102
|
+
* are deliberately looser than our own word lists so future list growth
|
|
103
|
+
* doesn't invalidate bookmarked URLs.
|
|
104
|
+
*/
|
|
105
|
+
export function isValidSessionId(id) {
|
|
106
|
+
if (typeof id !== 'string')
|
|
107
|
+
return false;
|
|
108
|
+
if (id.length < 9 || id.length > 40)
|
|
109
|
+
return false;
|
|
110
|
+
return SESSION_ID_RE.test(id);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Generate a fresh session ID. Uses crypto.getRandomValues for uniform
|
|
114
|
+
* sampling — NOT Math.random (which on Workers is seeded at isolate start
|
|
115
|
+
* and can repeat across concurrent requests that share an isolate).
|
|
116
|
+
*
|
|
117
|
+
* Suffix is a 4-digit zero-padded decimal (0000–9999), so parseInt can
|
|
118
|
+
* round-trip it cleanly if anyone ever needs to sort/compare.
|
|
119
|
+
*/
|
|
120
|
+
export function generateSessionId() {
|
|
121
|
+
const buf = new Uint32Array(3);
|
|
122
|
+
crypto.getRandomValues(buf);
|
|
123
|
+
const adj = ADJECTIVES[buf[0] % ADJECTIVES.length];
|
|
124
|
+
const noun = NOUNS[buf[1] % NOUNS.length];
|
|
125
|
+
const num = (buf[2] % 10000).toString().padStart(4, '0');
|
|
126
|
+
return `${adj}-${noun}-${num}`;
|
|
127
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-router.ts — Rewrite `/s/<id>/<rest>` → DO-internal `/ <rest>`.
|
|
3
|
+
*
|
|
4
|
+
* Nimbus sessions live behind friendly URLs like `/s/nimble-otter-4271/`.
|
|
5
|
+
* The Worker's fetch handler delegates to this module for anything that
|
|
6
|
+
* starts with `/s/`. The DO itself remains blissfully ignorant of its
|
|
7
|
+
* public URL: it sees `/ws`, `/api/*`, `/preview/*`, `/__nimbus/worker/*`
|
|
8
|
+
* (and the deprecated `/worker/*`), `/port/<n>/*` — the same shape it
|
|
9
|
+
* did in the single-session era.
|
|
10
|
+
*
|
|
11
|
+
* Session identity flows three places:
|
|
12
|
+
* 1. The DO ID — derived via `env.NIMBUS_SESSION.idFromName(doName)`.
|
|
13
|
+
* `doName` is `${tenant}:${sub || '_'}:${sessionId}` when tenant
|
|
14
|
+
* scoping is on (JWT verified upstream); falls back to
|
|
15
|
+
* `legacy:public:_:${sessionId}` when `NIMBUS_LEGACY_PUBLIC === "1"`
|
|
16
|
+
* or `JWT_SECRET` is unset. The fallback preserves the live-demo
|
|
17
|
+
* single-tenant behavior while letting third-party embedders enforce
|
|
18
|
+
* tenant isolation by default. Caller (the worker fetch handler)
|
|
19
|
+
* passes `tenantSegment` in via {@link forwardToSession}.
|
|
20
|
+
* 2. The `X-Nimbus-Base` request header — set to the URL-prefix the DO
|
|
21
|
+
* is mounted at (e.g. `/s/nimble-otter-4271`). ViteDevServer uses
|
|
22
|
+
* this to emit correct `<base href>`, module URLs, `import.meta.env
|
|
23
|
+
* .BASE_URL`, and router `basename` so the user's React app resolves
|
|
24
|
+
* `<NavLink to="/docs">` → `/s/nimble-otter-4271/preview/docs`.
|
|
25
|
+
* 3. The `X-Nimbus-Tenant` request header — set to the verified
|
|
26
|
+
* `${tenant}:${sub}` so downstream RPC handlers can audit cross-
|
|
27
|
+
* session multi-tenancy invariants. Optional; never trusted as
|
|
28
|
+
* authority (the DO name itself is the trust boundary).
|
|
29
|
+
*
|
|
30
|
+
* Why a header instead of the DO auto-detecting?
|
|
31
|
+
* - The DO never sees the outer URL (we forward a rewritten Request).
|
|
32
|
+
* - Explicit plumbing is easier to test; auto-detect from Referer is
|
|
33
|
+
* unreliable (no Referer for API fetches from the xterm shell).
|
|
34
|
+
* - Single source of truth: this module owns the `/s/<id>/` mapping.
|
|
35
|
+
*/
|
|
36
|
+
/** Prefix for all session-scoped routes. Centralized for future refactors. */
|
|
37
|
+
export declare const SESSION_ROUTE_PREFIX = "/s";
|
|
38
|
+
/** Header the Worker sets on forwarded requests. The DO reads it. */
|
|
39
|
+
export declare const BASE_PATH_HEADER = "X-Nimbus-Base";
|
|
40
|
+
/**
|
|
41
|
+
* Header the Worker sets to inform the DO of its tenant scope. The DO
|
|
42
|
+
* reads it for audit-logging / RPC-routing; it does NOT use it for
|
|
43
|
+
* authority — the DO instance name is the trust boundary.
|
|
44
|
+
*/
|
|
45
|
+
export declare const TENANT_HEADER = "X-Nimbus-Tenant";
|
|
46
|
+
/**
|
|
47
|
+
* DO-name segment used when tenant scoping is disabled (legacy-public).
|
|
48
|
+
* Picked so it cannot collide with a verified token's
|
|
49
|
+
* `${tn}:${sub || '_'}` (because `legacy` is never a valid `tn` shape
|
|
50
|
+
* starting with that exact prefix-then-colon when JWT_SECRET is set).
|
|
51
|
+
*/
|
|
52
|
+
export declare const LEGACY_PUBLIC_DO_SEGMENT = "legacy:public:_";
|
|
53
|
+
export interface ParsedSessionRoute {
|
|
54
|
+
/** Session ID portion (unverified until `isValidSessionId` check). */
|
|
55
|
+
sessionId: string;
|
|
56
|
+
/** Inner path the DO should see, starting with "/" (e.g. "/ws", "/api/stats"). */
|
|
57
|
+
innerPath: string;
|
|
58
|
+
/** Public URL prefix the DO is mounted at (e.g. "/s/nimble-otter-4271"). */
|
|
59
|
+
basePath: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Attempt to parse a URL pathname as a session route.
|
|
63
|
+
* Returns null if the pathname doesn't start with `/s/<something>`.
|
|
64
|
+
*
|
|
65
|
+
* Does NOT validate session ID shape — callers do that next so they can
|
|
66
|
+
* emit a specific 400 (bad ID) vs continuing with a 404 fall-through.
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseSessionRoute(pathname: string): ParsedSessionRoute | null;
|
|
69
|
+
/**
|
|
70
|
+
* Options for {@link forwardToSession}.
|
|
71
|
+
*
|
|
72
|
+
* `tenantSegment` is the verified `${tn}:${sub || '_'}` from JWT, or
|
|
73
|
+
* {@link LEGACY_PUBLIC_DO_SEGMENT} when running in legacy-public mode.
|
|
74
|
+
* The worker fetch handler computes this BEFORE calling this function
|
|
75
|
+
* — the router itself does not parse tokens.
|
|
76
|
+
*/
|
|
77
|
+
export interface ForwardOptions {
|
|
78
|
+
/** Verified tenant segment for DO naming. */
|
|
79
|
+
tenantSegment: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Forward a request to the session's DO.
|
|
83
|
+
*
|
|
84
|
+
* Contract:
|
|
85
|
+
* - Caller has already validated the session ID (or tolerates whatever
|
|
86
|
+
* DO spawns if they didn't — still safe, but malformed IDs should be
|
|
87
|
+
* rejected upstream with 400).
|
|
88
|
+
* - Caller has already verified the token (or chosen legacy-public).
|
|
89
|
+
* We trust `opts.tenantSegment` verbatim.
|
|
90
|
+
* - Original request's method, body, and headers are preserved.
|
|
91
|
+
* - `X-Nimbus-Base` is injected so the DO can thread it into ViteDevServer.
|
|
92
|
+
* - `X-Nimbus-Tenant` is injected for audit/observability (NOT authority).
|
|
93
|
+
* - WebSocket upgrades flow naturally: `stub.fetch()` returns a Response
|
|
94
|
+
* with a `webSocket` field and status 101, which workerd passes through.
|
|
95
|
+
*
|
|
96
|
+
* @param request The inbound Request. Method/body/headers preserved.
|
|
97
|
+
* @param route Output of {@link parseSessionRoute}.
|
|
98
|
+
* @param env Bindings env. Must carry `NIMBUS_SESSION` DO namespace.
|
|
99
|
+
* @param opts Tenant scoping. See {@link ForwardOptions}.
|
|
100
|
+
*/
|
|
101
|
+
export declare function forwardToSession(request: Request, route: ParsedSessionRoute, env: any, opts: ForwardOptions): Promise<Response>;
|
|
102
|
+
/** HTML body for the "invalid session ID" 400 page. Tiny, inline-only. */
|
|
103
|
+
export declare function renderInvalidSessionHtml(attemptedId: string): string;
|
|
104
|
+
//# sourceMappingURL=session-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-router.d.ts","sourceRoot":"","sources":["../../src/_shared/session-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,qEAAqE;AACrE,eAAO,MAAM,gBAAgB,kBAAkB,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,oBAAoB,CAAC;AAS1D,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAa7E;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,kBAAkB,EACzB,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,QAAQ,CAAC,CAoCnB;AAED,0EAA0E;AAC1E,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CA6BpE"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-router.ts — Rewrite `/s/<id>/<rest>` → DO-internal `/ <rest>`.
|
|
3
|
+
*
|
|
4
|
+
* Nimbus sessions live behind friendly URLs like `/s/nimble-otter-4271/`.
|
|
5
|
+
* The Worker's fetch handler delegates to this module for anything that
|
|
6
|
+
* starts with `/s/`. The DO itself remains blissfully ignorant of its
|
|
7
|
+
* public URL: it sees `/ws`, `/api/*`, `/preview/*`, `/__nimbus/worker/*`
|
|
8
|
+
* (and the deprecated `/worker/*`), `/port/<n>/*` — the same shape it
|
|
9
|
+
* did in the single-session era.
|
|
10
|
+
*
|
|
11
|
+
* Session identity flows three places:
|
|
12
|
+
* 1. The DO ID — derived via `env.NIMBUS_SESSION.idFromName(doName)`.
|
|
13
|
+
* `doName` is `${tenant}:${sub || '_'}:${sessionId}` when tenant
|
|
14
|
+
* scoping is on (JWT verified upstream); falls back to
|
|
15
|
+
* `legacy:public:_:${sessionId}` when `NIMBUS_LEGACY_PUBLIC === "1"`
|
|
16
|
+
* or `JWT_SECRET` is unset. The fallback preserves the live-demo
|
|
17
|
+
* single-tenant behavior while letting third-party embedders enforce
|
|
18
|
+
* tenant isolation by default. Caller (the worker fetch handler)
|
|
19
|
+
* passes `tenantSegment` in via {@link forwardToSession}.
|
|
20
|
+
* 2. The `X-Nimbus-Base` request header — set to the URL-prefix the DO
|
|
21
|
+
* is mounted at (e.g. `/s/nimble-otter-4271`). ViteDevServer uses
|
|
22
|
+
* this to emit correct `<base href>`, module URLs, `import.meta.env
|
|
23
|
+
* .BASE_URL`, and router `basename` so the user's React app resolves
|
|
24
|
+
* `<NavLink to="/docs">` → `/s/nimble-otter-4271/preview/docs`.
|
|
25
|
+
* 3. The `X-Nimbus-Tenant` request header — set to the verified
|
|
26
|
+
* `${tenant}:${sub}` so downstream RPC handlers can audit cross-
|
|
27
|
+
* session multi-tenancy invariants. Optional; never trusted as
|
|
28
|
+
* authority (the DO name itself is the trust boundary).
|
|
29
|
+
*
|
|
30
|
+
* Why a header instead of the DO auto-detecting?
|
|
31
|
+
* - The DO never sees the outer URL (we forward a rewritten Request).
|
|
32
|
+
* - Explicit plumbing is easier to test; auto-detect from Referer is
|
|
33
|
+
* unreliable (no Referer for API fetches from the xterm shell).
|
|
34
|
+
* - Single source of truth: this module owns the `/s/<id>/` mapping.
|
|
35
|
+
*/
|
|
36
|
+
/** Prefix for all session-scoped routes. Centralized for future refactors. */
|
|
37
|
+
export const SESSION_ROUTE_PREFIX = '/s';
|
|
38
|
+
/** Header the Worker sets on forwarded requests. The DO reads it. */
|
|
39
|
+
export const BASE_PATH_HEADER = 'X-Nimbus-Base';
|
|
40
|
+
/**
|
|
41
|
+
* Header the Worker sets to inform the DO of its tenant scope. The DO
|
|
42
|
+
* reads it for audit-logging / RPC-routing; it does NOT use it for
|
|
43
|
+
* authority — the DO instance name is the trust boundary.
|
|
44
|
+
*/
|
|
45
|
+
export const TENANT_HEADER = 'X-Nimbus-Tenant';
|
|
46
|
+
/**
|
|
47
|
+
* DO-name segment used when tenant scoping is disabled (legacy-public).
|
|
48
|
+
* Picked so it cannot collide with a verified token's
|
|
49
|
+
* `${tn}:${sub || '_'}` (because `legacy` is never a valid `tn` shape
|
|
50
|
+
* starting with that exact prefix-then-colon when JWT_SECRET is set).
|
|
51
|
+
*/
|
|
52
|
+
export const LEGACY_PUBLIC_DO_SEGMENT = 'legacy:public:_';
|
|
53
|
+
/**
|
|
54
|
+
* Match `/s/<id>(/<rest>)?` with `<id>` being ANY lowercase-letters-digits-
|
|
55
|
+
* dashes token. Shape validation happens in a second step so we can return
|
|
56
|
+
* a specific 400 for malformed IDs (vs falling through to the Worker's 404).
|
|
57
|
+
*/
|
|
58
|
+
const SESSION_PATH_RE = /^\/s\/([^\/]+)(\/.*)?$/;
|
|
59
|
+
/**
|
|
60
|
+
* Attempt to parse a URL pathname as a session route.
|
|
61
|
+
* Returns null if the pathname doesn't start with `/s/<something>`.
|
|
62
|
+
*
|
|
63
|
+
* Does NOT validate session ID shape — callers do that next so they can
|
|
64
|
+
* emit a specific 400 (bad ID) vs continuing with a 404 fall-through.
|
|
65
|
+
*/
|
|
66
|
+
export function parseSessionRoute(pathname) {
|
|
67
|
+
const m = pathname.match(SESSION_PATH_RE);
|
|
68
|
+
if (!m)
|
|
69
|
+
return null;
|
|
70
|
+
const sessionId = m[1];
|
|
71
|
+
// Ensure inner path starts with "/" and defaults to "/" for `/s/<id>`
|
|
72
|
+
// (no trailing slash). This matches how the DO's existing handlers
|
|
73
|
+
// distinguish `/preview` from `/preview/` — both reach the same code.
|
|
74
|
+
const innerPath = m[2] && m[2].length > 0 ? m[2] : '/';
|
|
75
|
+
return {
|
|
76
|
+
sessionId,
|
|
77
|
+
innerPath,
|
|
78
|
+
basePath: `${SESSION_ROUTE_PREFIX}/${sessionId}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Forward a request to the session's DO.
|
|
83
|
+
*
|
|
84
|
+
* Contract:
|
|
85
|
+
* - Caller has already validated the session ID (or tolerates whatever
|
|
86
|
+
* DO spawns if they didn't — still safe, but malformed IDs should be
|
|
87
|
+
* rejected upstream with 400).
|
|
88
|
+
* - Caller has already verified the token (or chosen legacy-public).
|
|
89
|
+
* We trust `opts.tenantSegment` verbatim.
|
|
90
|
+
* - Original request's method, body, and headers are preserved.
|
|
91
|
+
* - `X-Nimbus-Base` is injected so the DO can thread it into ViteDevServer.
|
|
92
|
+
* - `X-Nimbus-Tenant` is injected for audit/observability (NOT authority).
|
|
93
|
+
* - WebSocket upgrades flow naturally: `stub.fetch()` returns a Response
|
|
94
|
+
* with a `webSocket` field and status 101, which workerd passes through.
|
|
95
|
+
*
|
|
96
|
+
* @param request The inbound Request. Method/body/headers preserved.
|
|
97
|
+
* @param route Output of {@link parseSessionRoute}.
|
|
98
|
+
* @param env Bindings env. Must carry `NIMBUS_SESSION` DO namespace.
|
|
99
|
+
* @param opts Tenant scoping. See {@link ForwardOptions}.
|
|
100
|
+
*/
|
|
101
|
+
export function forwardToSession(request, route, env, opts) {
|
|
102
|
+
const url = new URL(request.url);
|
|
103
|
+
// Rebuild the URL the DO will see: same origin, inner path, preserved query.
|
|
104
|
+
const innerUrl = new URL(route.innerPath + url.search + url.hash, url.origin);
|
|
105
|
+
// Clone headers and set the X-Nimbus-* metadata. We can't mutate the
|
|
106
|
+
// original Request's headers directly (they're immutable on a real Request).
|
|
107
|
+
const headers = new Headers(request.headers);
|
|
108
|
+
headers.set(BASE_PATH_HEADER, route.basePath);
|
|
109
|
+
headers.set(TENANT_HEADER, opts.tenantSegment);
|
|
110
|
+
// Defense in depth: strip any user-supplied `nimbus_token` query param
|
|
111
|
+
// BEFORE forwarding so it doesn't end up in inner-DO logs / referrers.
|
|
112
|
+
// The DO has its own header-based auth model post-router (none today,
|
|
113
|
+
// but this future-proofs the surface).
|
|
114
|
+
innerUrl.searchParams.delete('nimbus_token');
|
|
115
|
+
// Build the forwarded Request. Preserve body + method. For GETs/HEADs,
|
|
116
|
+
// body is undefined (Request constructor rejects bodies there anyway).
|
|
117
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
118
|
+
const inner = new Request(innerUrl.toString(), {
|
|
119
|
+
method: request.method,
|
|
120
|
+
headers,
|
|
121
|
+
body: hasBody ? request.body : undefined,
|
|
122
|
+
// Preserve WS upgrade. Workerd needs the Upgrade header to stay put;
|
|
123
|
+
// copying via new Headers(...) does preserve it.
|
|
124
|
+
redirect: 'manual',
|
|
125
|
+
});
|
|
126
|
+
// Tenant-scoped DO instance name. `tenantSegment` is `${tn}:${sub||'_'}`
|
|
127
|
+
// (verified token) or `legacy:public:_` (legacy mode). Either way, the
|
|
128
|
+
// sessionId is appended — different tenants get different DOs even with
|
|
129
|
+
// the same sessionId in the URL.
|
|
130
|
+
const doName = `${opts.tenantSegment}:${route.sessionId}`;
|
|
131
|
+
const id = env.NIMBUS_SESSION.idFromName(doName);
|
|
132
|
+
const stub = env.NIMBUS_SESSION.get(id);
|
|
133
|
+
return stub.fetch(inner);
|
|
134
|
+
}
|
|
135
|
+
/** HTML body for the "invalid session ID" 400 page. Tiny, inline-only. */
|
|
136
|
+
export function renderInvalidSessionHtml(attemptedId) {
|
|
137
|
+
// Escape the attempted ID for display. We don't use innerHTML anywhere,
|
|
138
|
+
// but defensive escaping keeps the HTML validator happy and avoids any
|
|
139
|
+
// future XSS footguns if someone refactors this to document.write().
|
|
140
|
+
const safe = String(attemptedId).replace(/[&<>"']/g, (c) => ({
|
|
141
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
142
|
+
}[c]));
|
|
143
|
+
return `<!DOCTYPE html>
|
|
144
|
+
<html lang="en"><head>
|
|
145
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
146
|
+
<title>Invalid session — Nimbus</title>
|
|
147
|
+
<style>
|
|
148
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
149
|
+
html,body{height:100%}
|
|
150
|
+
body{background:#0a0a0a;color:#e6edf3;font:15px/1.6 ui-sans-serif,-apple-system,"Segoe UI",sans-serif;
|
|
151
|
+
display:flex;align-items:center;justify-content:center;padding:24px;
|
|
152
|
+
background-image:radial-gradient(700px 400px at 50% -10%,rgba(100,255,218,0.05),transparent 60%)}
|
|
153
|
+
.card{max-width:520px;text-align:center}
|
|
154
|
+
h1{font-size:28px;color:#64ffda;margin-bottom:12px;font-family:ui-monospace,Menlo,monospace}
|
|
155
|
+
p{color:#8b949e;margin-bottom:24px}
|
|
156
|
+
code{font-family:ui-monospace,Menlo,monospace;background:#111;padding:2px 8px;border-radius:4px;color:#e6edf3}
|
|
157
|
+
a.btn{display:inline-block;background:#64ffda;color:#000;font-weight:700;padding:10px 22px;border-radius:6px;text-decoration:none}
|
|
158
|
+
a.btn:hover{filter:brightness(1.1)}
|
|
159
|
+
</style></head>
|
|
160
|
+
<body><div class="card">
|
|
161
|
+
<h1>Invalid session</h1>
|
|
162
|
+
<p>The ID <code>${safe}</code> isn’t a valid Nimbus session URL.<br>Launch a new one to get started.</p>
|
|
163
|
+
<a class="btn" href="/">← Back to Nimbus</a>
|
|
164
|
+
</div></body></html>`;
|
|
165
|
+
}
|