@mysten-incubation/devstack 0.1.0 → 0.2.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 +7 -4
- package/dashboard-ui/assets/grpc-a4usE3Nk.js +3 -0
- package/dashboard-ui/assets/index-B82Bh84P.css +1 -0
- package/dashboard-ui/assets/index-CRYJ4pru.js +1277 -0
- package/dashboard-ui/index.html +2 -2
- package/dist/api/define-capabilities.d.mts +1 -20
- package/dist/api/define-capabilities.mjs +15 -22
- package/dist/api/define-capabilities.mjs.map +1 -1
- package/dist/api/define-devstack-with.d.mts +7 -1
- package/dist/api/define-devstack-with.mjs.map +1 -1
- package/dist/api/define-devstack.mjs.map +1 -1
- package/dist/api/define-plugin.d.mts +2 -1
- package/dist/api/inference-network.d.mts +31 -17
- package/dist/api/inference-network.mjs +40 -43
- package/dist/api/inference-network.mjs.map +1 -1
- package/dist/api/load-config.mjs +88 -0
- package/dist/api/load-config.mjs.map +1 -0
- package/dist/api/mode-narrowed-factory.d.mts +1 -1
- package/dist/api/mode-narrowed-factory.mjs.map +1 -1
- package/dist/api/run-stack-internal.mjs +202 -0
- package/dist/api/run-stack-internal.mjs.map +1 -0
- package/dist/api/run-stack.d.mts +82 -31
- package/dist/api/run-stack.mjs +22 -141
- package/dist/api/run-stack.mjs.map +1 -1
- package/dist/build-integrations/dapp-kit/index.d.mts +61 -0
- package/dist/build-integrations/dapp-kit/index.mjs +58 -0
- package/dist/build-integrations/dapp-kit/index.mjs.map +1 -0
- package/dist/build-integrations/playwright/config.d.mts +1 -30
- package/dist/build-integrations/playwright/config.mjs +1 -27
- package/dist/build-integrations/playwright/config.mjs.map +1 -1
- package/dist/build-integrations/playwright/errors.d.mts +4 -7
- package/dist/build-integrations/playwright/errors.mjs.map +1 -1
- package/dist/build-integrations/playwright/global-setup.d.mts +42 -71
- package/dist/build-integrations/playwright/global-setup.mjs +142 -218
- package/dist/build-integrations/playwright/global-setup.mjs.map +1 -1
- package/dist/build-integrations/playwright/index.d.mts +2 -2
- package/dist/build-integrations/playwright/index.mjs +2 -2
- package/dist/build-integrations/playwright/stack-context.d.mts +3 -1
- package/dist/build-integrations/playwright/stack-context.mjs +4 -2
- package/dist/build-integrations/playwright/stack-context.mjs.map +1 -1
- package/dist/build-integrations/playwright/wallet-context.mjs +44 -19
- package/dist/build-integrations/playwright/wallet-context.mjs.map +1 -1
- package/dist/build-integrations/runtime/cold-start-url.mjs.map +1 -1
- package/dist/build-integrations/runtime/conventional-routes.mjs.map +1 -1
- package/dist/build-integrations/runtime/dapp-kit-slot.mjs.map +1 -1
- package/dist/build-integrations/runtime/discover.d.mts +6 -4
- package/dist/build-integrations/runtime/discover.mjs +15 -4
- package/dist/build-integrations/runtime/discover.mjs.map +1 -1
- package/dist/build-integrations/runtime/endpoint-registry.mjs.map +1 -1
- package/dist/build-integrations/runtime/errors.mjs.map +1 -1
- package/dist/build-integrations/runtime/read-stack-context.mjs +3 -3
- package/dist/build-integrations/runtime/read-stack-context.mjs.map +1 -1
- package/dist/build-integrations/runtime/resolve-discovery-env.d.mts +23 -7
- package/dist/build-integrations/runtime/resolve-discovery-env.mjs +25 -12
- package/dist/build-integrations/runtime/resolve-discovery-env.mjs.map +1 -1
- package/dist/build-integrations/runtime/stack-context.d.mts +2 -2
- package/dist/build-integrations/vite/index.d.mts +84 -19
- package/dist/build-integrations/vite/index.mjs +203 -28
- package/dist/build-integrations/vite/index.mjs.map +1 -1
- package/dist/build-integrations/vitest/config.d.mts +20 -2
- package/dist/build-integrations/vitest/config.mjs +16 -4
- package/dist/build-integrations/vitest/config.mjs.map +1 -1
- package/dist/build-integrations/vitest/env.d.mts +18 -10
- package/dist/build-integrations/vitest/env.mjs +11 -10
- package/dist/build-integrations/vitest/env.mjs.map +1 -1
- package/dist/build-integrations/vitest/errors.d.mts +7 -11
- package/dist/build-integrations/vitest/errors.mjs +3 -4
- package/dist/build-integrations/vitest/errors.mjs.map +1 -1
- package/dist/build-integrations/vitest/global-setup.d.mts +35 -0
- package/dist/build-integrations/vitest/global-setup.mjs +156 -0
- package/dist/build-integrations/vitest/global-setup.mjs.map +1 -0
- package/dist/build-integrations/vitest/index.d.mts +3 -2
- package/dist/build-integrations/vitest/index.mjs +3 -2
- package/dist/build-integrations/vitest/setup.mjs +2 -2
- package/dist/build-integrations/vitest/setup.mjs.map +1 -1
- package/dist/build-integrations/vitest/stack-context.d.mts +5 -3
- package/dist/build-integrations/vitest/stack-context.mjs +3 -2
- package/dist/build-integrations/vitest/stack-context.mjs.map +1 -1
- package/dist/cli/bail.mjs.map +1 -1
- package/dist/cli/doctor-probes.mjs +1 -3
- package/dist/cli/doctor-probes.mjs.map +1 -1
- package/dist/cli/main.d.mts +23 -1
- package/dist/cli/main.mjs +63 -8
- package/dist/cli/main.mjs.map +1 -1
- package/dist/cli/prune-direct.mjs.map +1 -1
- package/dist/cli/snapshot-reader.mjs.map +1 -1
- package/dist/cli/up-lifecycle.mjs.map +1 -1
- package/dist/cli/wirings/apply.mjs +28 -21
- package/dist/cli/wirings/apply.mjs.map +1 -1
- package/dist/cli/wirings/codegen.mjs +108 -0
- package/dist/cli/wirings/codegen.mjs.map +1 -0
- package/dist/cli/wirings/config-loader.mjs +12 -62
- package/dist/cli/wirings/config-loader.mjs.map +1 -1
- package/dist/cli/wirings/dump-ids.mjs +86 -0
- package/dist/cli/wirings/dump-ids.mjs.map +1 -0
- package/dist/cli/wirings/engine-command.mjs +1 -1
- package/dist/cli/wirings/engine-command.mjs.map +1 -1
- package/dist/cli/wirings/identity.mjs +2 -2
- package/dist/cli/wirings/identity.mjs.map +1 -1
- package/dist/cli/wirings/provide-file-system.mjs.map +1 -1
- package/dist/cli/wirings/read-devstack-version.mjs +17 -0
- package/dist/cli/wirings/read-devstack-version.mjs.map +1 -0
- package/dist/cli/wirings/snapshot.mjs +27 -34
- package/dist/cli/wirings/snapshot.mjs.map +1 -1
- package/dist/cli/wirings/up-ipc.mjs +147 -0
- package/dist/cli/wirings/up-ipc.mjs.map +1 -0
- package/dist/cli/wirings/up.mjs +218 -198
- package/dist/cli/wirings/up.mjs.map +1 -1
- package/dist/cli/wirings/wipe.mjs +1 -1
- package/dist/cli/wirings/wipe.mjs.map +1 -1
- package/dist/contracts/chain-probe.d.mts +1 -1
- package/dist/contracts/chain-probe.mjs +1 -1
- package/dist/contracts/chain-probe.mjs.map +1 -1
- package/dist/contracts/codegenable.d.mts +60 -6
- package/dist/contracts/codegenable.mjs +28 -0
- package/dist/contracts/codegenable.mjs.map +1 -0
- package/dist/contracts/config-bindings.mjs +162 -0
- package/dist/contracts/config-bindings.mjs.map +1 -0
- package/dist/contracts/container-runtime.d.mts +23 -17
- package/dist/contracts/plugin-expander.mjs.map +1 -1
- package/dist/contracts/snapshotable.d.mts +5 -7
- package/dist/contracts/wallet-protocol.d.mts +2 -5
- package/dist/contracts/wallet-protocol.mjs +2 -5
- package/dist/contracts/wallet-protocol.mjs.map +1 -1
- package/dist/index.d.mts +21 -26
- package/dist/index.mjs +8 -9
- package/dist/orchestrators/boot.d.mts +28 -0
- package/dist/orchestrators/boot.mjs +440 -0
- package/dist/orchestrators/boot.mjs.map +1 -0
- package/dist/orchestrators/codegen/bindings.d.mts +13 -1
- package/dist/orchestrators/codegen/bindings.mjs +23 -9
- package/dist/orchestrators/codegen/bindings.mjs.map +1 -1
- package/dist/orchestrators/codegen/config-runtime.mjs +208 -0
- package/dist/orchestrators/codegen/config-runtime.mjs.map +1 -0
- package/dist/orchestrators/codegen/emit.mjs +1 -1
- package/dist/orchestrators/codegen/emit.mjs.map +1 -1
- package/dist/orchestrators/codegen/errors.mjs +21 -1
- package/dist/orchestrators/codegen/errors.mjs.map +1 -1
- package/dist/orchestrators/codegen/format.mjs +2 -0
- package/dist/orchestrators/codegen/format.mjs.map +1 -1
- package/dist/orchestrators/codegen/gitignore.mjs +30 -11
- package/dist/orchestrators/codegen/gitignore.mjs.map +1 -1
- package/dist/orchestrators/codegen/id-config.mjs +107 -0
- package/dist/orchestrators/codegen/id-config.mjs.map +1 -0
- package/dist/orchestrators/codegen/output-location.mjs +23 -31
- package/dist/orchestrators/codegen/output-location.mjs.map +1 -1
- package/dist/orchestrators/codegen/paths.mjs +8 -1
- package/dist/orchestrators/codegen/paths.mjs.map +1 -1
- package/dist/orchestrators/codegen/permissions.mjs.map +1 -1
- package/dist/orchestrators/codegen/service.mjs +286 -58
- package/dist/orchestrators/codegen/service.mjs.map +1 -1
- package/dist/orchestrators/layers.mjs +31 -0
- package/dist/orchestrators/layers.mjs.map +1 -0
- package/dist/orchestrators/lifecycle-prune/errors.mjs.map +1 -1
- package/dist/orchestrators/lifecycle-prune/index.mjs +8 -10
- package/dist/orchestrators/lifecycle-prune/index.mjs.map +1 -1
- package/dist/orchestrators/network-options.d.mts +52 -0
- package/dist/orchestrators/network-options.mjs +43 -0
- package/dist/orchestrators/network-options.mjs.map +1 -0
- package/dist/orchestrators/router/cleanup.mjs.map +1 -1
- package/dist/orchestrators/router/cors.mjs.map +1 -1
- package/dist/orchestrators/router/entrypoints.mjs.map +1 -1
- package/dist/orchestrators/router/errors.mjs.map +1 -1
- package/dist/orchestrators/router/file-provider.mjs.map +1 -1
- package/dist/orchestrators/router/hostname.mjs.map +1 -1
- package/dist/orchestrators/router/profile.mjs +1 -1
- package/dist/orchestrators/router/profile.mjs.map +1 -1
- package/dist/orchestrators/router/service.d.mts +2 -2
- package/dist/orchestrators/router/service.mjs +12 -27
- package/dist/orchestrators/router/service.mjs.map +1 -1
- package/dist/orchestrators/router/traefik-container.mjs +1 -1
- package/dist/orchestrators/router/traefik-container.mjs.map +1 -1
- package/dist/orchestrators/snapshot/capture.mjs +171 -195
- package/dist/orchestrators/snapshot/capture.mjs.map +1 -1
- package/dist/orchestrators/snapshot/descriptor.d.mts +1 -1
- package/dist/orchestrators/snapshot/descriptor.mjs +53 -11
- package/dist/orchestrators/snapshot/descriptor.mjs.map +1 -1
- package/dist/orchestrators/snapshot/identity-guard.mjs +3 -5
- package/dist/orchestrators/snapshot/identity-guard.mjs.map +1 -1
- package/dist/orchestrators/snapshot/image-bundle-tags.mjs +32 -101
- package/dist/orchestrators/snapshot/image-bundle-tags.mjs.map +1 -1
- package/dist/orchestrators/snapshot/index.mjs +1 -4
- package/dist/orchestrators/snapshot/integrity.mjs.map +1 -1
- package/dist/orchestrators/snapshot/interrupted-restore.mjs +116 -0
- package/dist/orchestrators/snapshot/interrupted-restore.mjs.map +1 -0
- package/dist/orchestrators/snapshot/phase-error.mjs.map +1 -1
- package/dist/orchestrators/snapshot/prune.mjs +40 -31
- package/dist/orchestrators/snapshot/prune.mjs.map +1 -1
- package/dist/orchestrators/snapshot/restore.mjs +221 -196
- package/dist/orchestrators/snapshot/restore.mjs.map +1 -1
- package/dist/orchestrators/snapshot/service.mjs +72 -62
- package/dist/orchestrators/snapshot/service.mjs.map +1 -1
- package/dist/orchestrators/snapshot/wipe.mjs +67 -56
- package/dist/orchestrators/snapshot/wipe.mjs.map +1 -1
- package/dist/plugins/account/codegen.mjs +3 -0
- package/dist/plugins/account/codegen.mjs.map +1 -1
- package/dist/plugins/account/errors.d.mts +2 -2
- package/dist/plugins/account/errors.mjs +1 -4
- package/dist/plugins/account/errors.mjs.map +1 -1
- package/dist/plugins/account/funding.mjs +3 -27
- package/dist/plugins/account/funding.mjs.map +1 -1
- package/dist/plugins/account/index.d.mts +3 -8
- package/dist/plugins/account/index.mjs +71 -35
- package/dist/plugins/account/index.mjs.map +1 -1
- package/dist/plugins/account/keypair.d.mts +8 -7
- package/dist/plugins/account/keypair.mjs +5 -18
- package/dist/plugins/account/keypair.mjs.map +1 -1
- package/dist/plugins/account/lease.mjs.map +1 -1
- package/dist/plugins/account/registry.d.mts +1 -46
- package/dist/plugins/account/registry.mjs.map +1 -1
- package/dist/plugins/account/service.d.mts +43 -34
- package/dist/plugins/account/service.mjs +9 -54
- package/dist/plugins/account/service.mjs.map +1 -1
- package/dist/plugins/account/snapshot.mjs.map +1 -1
- package/dist/plugins/account/variants/ephemeral.mjs.map +1 -1
- package/dist/plugins/account/variants/impersonate.mjs.map +1 -1
- package/dist/plugins/account/variants/signer.mjs.map +1 -1
- package/dist/plugins/action/discriminator.mjs.map +1 -1
- package/dist/plugins/action/errors.mjs +1 -4
- package/dist/plugins/action/errors.mjs.map +1 -1
- package/dist/plugins/action/execute.mjs +3 -4
- package/dist/plugins/action/execute.mjs.map +1 -1
- package/dist/plugins/action/index.d.mts +2 -2
- package/dist/plugins/action/index.mjs +7 -11
- package/dist/plugins/action/index.mjs.map +1 -1
- package/dist/plugins/action/service.d.mts +5 -7
- package/dist/plugins/action/service.mjs +2 -16
- package/dist/plugins/action/service.mjs.map +1 -1
- package/dist/plugins/coin/address-resolution.mjs.map +1 -1
- package/dist/plugins/coin/codegen.mjs +97 -16
- package/dist/plugins/coin/codegen.mjs.map +1 -1
- package/dist/plugins/coin/discovery.mjs.map +1 -1
- package/dist/plugins/coin/errors.mjs +1 -4
- package/dist/plugins/coin/errors.mjs.map +1 -1
- package/dist/plugins/coin/index.d.mts +4 -34
- package/dist/plugins/coin/index.mjs +47 -28
- package/dist/plugins/coin/index.mjs.map +1 -1
- package/dist/plugins/coin/metadata.d.mts +1 -0
- package/dist/plugins/coin/metadata.mjs +8 -9
- package/dist/plugins/coin/metadata.mjs.map +1 -1
- package/dist/plugins/coin/mint.mjs +7 -21
- package/dist/plugins/coin/mint.mjs.map +1 -1
- package/dist/plugins/coin/registry.mjs +33 -20
- package/dist/plugins/coin/registry.mjs.map +1 -1
- package/dist/plugins/coin/service.d.mts +1 -1
- package/dist/plugins/coin/service.mjs +1 -8
- package/dist/plugins/coin/service.mjs.map +1 -1
- package/dist/plugins/coin/snapshot.mjs.map +1 -1
- package/dist/plugins/coin/type-strings.mjs.map +1 -1
- package/dist/plugins/dashboard/domain.mjs +106 -167
- package/dist/plugins/dashboard/domain.mjs.map +1 -1
- package/dist/plugins/dashboard/index.mjs +13 -13
- package/dist/plugins/dashboard/index.mjs.map +1 -1
- package/dist/plugins/dashboard/origin-policy.mjs +3 -3
- package/dist/plugins/dashboard/origin-policy.mjs.map +1 -1
- package/dist/plugins/dashboard/routable.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/builder.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/enums.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/root.mjs +23 -44
- package/dist/plugins/dashboard/schema/root.mjs.map +1 -1
- package/dist/plugins/dashboard/schema/types.mjs +37 -91
- package/dist/plugins/dashboard/schema/types.mjs.map +1 -1
- package/dist/plugins/dashboard/schema.mjs.map +1 -1
- package/dist/plugins/dashboard/server.mjs.map +1 -1
- package/dist/plugins/deepbook/codegen.d.mts +12 -11
- package/dist/plugins/deepbook/codegen.mjs +108 -11
- package/dist/plugins/deepbook/codegen.mjs.map +1 -1
- package/dist/plugins/deepbook/deploy.mjs +7 -14
- package/dist/plugins/deepbook/deploy.mjs.map +1 -1
- package/dist/plugins/deepbook/errors.mjs +4 -2
- package/dist/plugins/deepbook/errors.mjs.map +1 -1
- package/dist/plugins/deepbook/faucet-strategy.mjs +4 -10
- package/dist/plugins/deepbook/faucet-strategy.mjs.map +1 -1
- package/dist/plugins/deepbook/hash.mjs.map +1 -1
- package/dist/plugins/deepbook/index.d.mts +38 -34
- package/dist/plugins/deepbook/index.mjs +133 -64
- package/dist/plugins/deepbook/index.mjs.map +1 -1
- package/dist/plugins/deepbook/plugin-key.mjs.map +1 -1
- package/dist/plugins/deepbook/pyth/index.mjs +4 -8
- package/dist/plugins/deepbook/pyth/index.mjs.map +1 -1
- package/dist/plugins/deepbook/snapshot.mjs +0 -1
- package/dist/plugins/deepbook/snapshot.mjs.map +1 -1
- package/dist/plugins/deepbook/type-strings.mjs.map +1 -1
- package/dist/plugins/deepbook/types.d.mts +2 -1
- package/dist/plugins/deepbook/types.mjs.map +1 -1
- package/dist/plugins/faucet/dispatcher.d.mts +3 -3
- package/dist/plugins/faucet/dispatcher.mjs.map +1 -1
- package/dist/plugins/faucet/errors.mjs.map +1 -1
- package/dist/plugins/faucet/http.mjs +2 -10
- package/dist/plugins/faucet/http.mjs.map +1 -1
- package/dist/plugins/faucet/index.mjs +0 -1
- package/dist/plugins/faucet/index.mjs.map +1 -1
- package/dist/plugins/host-service/errors.d.mts +1 -3
- package/dist/plugins/host-service/errors.mjs +1 -2
- package/dist/plugins/host-service/errors.mjs.map +1 -1
- package/dist/plugins/host-service/index.d.mts +1 -2
- package/dist/plugins/host-service/index.mjs +11 -11
- package/dist/plugins/host-service/index.mjs.map +1 -1
- package/dist/plugins/host-service/routable.mjs.map +1 -1
- package/dist/plugins/host-service/service.mjs +13 -13
- package/dist/plugins/host-service/service.mjs.map +1 -1
- package/dist/plugins/internal/acquire-on-chain-artifact.mjs.map +1 -1
- package/dist/plugins/internal/codegen-helpers.mjs +2 -0
- package/dist/plugins/internal/codegen-helpers.mjs.map +1 -1
- package/dist/plugins/internal/funding-failure-error.mjs.map +1 -1
- package/dist/plugins/{postgres → internal/postgres-sidecar}/connection.mjs +1 -1
- package/dist/plugins/internal/postgres-sidecar/connection.mjs.map +1 -0
- package/dist/plugins/{postgres → internal/postgres-sidecar}/db-ensure.mjs +30 -16
- package/dist/plugins/internal/postgres-sidecar/db-ensure.mjs.map +1 -0
- package/dist/plugins/internal/postgres-sidecar/errors.mjs +21 -0
- package/dist/plugins/internal/postgres-sidecar/errors.mjs.map +1 -0
- package/dist/{substrate/runtime/scoped-multimap → plugins/internal/postgres-sidecar}/index.mjs +1 -0
- package/dist/plugins/internal/postgres-sidecar/service.mjs +121 -0
- package/dist/plugins/internal/postgres-sidecar/service.mjs.map +1 -0
- package/dist/plugins/package/build.mjs +2 -2
- package/dist/plugins/package/build.mjs.map +1 -1
- package/dist/plugins/package/codegen.d.mts +7 -14
- package/dist/plugins/package/codegen.mjs +187 -59
- package/dist/plugins/package/codegen.mjs.map +1 -1
- package/dist/plugins/package/dep-resolution.mjs +51 -3
- package/dist/plugins/package/dep-resolution.mjs.map +1 -1
- package/dist/plugins/package/errors.mjs +1 -4
- package/dist/plugins/package/errors.mjs.map +1 -1
- package/dist/plugins/package/git-source.d.mts +18 -0
- package/dist/plugins/package/git-source.mjs +119 -0
- package/dist/plugins/package/git-source.mjs.map +1 -0
- package/dist/plugins/package/index.d.mts +28 -19
- package/dist/plugins/package/index.mjs +107 -32
- package/dist/plugins/package/index.mjs.map +1 -1
- package/dist/plugins/package/mode-known.mjs +2 -2
- package/dist/plugins/package/mode-known.mjs.map +1 -1
- package/dist/plugins/package/mode-local.mjs +18 -35
- package/dist/plugins/package/mode-local.mjs.map +1 -1
- package/dist/plugins/package/publish-executor.mjs +13 -8
- package/dist/plugins/package/publish-executor.mjs.map +1 -1
- package/dist/plugins/package/publish-output.d.mts +1 -11
- package/dist/plugins/package/publish-output.mjs +1 -9
- package/dist/plugins/package/publish-output.mjs.map +1 -1
- package/dist/plugins/package/registry.d.mts +1 -1
- package/dist/plugins/package/registry.mjs +26 -14
- package/dist/plugins/package/registry.mjs.map +1 -1
- package/dist/plugins/package/service.mjs.map +1 -1
- package/dist/plugins/package/snapshot.mjs.map +1 -1
- package/dist/plugins/router-entrypoints.mjs +1 -3
- package/dist/plugins/router-entrypoints.mjs.map +1 -1
- package/dist/plugins/seal/bootstrap-assets/cargo-image.mjs +1 -2
- package/dist/plugins/seal/bootstrap-assets/cargo-image.mjs.map +1 -1
- package/dist/plugins/seal/bootstrap-assets/source-fetch.mjs +1 -6
- package/dist/plugins/seal/bootstrap-assets/source-fetch.mjs.map +1 -1
- package/dist/plugins/seal/codegen.d.mts +1 -5
- package/dist/plugins/seal/codegen.mjs +83 -12
- package/dist/plugins/seal/codegen.mjs.map +1 -1
- package/dist/plugins/seal/config-render.mjs +2 -6
- package/dist/plugins/seal/config-render.mjs.map +1 -1
- package/dist/plugins/seal/deploy.mjs +18 -29
- package/dist/plugins/seal/deploy.mjs.map +1 -1
- package/dist/plugins/seal/errors.d.mts +4 -5
- package/dist/plugins/seal/errors.mjs +1 -8
- package/dist/plugins/seal/errors.mjs.map +1 -1
- package/dist/plugins/seal/index.d.mts +7 -28
- package/dist/plugins/seal/index.mjs +69 -47
- package/dist/plugins/seal/index.mjs.map +1 -1
- package/dist/plugins/seal/key-manager.mjs.map +1 -1
- package/dist/plugins/seal/key-server.mjs +1 -6
- package/dist/plugins/seal/key-server.mjs.map +1 -1
- package/dist/plugins/seal/keygen.mjs +2 -3
- package/dist/plugins/seal/keygen.mjs.map +1 -1
- package/dist/plugins/seal/mode/fork-known.mjs.map +1 -1
- package/dist/plugins/seal/mode/live.mjs +1 -1
- package/dist/plugins/seal/mode/live.mjs.map +1 -1
- package/dist/plugins/seal/mode/local-keygen.mjs +4 -10
- package/dist/plugins/seal/mode/local-keygen.mjs.map +1 -1
- package/dist/plugins/seal/plugin-key.mjs.map +1 -1
- package/dist/plugins/seal/registry-publish.mjs.map +1 -1
- package/dist/plugins/seal/routable.mjs.map +1 -1
- package/dist/plugins/seal/service.mjs.map +1 -1
- package/dist/plugins/seal/snapshot.mjs +1 -2
- package/dist/plugins/seal/snapshot.mjs.map +1 -1
- package/dist/plugins/sui/auto-tick.mjs +5 -5
- package/dist/plugins/sui/auto-tick.mjs.map +1 -1
- package/dist/plugins/sui/chain-build-container.mjs +2 -2
- package/dist/plugins/sui/chain-build-container.mjs.map +1 -1
- package/dist/plugins/sui/chain-probe.mjs +4 -4
- package/dist/plugins/sui/chain-probe.mjs.map +1 -1
- package/dist/plugins/sui/codegen.d.mts +9 -7
- package/dist/plugins/sui/codegen.mjs +49 -46
- package/dist/plugins/sui/codegen.mjs.map +1 -1
- package/dist/plugins/sui/errors.d.mts +1 -1
- package/dist/plugins/sui/errors.mjs +1 -9
- package/dist/plugins/sui/errors.mjs.map +1 -1
- package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/index.d.mts +1 -1
- package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/index.mjs +3 -7
- package/dist/plugins/sui/exec/index.mjs.map +1 -0
- package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/sign-and-dispatch.mjs +1 -1
- package/dist/plugins/sui/exec/sign-and-dispatch.mjs.map +1 -0
- package/dist/plugins/sui/fork-faucet-strategy.mjs +2 -6
- package/dist/plugins/sui/fork-faucet-strategy.mjs.map +1 -1
- package/dist/plugins/sui/fork-orchestration.mjs +13 -23
- package/dist/plugins/sui/fork-orchestration.mjs.map +1 -1
- package/dist/plugins/sui/fork-transaction.mjs.map +1 -1
- package/dist/plugins/sui/index.d.mts +34 -83
- package/dist/plugins/sui/index.mjs +200 -68
- package/dist/plugins/sui/index.mjs.map +1 -1
- package/dist/{substrate/runtime/sui-ledger → plugins/sui/ledger}/object-ref.mjs +1 -1
- package/dist/plugins/sui/ledger/object-ref.mjs.map +1 -0
- package/dist/plugins/sui/local-faucet-strategy.mjs +1 -5
- package/dist/plugins/sui/local-faucet-strategy.mjs.map +1 -1
- package/dist/plugins/sui/log-attrs.mjs +11 -0
- package/dist/plugins/sui/log-attrs.mjs.map +1 -0
- package/dist/plugins/sui/mode/external.mjs +4 -12
- package/dist/plugins/sui/mode/external.mjs.map +1 -1
- package/dist/plugins/sui/mode/fork.mjs +19 -16
- package/dist/plugins/sui/mode/fork.mjs.map +1 -1
- package/dist/plugins/sui/mode/live.mjs +4 -17
- package/dist/plugins/sui/mode/live.mjs.map +1 -1
- package/dist/plugins/sui/mode/local.d.mts +1 -0
- package/dist/plugins/sui/mode/local.mjs +203 -61
- package/dist/plugins/sui/mode/local.mjs.map +1 -1
- package/dist/plugins/sui/mode/shared-boot.mjs +18 -23
- package/dist/plugins/sui/mode/shared-boot.mjs.map +1 -1
- package/dist/plugins/sui/mode/shared.d.mts +6 -7
- package/dist/plugins/sui/mode/shared.mjs.map +1 -1
- package/dist/plugins/sui/mode/spec.d.mts +23 -6
- package/dist/{substrate/runtime/sui-move-build → plugins/sui/move}/index.mjs +28 -30
- package/dist/plugins/sui/move/index.mjs.map +1 -0
- package/dist/plugins/sui/move-summary-runner.mjs +8 -4
- package/dist/plugins/sui/move-summary-runner.mjs.map +1 -1
- package/dist/{substrate/network.d.mts → plugins/sui/network-config.d.mts} +11 -10
- package/dist/plugins/sui/network-resolver.d.mts +6 -6
- package/dist/plugins/sui/routable.mjs.map +1 -1
- package/dist/plugins/sui/service.mjs +10 -6
- package/dist/plugins/sui/service.mjs.map +1 -1
- package/dist/plugins/sui/snapshot.mjs +10 -5
- package/dist/plugins/sui/snapshot.mjs.map +1 -1
- package/dist/plugins/wallet/codegen.d.mts +8 -6
- package/dist/plugins/wallet/codegen.mjs +4 -10
- package/dist/plugins/wallet/codegen.mjs.map +1 -1
- package/dist/plugins/wallet/errors.mjs +1 -4
- package/dist/plugins/wallet/errors.mjs.map +1 -1
- package/dist/plugins/wallet/index.d.mts +3 -6
- package/dist/plugins/wallet/index.mjs +16 -21
- package/dist/plugins/wallet/index.mjs.map +1 -1
- package/dist/plugins/wallet/origin-policy.mjs +3 -3
- package/dist/plugins/wallet/origin-policy.mjs.map +1 -1
- package/dist/plugins/wallet/pairing.mjs +6 -24
- package/dist/plugins/wallet/pairing.mjs.map +1 -1
- package/dist/plugins/wallet/protocol.mjs.map +1 -1
- package/dist/plugins/wallet/routable.mjs.map +1 -1
- package/dist/plugins/wallet/server.mjs +17 -24
- package/dist/plugins/wallet/server.mjs.map +1 -1
- package/dist/plugins/wallet/service.d.mts +2 -2
- package/dist/plugins/wallet/service.mjs +4 -15
- package/dist/plugins/wallet/service.mjs.map +1 -1
- package/dist/plugins/wallet/snapshot.mjs.map +1 -1
- package/dist/plugins/walrus/bootstrap-assets/cargo-image.mjs +1 -5
- package/dist/plugins/walrus/bootstrap-assets/cargo-image.mjs.map +1 -1
- package/dist/plugins/walrus/codegen.d.mts +2 -13
- package/dist/plugins/walrus/codegen.mjs +93 -28
- package/dist/plugins/walrus/codegen.mjs.map +1 -1
- package/dist/plugins/walrus/deploy-paths.mjs +2 -3
- package/dist/plugins/walrus/deploy-paths.mjs.map +1 -1
- package/dist/plugins/walrus/deploy.mjs +26 -19
- package/dist/plugins/walrus/deploy.mjs.map +1 -1
- package/dist/plugins/walrus/errors.mjs +1 -8
- package/dist/plugins/walrus/errors.mjs.map +1 -1
- package/dist/plugins/walrus/faucet-strategy.mjs +15 -1
- package/dist/plugins/walrus/faucet-strategy.mjs.map +1 -1
- package/dist/plugins/walrus/index.d.mts +13 -44
- package/dist/plugins/walrus/index.mjs +119 -122
- package/dist/plugins/walrus/index.mjs.map +1 -1
- package/dist/plugins/walrus/mode/known-deploy.mjs +6 -6
- package/dist/plugins/walrus/mode/known-deploy.mjs.map +1 -1
- package/dist/plugins/walrus/mode/local-cluster.d.mts +2 -2
- package/dist/plugins/walrus/mode/local-cluster.mjs +2 -10
- package/dist/plugins/walrus/mode/local-cluster.mjs.map +1 -1
- package/dist/plugins/walrus/plugin-key.mjs.map +1 -1
- package/dist/plugins/walrus/registry-publish.mjs.map +1 -1
- package/dist/plugins/walrus/routable.mjs.map +1 -1
- package/dist/plugins/walrus/service.mjs.map +1 -1
- package/dist/plugins/walrus/snapshot.mjs +3 -3
- package/dist/plugins/walrus/snapshot.mjs.map +1 -1
- package/dist/plugins/walrus/storage-nodes.mjs +53 -14
- package/dist/plugins/walrus/storage-nodes.mjs.map +1 -1
- package/dist/plugins/walrus/wal-swap.mjs +4 -8
- package/dist/plugins/walrus/wal-swap.mjs.map +1 -1
- package/dist/primitives/artifact-publisher.mjs.map +1 -1
- package/dist/primitives/cache.d.mts +1 -1
- package/dist/runtime/docker/client.mjs +47 -3
- package/dist/runtime/docker/client.mjs.map +1 -1
- package/dist/runtime/docker/container.mjs +37 -57
- package/dist/runtime/docker/container.mjs.map +1 -1
- package/dist/runtime/docker/errors.mjs.map +1 -1
- package/dist/runtime/docker/exec.mjs +2 -2
- package/dist/runtime/docker/exec.mjs.map +1 -1
- package/dist/runtime/docker/image.mjs +10 -11
- package/dist/runtime/docker/image.mjs.map +1 -1
- package/dist/runtime/docker/index.mjs +0 -1
- package/dist/runtime/docker/inspect-and-decode.mjs.map +1 -1
- package/dist/runtime/docker/inventory.mjs +9 -9
- package/dist/runtime/docker/inventory.mjs.map +1 -1
- package/dist/runtime/docker/labels.mjs +12 -1
- package/dist/runtime/docker/labels.mjs.map +1 -1
- package/dist/runtime/docker/network.mjs +6 -6
- package/dist/runtime/docker/network.mjs.map +1 -1
- package/dist/runtime/docker/render-run-args.mjs.map +1 -1
- package/dist/runtime/docker/service.mjs +31 -45
- package/dist/runtime/docker/service.mjs.map +1 -1
- package/dist/runtime/docker/sweep.mjs +14 -64
- package/dist/runtime/docker/sweep.mjs.map +1 -1
- package/dist/runtime/docker/volume.mjs +1 -1
- package/dist/runtime/docker/volume.mjs.map +1 -1
- package/dist/runtime/docker/wrap.mjs +14 -2
- package/dist/runtime/docker/wrap.mjs.map +1 -1
- package/dist/substrate/brand.d.mts +2 -5
- package/dist/substrate/brand.mjs +3 -2
- package/dist/substrate/brand.mjs.map +1 -1
- package/dist/substrate/cross-process.mjs +2 -16
- package/dist/substrate/cross-process.mjs.map +1 -1
- package/dist/substrate/event-time.mjs +0 -2
- package/dist/substrate/event-time.mjs.map +1 -1
- package/dist/substrate/events.d.mts +46 -16
- package/dist/substrate/identity.d.mts +11 -5
- package/dist/substrate/manifest.d.mts +33 -26
- package/dist/substrate/manifest.mjs +8 -3
- package/dist/substrate/manifest.mjs.map +1 -1
- package/dist/substrate/options.d.mts +33 -4
- package/dist/substrate/plugin-ctx.d.mts +70 -0
- package/dist/substrate/plugin-ctx.mjs +66 -0
- package/dist/substrate/plugin-ctx.mjs.map +1 -0
- package/dist/substrate/plugin.d.mts +46 -38
- package/dist/substrate/plugin.mjs +13 -9
- package/dist/substrate/plugin.mjs.map +1 -1
- package/dist/substrate/projection.d.mts +1 -1
- package/dist/substrate/runtime/atomic-write.mjs +3 -4
- package/dist/substrate/runtime/atomic-write.mjs.map +1 -1
- package/dist/substrate/runtime/cache/schema.mjs.map +1 -1
- package/dist/substrate/runtime/cache/service.mjs +50 -20
- package/dist/substrate/runtime/cache/service.mjs.map +1 -1
- package/dist/substrate/runtime/config-validation.mjs.map +1 -1
- package/dist/substrate/runtime/control-plane/domain.mjs +34 -12
- package/dist/substrate/runtime/control-plane/domain.mjs.map +1 -1
- package/dist/substrate/runtime/control-plane/service.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/command-channel/channel.mjs +14 -17
- package/dist/substrate/runtime/cross-process/command-channel/channel.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/command-channel/file-channel.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/command-channel/protocol.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/command-channel/runtime-control-lock.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/index.mjs +0 -2
- package/dist/substrate/runtime/cross-process/live-clock.mjs +1 -1
- package/dist/substrate/runtime/cross-process/live-clock.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/liveness.mjs +11 -18
- package/dist/substrate/runtime/cross-process/liveness.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/reclaim-stale-file.mjs +2 -2
- package/dist/substrate/runtime/cross-process/reclaim-stale-file.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/roster.mjs +17 -127
- package/dist/substrate/runtime/cross-process/roster.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/self-pid.mjs.map +1 -1
- package/dist/substrate/runtime/cross-process/stack-lock.mjs +29 -28
- package/dist/substrate/runtime/cross-process/stack-lock.mjs.map +1 -1
- package/dist/substrate/runtime/current-plugin.mjs.map +1 -1
- package/dist/substrate/runtime/errors.mjs +1 -13
- package/dist/substrate/runtime/errors.mjs.map +1 -1
- package/dist/substrate/runtime/format-unknown-error.mjs.map +1 -1
- package/dist/substrate/runtime/host-bind-mount-owner.mjs.map +1 -1
- package/dist/substrate/runtime/host-gateway.mjs.map +1 -1
- package/dist/substrate/runtime/host-tree-tar/index.mjs +15 -163
- package/dist/substrate/runtime/host-tree-tar/index.mjs.map +1 -1
- package/dist/substrate/runtime/http-probe.d.mts +1 -3
- package/dist/substrate/runtime/http-probe.mjs.map +1 -1
- package/dist/substrate/runtime/index.mjs +1 -6
- package/dist/substrate/runtime/lease-broker/service.mjs +5 -23
- package/dist/substrate/runtime/lease-broker/service.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/dep-graph.mjs +3 -3
- package/dist/substrate/runtime/lifecycle/dep-graph.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/graph-input-id.d.mts +1 -0
- package/dist/substrate/runtime/lifecycle/graph-input-id.mjs +133 -0
- package/dist/substrate/runtime/lifecycle/graph-input-id.mjs.map +1 -0
- package/dist/substrate/runtime/lifecycle/index.mjs +2 -2
- package/dist/substrate/runtime/lifecycle/plugin-registry.d.mts +1 -1
- package/dist/substrate/runtime/lifecycle/plugin-registry.mjs +22 -1
- package/dist/substrate/runtime/lifecycle/plugin-registry.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/ready-gate.mjs +2 -15
- package/dist/substrate/runtime/lifecycle/ready-gate.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/selective-restart.mjs +29 -25
- package/dist/substrate/runtime/lifecycle/selective-restart.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/signals.mjs +1 -1
- package/dist/substrate/runtime/lifecycle/signals.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/state-machine.mjs.map +1 -1
- package/dist/substrate/runtime/lifecycle/watch-attribution.d.mts +1 -1
- package/dist/substrate/runtime/lifecycle/watch-attribution.mjs +0 -1
- package/dist/substrate/runtime/lifecycle/watch-attribution.mjs.map +1 -1
- package/dist/substrate/runtime/managed-container.d.mts +5 -1
- package/dist/substrate/runtime/managed-container.mjs +8 -10
- package/dist/substrate/runtime/managed-container.mjs.map +1 -1
- package/dist/substrate/runtime/manifest/manifest.mjs +8 -30
- package/dist/substrate/runtime/manifest/manifest.mjs.map +1 -1
- package/dist/substrate/runtime/mode-errors.d.mts +1 -2
- package/dist/substrate/runtime/mode-errors.mjs.map +1 -1
- package/dist/substrate/runtime/observability/cascade-formatter.mjs +1 -11
- package/dist/substrate/runtime/observability/cascade-formatter.mjs.map +1 -1
- package/dist/substrate/runtime/observability/ignore-with-log.mjs.map +1 -1
- package/dist/substrate/runtime/observability/index.d.mts +1 -1
- package/dist/substrate/runtime/observability/index.mjs +4 -6
- package/dist/substrate/runtime/observability/log-attrs.mjs +46 -0
- package/dist/substrate/runtime/observability/log-attrs.mjs.map +1 -0
- package/dist/substrate/runtime/observability/log-store.mjs.map +1 -1
- package/dist/substrate/runtime/observability/logger.mjs +3 -3
- package/dist/substrate/runtime/observability/logger.mjs.map +1 -1
- package/dist/substrate/runtime/observability/output-truncate.mjs.map +1 -1
- package/dist/substrate/runtime/observability/pretty-error.mjs.map +1 -1
- package/dist/substrate/runtime/observability/process-lines.mjs.map +1 -1
- package/dist/substrate/runtime/observability/redaction.mjs.map +1 -1
- package/dist/substrate/runtime/observability/subprocess-capture.mjs +2 -2
- package/dist/substrate/runtime/observability/subprocess-capture.mjs.map +1 -1
- package/dist/substrate/runtime/passthrough-or-wrap.mjs.map +1 -1
- package/dist/substrate/runtime/paths.mjs +27 -9
- package/dist/substrate/runtime/paths.mjs.map +1 -1
- package/dist/substrate/runtime/phase-preserving-produce.mjs.map +1 -1
- package/dist/substrate/runtime/port-broker/service.mjs +10 -27
- package/dist/substrate/runtime/port-broker/service.mjs.map +1 -1
- package/dist/substrate/runtime/post-acquire-tasks.mjs.map +1 -1
- package/dist/substrate/runtime/probes.d.mts +2 -3
- package/dist/substrate/runtime/probes.mjs.map +1 -1
- package/dist/substrate/runtime/process-supervisor.mjs.map +1 -1
- package/dist/substrate/runtime/projection/operational-endpoints.mjs.map +1 -1
- package/dist/substrate/runtime/projection/state-ref.mjs.map +1 -1
- package/dist/substrate/runtime/projection/update.mjs +87 -11
- package/dist/substrate/runtime/projection/update.mjs.map +1 -1
- package/dist/substrate/runtime/random-suffix.mjs.map +1 -1
- package/dist/substrate/runtime/reconcile/fs-plan.mjs +146 -0
- package/dist/substrate/runtime/reconcile/fs-plan.mjs.map +1 -0
- package/dist/substrate/runtime/reconcile/graph.mjs +19 -0
- package/dist/substrate/runtime/reconcile/graph.mjs.map +1 -0
- package/dist/substrate/runtime/reconcile/index.mjs +5 -0
- package/dist/substrate/runtime/reconcile/label.mjs +41 -0
- package/dist/substrate/runtime/reconcile/label.mjs.map +1 -0
- package/dist/substrate/runtime/reconcile/spec.mjs +15 -0
- package/dist/substrate/runtime/reconcile/spec.mjs.map +1 -0
- package/dist/substrate/runtime/retry-policy.d.mts +2 -3
- package/dist/substrate/runtime/retry-policy.mjs.map +1 -1
- package/dist/substrate/runtime/routed-url.mjs.map +1 -1
- package/dist/substrate/runtime/runtime-decode.mjs.map +1 -1
- package/dist/substrate/runtime/scoped-http-server.mjs.map +1 -1
- package/dist/substrate/runtime/stage-and-swap/index.mjs +49 -12
- package/dist/substrate/runtime/stage-and-swap/index.mjs.map +1 -1
- package/dist/substrate/runtime/strategy-registry/chain-keyed-strategy-for.mjs.map +1 -1
- package/dist/substrate/runtime/strategy-registry/chain-probe-for.mjs.map +1 -1
- package/dist/substrate/runtime/strategy-registry/service.mjs +29 -17
- package/dist/substrate/runtime/strategy-registry/service.mjs.map +1 -1
- package/dist/substrate/runtime/subnet-broker.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/acquire-node.mjs +188 -56
- package/dist/substrate/runtime/supervisor/acquire-node.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/background-tasks.mjs +79 -65
- package/dist/substrate/runtime/supervisor/background-tasks.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/command-loop.mjs +82 -23
- package/dist/substrate/runtime/supervisor/command-loop.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/contribution-dispatcher.mjs +16 -0
- package/dist/substrate/runtime/supervisor/contribution-dispatcher.mjs.map +1 -0
- package/dist/substrate/runtime/supervisor/errors.mjs +11 -2
- package/dist/substrate/runtime/supervisor/errors.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/index.mjs +2 -0
- package/dist/substrate/runtime/supervisor/shutdown.mjs +6 -6
- package/dist/substrate/runtime/supervisor/shutdown.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs +60 -70
- package/dist/substrate/runtime/supervisor/start-supervisor.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/state.d.mts +1 -1
- package/dist/substrate/runtime/supervisor/state.mjs +1 -1
- package/dist/substrate/runtime/supervisor/state.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/teardown.mjs +10 -9
- package/dist/substrate/runtime/supervisor/teardown.mjs.map +1 -1
- package/dist/substrate/runtime/supervisor/wiring.mjs +6 -20
- package/dist/substrate/runtime/supervisor/wiring.mjs.map +1 -1
- package/dist/substrate/runtime/tar/reader.mjs +216 -0
- package/dist/substrate/runtime/tar/reader.mjs.map +1 -0
- package/dist/substrate/runtime/typed-env.mjs.map +1 -1
- package/dist/substrate/versioned-doc-schema.mjs.map +1 -1
- package/dist/substrate/versioned-doc-sync.mjs +5 -5
- package/dist/substrate/versioned-doc-sync.mjs.map +1 -1
- package/dist/surfaces/cli/command-tree.mjs +49 -7
- package/dist/surfaces/cli/command-tree.mjs.map +1 -1
- package/dist/surfaces/cli/commands/config.mjs +1 -1
- package/dist/surfaces/cli/commands/config.mjs.map +1 -1
- package/dist/surfaces/cli/commands/confirm-node.mjs.map +1 -1
- package/dist/surfaces/cli/commands/confirm.mjs.map +1 -1
- package/dist/surfaces/cli/commands/doctor.mjs +1 -1
- package/dist/surfaces/cli/commands/doctor.mjs.map +1 -1
- package/dist/surfaces/cli/commands/prune-picker-entry.mjs.map +1 -1
- package/dist/surfaces/cli/commands/prune-picker.mjs.map +1 -1
- package/dist/surfaces/cli/commands/prune.mjs +1 -1
- package/dist/surfaces/cli/commands/prune.mjs.map +1 -1
- package/dist/surfaces/cli/commands/snapshot.mjs +1 -1
- package/dist/surfaces/cli/commands/snapshot.mjs.map +1 -1
- package/dist/surfaces/cli/commands/status.mjs +1 -1
- package/dist/surfaces/cli/commands/status.mjs.map +1 -1
- package/dist/surfaces/cli/commands/supervisor-presence.mjs.map +1 -1
- package/dist/surfaces/cli/commands/wipe.mjs +3 -4
- package/dist/surfaces/cli/commands/wipe.mjs.map +1 -1
- package/dist/surfaces/cli/envelope.mjs.map +1 -1
- package/dist/surfaces/cli/errors.mjs.map +1 -1
- package/dist/surfaces/cli/flags.mjs.map +1 -1
- package/dist/surfaces/cli/index.mjs +45 -14
- package/dist/surfaces/cli/index.mjs.map +1 -1
- package/dist/surfaces/cli/output.mjs.map +1 -1
- package/dist/surfaces/cli/sysexits.mjs.map +1 -1
- package/dist/surfaces/tui/app.mjs +0 -21
- package/dist/surfaces/tui/app.mjs.map +1 -1
- package/dist/surfaces/tui/dashboard.mjs +0 -48
- package/dist/surfaces/tui/dashboard.mjs.map +1 -1
- package/dist/surfaces/tui/display-derivation.mjs +80 -16
- package/dist/surfaces/tui/display-derivation.mjs.map +1 -1
- package/dist/surfaces/tui/errors.mjs.map +1 -1
- package/dist/surfaces/tui/event-log.mjs +0 -9
- package/dist/surfaces/tui/event-log.mjs.map +1 -1
- package/dist/surfaces/tui/heartbeat.mjs.map +1 -1
- package/dist/surfaces/tui/index.mjs +2 -2
- package/dist/surfaces/tui/index.mjs.map +1 -1
- package/dist/surfaces/tui/input.mjs.map +1 -1
- package/dist/surfaces/tui/mode-detect.mjs.map +1 -1
- package/dist/surfaces/tui/mount-ink.mjs.map +1 -1
- package/dist/surfaces/tui/plain-renderer.mjs +26 -19
- package/dist/surfaces/tui/plain-renderer.mjs.map +1 -1
- package/dist/surfaces/tui/resource-table.mjs +8 -10
- package/dist/surfaces/tui/resource-table.mjs.map +1 -1
- package/images/postgres/Dockerfile +0 -1
- package/images/sui/Dockerfile +27 -54
- package/images/sui/entrypoint.sh +17 -179
- package/package.json +23 -15
- package/dashboard-ui/assets/index-Bmi1UtAg.js +0 -1356
- package/dashboard-ui/assets/index-D5EShVt4.js +0 -3
- package/dashboard-ui/assets/index-Deml9drg.css +0 -1
- package/dist/api/plugin-errors.d.mts +0 -7
- package/dist/api/plugin-errors.mjs +0 -10
- package/dist/api/plugin-errors.mjs.map +0 -1
- package/dist/cli/wirings/build-verb-layers.mjs +0 -42
- package/dist/cli/wirings/build-verb-layers.mjs.map +0 -1
- package/dist/contracts/capability-decl.d.mts +0 -40
- package/dist/orchestrators/built-in-plugin-layers.mjs +0 -54
- package/dist/orchestrators/built-in-plugin-layers.mjs.map +0 -1
- package/dist/orchestrators/run.mjs +0 -91
- package/dist/orchestrators/run.mjs.map +0 -1
- package/dist/orchestrators/runtime-composition.d.mts +0 -10
- package/dist/orchestrators/runtime-composition.mjs +0 -252
- package/dist/orchestrators/runtime-composition.mjs.map +0 -1
- package/dist/orchestrators/snapshot/capture-command.d.mts +0 -1
- package/dist/orchestrators/snapshot/capture-command.mjs +0 -25
- package/dist/orchestrators/snapshot/capture-command.mjs.map +0 -1
- package/dist/orchestrators/snapshot/pending-marker.mjs +0 -99
- package/dist/orchestrators/snapshot/pending-marker.mjs.map +0 -1
- package/dist/orchestrators/snapshot/recover-pending.mjs +0 -223
- package/dist/orchestrators/snapshot/recover-pending.mjs.map +0 -1
- package/dist/orchestrators/snapshot/state-document.d.mts +0 -1
- package/dist/orchestrators/snapshot/state-document.mjs +0 -58
- package/dist/orchestrators/snapshot/state-document.mjs.map +0 -1
- package/dist/plugins/account/spans.mjs +0 -16
- package/dist/plugins/account/spans.mjs.map +0 -1
- package/dist/plugins/account/variants/env.mjs +0 -24
- package/dist/plugins/account/variants/env.mjs.map +0 -1
- package/dist/plugins/account/variants/inline.mjs +0 -13
- package/dist/plugins/account/variants/inline.mjs.map +0 -1
- package/dist/plugins/account/variants/keystore.mjs +0 -91
- package/dist/plugins/account/variants/keystore.mjs.map +0 -1
- package/dist/plugins/action/spans.mjs +0 -11
- package/dist/plugins/action/spans.mjs.map +0 -1
- package/dist/plugins/coin/spans.mjs +0 -20
- package/dist/plugins/coin/spans.mjs.map +0 -1
- package/dist/plugins/deepbook/spans.mjs +0 -18
- package/dist/plugins/deepbook/spans.mjs.map +0 -1
- package/dist/plugins/faucet/spans.mjs +0 -12
- package/dist/plugins/faucet/spans.mjs.map +0 -1
- package/dist/plugins/package/spans.mjs +0 -14
- package/dist/plugins/package/spans.mjs.map +0 -1
- package/dist/plugins/postgres/codegen.mjs +0 -42
- package/dist/plugins/postgres/codegen.mjs.map +0 -1
- package/dist/plugins/postgres/connection.d.mts +0 -33
- package/dist/plugins/postgres/connection.mjs.map +0 -1
- package/dist/plugins/postgres/db-ensure.mjs.map +0 -1
- package/dist/plugins/postgres/errors.d.mts +0 -60
- package/dist/plugins/postgres/errors.mjs +0 -29
- package/dist/plugins/postgres/errors.mjs.map +0 -1
- package/dist/plugins/postgres/index.d.mts +0 -37
- package/dist/plugins/postgres/index.mjs +0 -68
- package/dist/plugins/postgres/index.mjs.map +0 -1
- package/dist/plugins/postgres/routable.mjs +0 -33
- package/dist/plugins/postgres/routable.mjs.map +0 -1
- package/dist/plugins/postgres/service.d.mts +0 -50
- package/dist/plugins/postgres/service.mjs +0 -179
- package/dist/plugins/postgres/service.mjs.map +0 -1
- package/dist/plugins/postgres/snapshot.mjs +0 -32
- package/dist/plugins/postgres/snapshot.mjs.map +0 -1
- package/dist/plugins/postgres/spans.mjs +0 -11
- package/dist/plugins/postgres/spans.mjs.map +0 -1
- package/dist/plugins/seal/spans.mjs +0 -18
- package/dist/plugins/seal/spans.mjs.map +0 -1
- package/dist/plugins/sui/spans.mjs +0 -17
- package/dist/plugins/sui/spans.mjs.map +0 -1
- package/dist/plugins/wallet/spans.mjs +0 -22
- package/dist/plugins/wallet/spans.mjs.map +0 -1
- package/dist/plugins/walrus/registry-publish.d.mts +0 -24
- package/dist/plugins/walrus/spans.mjs +0 -18
- package/dist/plugins/walrus/spans.mjs.map +0 -1
- package/dist/runtime/docker/logs.d.mts +0 -1
- package/dist/runtime/docker/logs.mjs +0 -34
- package/dist/runtime/docker/logs.mjs.map +0 -1
- package/dist/substrate/runtime/artifact-publisher/index.mjs +0 -86
- package/dist/substrate/runtime/artifact-publisher/index.mjs.map +0 -1
- package/dist/substrate/runtime/capability-sinks/index.d.mts +0 -1
- package/dist/substrate/runtime/capability-sinks/index.mjs +0 -3
- package/dist/substrate/runtime/capability-sinks/layer.d.mts +0 -1
- package/dist/substrate/runtime/capability-sinks/layer.mjs +0 -31
- package/dist/substrate/runtime/capability-sinks/layer.mjs.map +0 -1
- package/dist/substrate/runtime/capability-sinks/service.d.mts +0 -95
- package/dist/substrate/runtime/capability-sinks/service.mjs +0 -69
- package/dist/substrate/runtime/capability-sinks/service.mjs.map +0 -1
- package/dist/substrate/runtime/cross-process/lock.d.mts +0 -1
- package/dist/substrate/runtime/cross-process/lock.mjs +0 -23
- package/dist/substrate/runtime/cross-process/lock.mjs.map +0 -1
- package/dist/substrate/runtime/cross-process/snapshot-reservation.mjs +0 -113
- package/dist/substrate/runtime/cross-process/snapshot-reservation.mjs.map +0 -1
- package/dist/substrate/runtime/lifecycle/lifecycle-fact.mjs +0 -31
- package/dist/substrate/runtime/lifecycle/lifecycle-fact.mjs.map +0 -1
- package/dist/substrate/runtime/observability/formatter-registry.d.mts +0 -1
- package/dist/substrate/runtime/observability/formatter-registry.mjs +0 -48
- package/dist/substrate/runtime/observability/formatter-registry.mjs.map +0 -1
- package/dist/substrate/runtime/observability/span-store.d.mts +0 -1
- package/dist/substrate/runtime/observability/span-store.mjs +0 -110
- package/dist/substrate/runtime/observability/span-store.mjs.map +0 -1
- package/dist/substrate/runtime/observability/spans.d.mts +0 -1
- package/dist/substrate/runtime/observability/spans.mjs +0 -87
- package/dist/substrate/runtime/observability/spans.mjs.map +0 -1
- package/dist/substrate/runtime/projection/index.mjs +0 -4
- package/dist/substrate/runtime/projection/persisted.mjs +0 -213
- package/dist/substrate/runtime/projection/persisted.mjs.map +0 -1
- package/dist/substrate/runtime/scoped-multimap/service.mjs +0 -52
- package/dist/substrate/runtime/scoped-multimap/service.mjs.map +0 -1
- package/dist/substrate/runtime/scoped-ref-map/index.mjs +0 -2
- package/dist/substrate/runtime/scoped-ref-map/service.mjs +0 -83
- package/dist/substrate/runtime/scoped-ref-map/service.mjs.map +0 -1
- package/dist/substrate/runtime/state-store/index.mjs +0 -3
- package/dist/substrate/runtime/state-store/schema.d.mts +0 -1
- package/dist/substrate/runtime/state-store/schema.mjs +0 -41
- package/dist/substrate/runtime/state-store/schema.mjs.map +0 -1
- package/dist/substrate/runtime/state-store/service.d.mts +0 -1
- package/dist/substrate/runtime/state-store/service.mjs +0 -145
- package/dist/substrate/runtime/state-store/service.mjs.map +0 -1
- package/dist/substrate/runtime/sui-execute/index.mjs.map +0 -1
- package/dist/substrate/runtime/sui-execute/sign-and-dispatch.mjs.map +0 -1
- package/dist/substrate/runtime/sui-ledger/object-ref.mjs.map +0 -1
- package/dist/substrate/runtime/sui-move-build/index.mjs.map +0 -1
- package/dist/substrate/runtime/supervisor/dispatch-contributions.mjs +0 -151
- package/dist/substrate/runtime/supervisor/dispatch-contributions.mjs.map +0 -1
- package/dist/substrate/state-store.d.mts +0 -1
- /package/dist/orchestrators/{snapshot/pending-marker.d.mts → codegen/id-config.d.mts} +0 -0
- /package/dist/orchestrators/snapshot/{recover-pending.d.mts → interrupted-restore.d.mts} +0 -0
- /package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/sign-and-dispatch.d.mts +0 -0
- /package/dist/plugins/{postgres/snapshot.d.mts → sui/ledger/object-ref.d.mts} +0 -0
- /package/dist/{substrate/runtime/sui-move-build → plugins/sui/move}/index.d.mts +0 -0
- /package/dist/substrate/runtime/{state-store/index.d.mts → reconcile/graph.d.mts} +0 -0
- /package/dist/substrate/runtime/{cross-process/snapshot-reservation.d.mts → supervisor/contribution-dispatcher.d.mts} +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/orchestrators/network-options.ts
|
|
2
|
+
/**
|
|
3
|
+
* The default per-network policy: dev conveniences are ON for every
|
|
4
|
+
* network EXCEPT live `mainnet`. Fork networks (`mainnet-fork`, …) are
|
|
5
|
+
* local dev stacks, so they stay ON — only the real `mainnet` name opts
|
|
6
|
+
* out.
|
|
7
|
+
*/
|
|
8
|
+
const defaultNetworkOptions = (network) => {
|
|
9
|
+
const on = network !== "mainnet";
|
|
10
|
+
return {
|
|
11
|
+
devWallet: on,
|
|
12
|
+
faucet: on,
|
|
13
|
+
autoApproveSigning: on
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
const asBool = (v) => typeof v === "boolean" ? v : void 0;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the effective options for the active network: the default
|
|
19
|
+
* policy ({@link defaultNetworkOptions}) with the author's per-network
|
|
20
|
+
* overrides merged on top, field by field (an unset/non-boolean override
|
|
21
|
+
* field keeps the policy default). `overrides` is the opaque
|
|
22
|
+
* substrate-forwarded record — fields are read defensively.
|
|
23
|
+
*/
|
|
24
|
+
const resolveNetworkOptions = (network, overrides) => {
|
|
25
|
+
const base = defaultNetworkOptions(network);
|
|
26
|
+
const raw = overrides?.[network];
|
|
27
|
+
if (raw === null || typeof raw !== "object") return base;
|
|
28
|
+
const o = raw;
|
|
29
|
+
if (network === "mainnet") return {
|
|
30
|
+
devWallet: false,
|
|
31
|
+
faucet: false,
|
|
32
|
+
autoApproveSigning: false
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
devWallet: asBool(o["devWallet"]) ?? base.devWallet,
|
|
36
|
+
faucet: asBool(o["faucet"]) ?? base.faucet,
|
|
37
|
+
autoApproveSigning: asBool(o["autoApproveSigning"]) ?? base.autoApproveSigning
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { resolveNetworkOptions };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=network-options.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network-options.mjs","names":[],"sources":["../../src/orchestrators/network-options.ts"],"sourcesContent":["// Per-network options: the plugin-aware interpretation of the opaque\n// `DevstackOptions.networkOptions` the name-blind substrate forwards\n// verbatim. This module lives OUTSIDE substrate precisely because it\n// names dev conveniences (dev wallet, faucet, signing) — substrate stays\n// name-blind; the orchestrator reads the policy.\n\n/**\n * Per-network dev-convenience toggles. All optional — an unset field\n * defers to the default policy (on for every network EXCEPT live\n * `mainnet`). A CONSISTENT mechanism: declare these once per network in\n * the config instead of scattering one-off flags.\n *\n * - `devWallet` — per-network override is FORWARDED. Gates the\n * dev-wallet `generated-extras` flush at boot (`orchestrators/boot.ts`,\n * which resolves these options against the substrate-forwarded\n * `networkOptions` record), which the Vite plugin injects.\n * - `autoApproveSigning` — per-network override is FORWARDED. Gates the\n * dev-wallet auto-approve policy the Vite plugin emits into the\n * injected dev-wallet (`build-integrations/vite/index.ts`) — on\n * `mainnet` signing is never silently auto-approved.\n * - `faucet` — per-network override is NOT forwarded to the sui plugin\n * (see the field doc below). The plugin honours only the policy\n * default, which already carries the load-bearing `mainnet`\n * hard-clamp, so a non-faucet `mainnet` never registers\n * `faucet:request:<chainId>`.\n */\nexport interface NetworkScopedOptions {\n\t/** ENFORCED. Mount the test-only dev wallet and flush its\n\t * `generated-extras` tree (`dev-wallet.ts` + `accounts.ts`) at boot so\n\t * the Vite plugin's `@devstack-dev` injection has files to load. Off →\n\t * no flush, and the Vite `load` hook gracefully no-ops. */\n\treadonly devWallet?: boolean;\n\t/** Funding-faucet strategy gate (`faucet:request:<chainId>`) in the sui\n\t * plugin. The plugin follows the POLICY DEFAULT only — on for every\n\t * non-`mainnet` network, hard-clamped off on live `mainnet` (so a\n\t * non-faucet network never registers the strategy and account funding\n\t * surfaces the actionable \"no faucet strategy\" error instead of\n\t * faucet-funding). A per-network `faucet` OVERRIDE here is NOT currently\n\t * forwarded to the plugin (unlike `devWallet` / `autoApproveSigning`,\n\t * which boot resolves against the substrate-forwarded `networkOptions`):\n\t * the name-blind substrate does not thread `networkOptions` into plugins,\n\t * and the sui plugin only receives `IdentityContext`. The sui mode still\n\t * decides HOW a faucet is provisioned (local container / fork whale /\n\t * live endpoint); the policy default decides WHETHER the resolved\n\t * strategy is exposed. */\n\treadonly faucet?: boolean;\n\t/** ENFORCED. Default the injected dev-wallet's auto-approve policy for\n\t * this network (`build-integrations/vite/index.ts`). On → dev-wallet\n\t * signing requests auto-approve (headless Playwright / in-app \"Open as\"\n\t * ergonomics) unless an explicit `autoApprove` / `DEVSTACK_AUTO_APPROVE`\n\t * overrides. Hard-clamped off on live `mainnet`, so a real-funds\n\t * signature is never granted without a human in the loop. */\n\treadonly autoApproveSigning?: boolean;\n}\n\n/** Every field resolved to a concrete boolean (no `undefined`). */\nexport type ResolvedNetworkOptions = Required<NetworkScopedOptions>;\n\n/**\n * The default per-network policy: dev conveniences are ON for every\n * network EXCEPT live `mainnet`. Fork networks (`mainnet-fork`, …) are\n * local dev stacks, so they stay ON — only the real `mainnet` name opts\n * out.\n */\nexport const defaultNetworkOptions = (network: string): ResolvedNetworkOptions => {\n\tconst on = network !== 'mainnet';\n\treturn { devWallet: on, faucet: on, autoApproveSigning: on };\n};\n\nconst asBool = (v: unknown): boolean | undefined => (typeof v === 'boolean' ? v : undefined);\n\n/**\n * Resolve the effective options for the active network: the default\n * policy ({@link defaultNetworkOptions}) with the author's per-network\n * overrides merged on top, field by field (an unset/non-boolean override\n * field keeps the policy default). `overrides` is the opaque\n * substrate-forwarded record — fields are read defensively.\n */\nexport const resolveNetworkOptions = (\n\tnetwork: string,\n\toverrides?: Readonly<Record<string, unknown>>,\n): ResolvedNetworkOptions => {\n\tconst base = defaultNetworkOptions(network);\n\tconst raw = overrides?.[network];\n\tif (raw === null || typeof raw !== 'object') return base;\n\tconst o = raw as Record<string, unknown>;\n\t// HARD-CLAMP: every dev convenience is forced OFF on real `mainnet`,\n\t// regardless of an explicit `{ mainnet: { … : true } }` override. The\n\t// default policy is already off for `mainnet`; the clamp ALSO blocks a\n\t// silent explicit opt-in. Each clamp guards a distinct production-safety\n\t// failure mode:\n\t// - `devWallet` — flushing the secret `generated-extras` tree and\n\t// injecting a test-only signer into a production build.\n\t// - `faucet` — exposing a funding-faucet strategy against a real\n\t// network (there is no mainnet faucet to begin with).\n\t// - `autoApproveSigning` — auto-approving a real-funds signature with\n\t// no human in the loop.\n\tif (network === 'mainnet') {\n\t\treturn { devWallet: false, faucet: false, autoApproveSigning: false };\n\t}\n\treturn {\n\t\tdevWallet: asBool(o['devWallet']) ?? base.devWallet,\n\t\tfaucet: asBool(o['faucet']) ?? base.faucet,\n\t\tautoApproveSigning: asBool(o['autoApproveSigning']) ?? base.autoApproveSigning,\n\t};\n};\n"],"mappings":";;;;;;;AAgEA,MAAa,yBAAyB,YAA4C;CACjF,MAAM,KAAK,YAAY;CACvB,OAAO;EAAE,WAAW;EAAI,QAAQ;EAAI,oBAAoB;CAAG;AAC5D;AAEA,MAAM,UAAU,MAAqC,OAAO,MAAM,YAAY,IAAI,KAAA;;;;;;;;AASlF,MAAa,yBACZ,SACA,cAC4B;CAC5B,MAAM,OAAO,sBAAsB,OAAO;CAC1C,MAAM,MAAM,YAAY;CACxB,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU,OAAO;CACpD,MAAM,IAAI;CAYV,IAAI,YAAY,WACf,OAAO;EAAE,WAAW;EAAO,QAAQ;EAAO,oBAAoB;CAAM;CAErE,OAAO;EACN,WAAW,OAAO,EAAE,YAAY,KAAK,KAAK;EAC1C,QAAQ,OAAO,EAAE,SAAS,KAAK,KAAK;EACpC,oBAAoB,OAAO,EAAE,qBAAqB,KAAK,KAAK;CAC7D;AACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanup.mjs","names":[],"sources":["../../../src/orchestrators/router/cleanup.ts"],"sourcesContent":["import { join } from 'node:path';\n\nimport { Effect, FileSystem } from 'effect';\n\nimport { dispatchFileIdFromFilename, parseDispatchRouteFile } from './file-provider.ts';\nimport { ROUTER_CONTAINER_NAME_PREFIX } from './sentinels.ts';\n\nexport { ROUTER_CONTAINER_NAME_PREFIX, ROUTER_SHARED_APP } from './sentinels.ts';\n\nconst routerStateRoot = (runtimeRoot: string): string => join(runtimeRoot, 'router');\n\nconst safeDirectories = (\n\tfs: FileSystem.FileSystem,\n\tdir: string,\n): Effect.Effect<ReadonlyArray<string>> =>\n\tEffect.gen(function* () {\n\t\tconst entries = yield* fs\n\t\t\t.readDirectory(dir)\n\t\t\t.pipe(Effect.catch(() => Effect.succeed<ReadonlyArray<string>>([])));\n\t\tconst directories: Array<string> = [];\n\t\tfor (const entry of entries) {\n\t\t\tconst stat = yield* fs.stat(join(dir, entry)).pipe(Effect.catch(() => Effect.succeed(null)));\n\t\t\tif (stat !== null && stat.type === 'Directory') {\n\t\t\t\tdirectories.push(entry);\n\t\t\t}\n\t\t}\n\t\treturn directories;\n\t});\n\nexport const removeRouterDispatchFilesForStack = (inputs: {\n\treadonly runtimeRoot: string;\n\treadonly app: string;\n\treadonly stack: string;\n}): Effect.Effect<number, never, FileSystem.FileSystem> =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tlet removed = 0;\n\t\tfor (const profileDir of yield* safeDirectories(fs, routerStateRoot(inputs.runtimeRoot))) {\n\t\t\tconst dispatchDir = join(routerStateRoot(inputs.runtimeRoot), profileDir, 'dispatch');\n\t\t\tconst exists = yield* fs.exists(dispatchDir).pipe(Effect.catch(() => Effect.succeed(false)));\n\t\t\tif (!exists) continue;\n\t\t\tconst filenames = yield* fs\n\t\t\t\t.readDirectory(dispatchDir)\n\t\t\t\t.pipe(Effect.catch(() => Effect.succeed<ReadonlyArray<string>>([])));\n\t\t\tfor (const filename of filenames) {\n\t\t\t\tconst dispatchFileId = dispatchFileIdFromFilename(filename);\n\t\t\t\tif (dispatchFileId === null) continue;\n\t\t\t\tconst file = join(dispatchDir, filename);\n\t\t\t\tconst body = yield* fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed(null)));\n\t\t\t\tif (body === null) continue;\n\t\t\t\tconst parsed = parseDispatchRouteFile(body, dispatchFileId);\n\t\t\t\tif (\n\t\t\t\t\tparsed._tag === 'valid' &&\n\t\t\t\t\tparsed.route.lease?.app === inputs.app &&\n\t\t\t\t\tparsed.route.lease.stack === inputs.stack\n\t\t\t\t) {\n\t\t\t\t\tyield* fs.remove(file, { force: true }).pipe(Effect.ignore);\n\t\t\t\t\tremoved += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn removed;\n\t});\n\nexport const removeRouterProfileStateForDockerStack = (inputs: {\n\treadonly runtimeRoot: string;\n\treadonly routerStack: string;\n}): Effect.Effect<number, never, FileSystem.FileSystem> =>\n\tEffect.gen(function* () {\n\t\tif (!inputs.routerStack.startsWith(ROUTER_CONTAINER_NAME_PREFIX)) return 0;\n\t\tconst fingerprint = inputs.routerStack.slice(ROUTER_CONTAINER_NAME_PREFIX.length);\n\t\tif (fingerprint.length === 0) return 0;\n\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tlet removed = 0;\n\t\tfor (const profileDir of yield* safeDirectories(fs, routerStateRoot(inputs.runtimeRoot))) {\n\t\t\tif (!profileDir.endsWith(`-${fingerprint}`)) continue;\n\t\t\tyield* fs\n\t\t\t\t.remove(join(routerStateRoot(inputs.runtimeRoot), profileDir), {\n\t\t\t\t\trecursive: true,\n\t\t\t\t\tforce: true,\n\t\t\t\t})\n\t\t\t\t.pipe(Effect.ignore);\n\t\t\tremoved += 1;\n\t\t}\n\t\treturn removed;\n\t});\n"],"mappings":";;;;;AASA,MAAM,mBAAmB,gBAAgC,KAAK,aAAa,
|
|
1
|
+
{"version":3,"file":"cleanup.mjs","names":[],"sources":["../../../src/orchestrators/router/cleanup.ts"],"sourcesContent":["import { join } from 'node:path';\n\nimport { Effect, FileSystem } from 'effect';\n\nimport { dispatchFileIdFromFilename, parseDispatchRouteFile } from './file-provider.ts';\nimport { ROUTER_CONTAINER_NAME_PREFIX } from './sentinels.ts';\n\nexport { ROUTER_CONTAINER_NAME_PREFIX, ROUTER_SHARED_APP } from './sentinels.ts';\n\nconst routerStateRoot = (runtimeRoot: string): string => join(runtimeRoot, 'router');\n\nconst safeDirectories = (\n\tfs: FileSystem.FileSystem,\n\tdir: string,\n): Effect.Effect<ReadonlyArray<string>> =>\n\tEffect.gen(function* () {\n\t\tconst entries = yield* fs\n\t\t\t.readDirectory(dir)\n\t\t\t.pipe(Effect.catch(() => Effect.succeed<ReadonlyArray<string>>([])));\n\t\tconst directories: Array<string> = [];\n\t\tfor (const entry of entries) {\n\t\t\tconst stat = yield* fs.stat(join(dir, entry)).pipe(Effect.catch(() => Effect.succeed(null)));\n\t\t\tif (stat !== null && stat.type === 'Directory') {\n\t\t\t\tdirectories.push(entry);\n\t\t\t}\n\t\t}\n\t\treturn directories;\n\t});\n\nexport const removeRouterDispatchFilesForStack = (inputs: {\n\treadonly runtimeRoot: string;\n\treadonly app: string;\n\treadonly stack: string;\n}): Effect.Effect<number, never, FileSystem.FileSystem> =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tlet removed = 0;\n\t\tfor (const profileDir of yield* safeDirectories(fs, routerStateRoot(inputs.runtimeRoot))) {\n\t\t\tconst dispatchDir = join(routerStateRoot(inputs.runtimeRoot), profileDir, 'dispatch');\n\t\t\tconst exists = yield* fs.exists(dispatchDir).pipe(Effect.catch(() => Effect.succeed(false)));\n\t\t\tif (!exists) continue;\n\t\t\tconst filenames = yield* fs\n\t\t\t\t.readDirectory(dispatchDir)\n\t\t\t\t.pipe(Effect.catch(() => Effect.succeed<ReadonlyArray<string>>([])));\n\t\t\tfor (const filename of filenames) {\n\t\t\t\tconst dispatchFileId = dispatchFileIdFromFilename(filename);\n\t\t\t\tif (dispatchFileId === null) continue;\n\t\t\t\tconst file = join(dispatchDir, filename);\n\t\t\t\tconst body = yield* fs.readFileString(file).pipe(Effect.catch(() => Effect.succeed(null)));\n\t\t\t\tif (body === null) continue;\n\t\t\t\tconst parsed = parseDispatchRouteFile(body, dispatchFileId);\n\t\t\t\tif (\n\t\t\t\t\tparsed._tag === 'valid' &&\n\t\t\t\t\tparsed.route.lease?.app === inputs.app &&\n\t\t\t\t\tparsed.route.lease.stack === inputs.stack\n\t\t\t\t) {\n\t\t\t\t\tyield* fs.remove(file, { force: true }).pipe(Effect.ignore);\n\t\t\t\t\tremoved += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn removed;\n\t});\n\nexport const removeRouterProfileStateForDockerStack = (inputs: {\n\treadonly runtimeRoot: string;\n\treadonly routerStack: string;\n}): Effect.Effect<number, never, FileSystem.FileSystem> =>\n\tEffect.gen(function* () {\n\t\tif (!inputs.routerStack.startsWith(ROUTER_CONTAINER_NAME_PREFIX)) return 0;\n\t\tconst fingerprint = inputs.routerStack.slice(ROUTER_CONTAINER_NAME_PREFIX.length);\n\t\tif (fingerprint.length === 0) return 0;\n\n\t\tconst fs = yield* FileSystem.FileSystem;\n\t\tlet removed = 0;\n\t\tfor (const profileDir of yield* safeDirectories(fs, routerStateRoot(inputs.runtimeRoot))) {\n\t\t\tif (!profileDir.endsWith(`-${fingerprint}`)) continue;\n\t\t\tyield* fs\n\t\t\t\t.remove(join(routerStateRoot(inputs.runtimeRoot), profileDir), {\n\t\t\t\t\trecursive: true,\n\t\t\t\t\tforce: true,\n\t\t\t\t})\n\t\t\t\t.pipe(Effect.ignore);\n\t\t\tremoved += 1;\n\t\t}\n\t\treturn removed;\n\t});\n"],"mappings":";;;;;AASA,MAAM,mBAAmB,gBAAgC,KAAK,aAAa,QAAQ;AAEnF,MAAM,mBACL,IACA,QAEA,OAAO,IAAI,aAAa;CACvB,MAAM,UAAU,OAAO,GACrB,cAAc,GAAG,CAAC,CAClB,KAAK,OAAO,YAAY,OAAO,QAA+B,CAAC,CAAC,CAAC,CAAC;CACpE,MAAM,cAA6B,CAAC;CACpC,KAAK,MAAM,SAAS,SAAS;EAC5B,MAAM,OAAO,OAAO,GAAG,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,YAAY,OAAO,QAAQ,IAAI,CAAC,CAAC;EAC3F,IAAI,SAAS,QAAQ,KAAK,SAAS,aAClC,YAAY,KAAK,KAAK;CAExB;CACA,OAAO;AACR,CAAC;AAEF,MAAa,qCAAqC,WAKjD,OAAO,IAAI,aAAa;CACvB,MAAM,KAAK,OAAO,WAAW;CAC7B,IAAI,UAAU;CACd,KAAK,MAAM,cAAc,OAAO,gBAAgB,IAAI,gBAAgB,OAAO,WAAW,CAAC,GAAG;EACzF,MAAM,cAAc,KAAK,gBAAgB,OAAO,WAAW,GAAG,YAAY,UAAU;EAEpF,IAAI,EAAC,OADiB,GAAG,OAAO,WAAW,CAAC,CAAC,KAAK,OAAO,YAAY,OAAO,QAAQ,KAAK,CAAC,CAAC,IAC9E;EACb,MAAM,YAAY,OAAO,GACvB,cAAc,WAAW,CAAC,CAC1B,KAAK,OAAO,YAAY,OAAO,QAA+B,CAAC,CAAC,CAAC,CAAC;EACpE,KAAK,MAAM,YAAY,WAAW;GACjC,MAAM,iBAAiB,2BAA2B,QAAQ;GAC1D,IAAI,mBAAmB,MAAM;GAC7B,MAAM,OAAO,KAAK,aAAa,QAAQ;GACvC,MAAM,OAAO,OAAO,GAAG,eAAe,IAAI,CAAC,CAAC,KAAK,OAAO,YAAY,OAAO,QAAQ,IAAI,CAAC,CAAC;GACzF,IAAI,SAAS,MAAM;GACnB,MAAM,SAAS,uBAAuB,MAAM,cAAc;GAC1D,IACC,OAAO,SAAS,WAChB,OAAO,MAAM,OAAO,QAAQ,OAAO,OACnC,OAAO,MAAM,MAAM,UAAU,OAAO,OACnC;IACD,OAAO,GAAG,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM;IAC1D,WAAW;GACZ;EACD;CACD;CACA,OAAO;AACR,CAAC;AAEF,MAAa,0CAA0C,WAItD,OAAO,IAAI,aAAa;CACvB,IAAI,CAAC,OAAO,YAAY,WAAA,kBAAuC,GAAG,OAAO;CACzE,MAAM,cAAc,OAAO,YAAY,MAAM,6BAA6B,MAAM;CAChF,IAAI,YAAY,WAAW,GAAG,OAAO;CAErC,MAAM,KAAK,OAAO,WAAW;CAC7B,IAAI,UAAU;CACd,KAAK,MAAM,cAAc,OAAO,gBAAgB,IAAI,gBAAgB,OAAO,WAAW,CAAC,GAAG;EACzF,IAAI,CAAC,WAAW,SAAS,IAAI,aAAa,GAAG;EAC7C,OAAO,GACL,OAAO,KAAK,gBAAgB,OAAO,WAAW,GAAG,UAAU,GAAG;GAC9D,WAAW;GACX,OAAO;EACR,CAAC,CAAC,CACD,KAAK,OAAO,MAAM;EACpB,WAAW;CACZ;CACA,OAAO;AACR,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cors.mjs","names":[],"sources":["../../../src/orchestrators/router/cors.ts"],"sourcesContent":["// CORS middleware factory.\n//\n// Architecture distilled-doc §\"Shared CORS middleware\":\n// - ONE shared permissive CORS middleware. Backends opt in by setting\n// `Routable.cors: true`; the orchestrator wires the middleware\n// reference into the dispatch file. Backends never write CORS\n// headers themselves.\n//\n// - The middleware file is rewritten on each router boot, BEFORE any\n// per-backend file references it. It sorts lexicographically\n// ahead of per-backend filenames (we prefix with `00-`) so the\n// file-provider watcher loads it first.\n//\n// - Permissive (`*` allow-origin, very long max-age) — fine for the\n// dev-loop, footgun if copied elsewhere; documented at the call\n// site, not in production-ready posture.\n\n// The Traefik middleware name baked into the file-provider config. We\n// use a constant so the same string flows through `file-provider.ts`'s\n// route renderer.\nexport const CORS_MIDDLEWARE_NAME = 'devstack-cors';\n\n/** Filename for the shared-middlewares file. The `00-` prefix forces\n * alphabetic-sort ordering ahead of per-backend dispatch files. */\nexport const CORS_MIDDLEWARE_FILENAME = '00-shared-middlewares.yml';\n\n/** Render the permissive shared-CORS middleware as a Traefik\n * file-provider YAML body. Backends that set `cors: true` reference\n * this middleware by name (`devstack-cors`) from their per-backend\n * routers section. */\nexport const renderCorsMiddlewareYaml = (): string => {\n\t// The YAML body below is hand-rolled (no yaml lib dep) because:\n\t// - it is a static, fully-controlled string,\n\t// - the values are constants known at compile time,\n\t// - we explicitly want byte-identical output cycle-to-cycle so\n\t// the file-provider watcher doesn't wake on no-op rewrites.\n\treturn [\n\t\t`# Devstack shared CORS middleware. Permissive — dev-only.`,\n\t\t`# Architecture distilled-doc §\"Shared CORS middleware\".`,\n\t\t`http:`,\n\t\t` middlewares:`,\n\t\t` ${CORS_MIDDLEWARE_NAME}:`,\n\t\t` headers:`,\n\t\t` accessControlAllowOriginList: [\"*\"]`,\n\t\t` accessControlAllowMethods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\", \"PATCH\", \"HEAD\"]`,\n\t\t` accessControlAllowHeaders: [\"*\"]`,\n\t\t` accessControlExposeHeaders: [\"*\"]`,\n\t\t` accessControlAllowCredentials: true`,\n\t\t` accessControlMaxAge: 86400`,\n\t\t` addVaryHeader: true`,\n\t\t``, // trailing newline; Traefik tolerates either way\n\t].join('\\n');\n};\n"],"mappings":";AAoBA,MAAa,uBAAuB;;;AAIpC,MAAa,2BAA2B;;;;;AAMxC,MAAa,iCAAyC;
|
|
1
|
+
{"version":3,"file":"cors.mjs","names":[],"sources":["../../../src/orchestrators/router/cors.ts"],"sourcesContent":["// CORS middleware factory.\n//\n// Architecture distilled-doc §\"Shared CORS middleware\":\n// - ONE shared permissive CORS middleware. Backends opt in by setting\n// `Routable.cors: true`; the orchestrator wires the middleware\n// reference into the dispatch file. Backends never write CORS\n// headers themselves.\n//\n// - The middleware file is rewritten on each router boot, BEFORE any\n// per-backend file references it. It sorts lexicographically\n// ahead of per-backend filenames (we prefix with `00-`) so the\n// file-provider watcher loads it first.\n//\n// - Permissive (`*` allow-origin, very long max-age) — fine for the\n// dev-loop, footgun if copied elsewhere; documented at the call\n// site, not in production-ready posture.\n\n// The Traefik middleware name baked into the file-provider config. We\n// use a constant so the same string flows through `file-provider.ts`'s\n// route renderer.\nexport const CORS_MIDDLEWARE_NAME = 'devstack-cors';\n\n/** Filename for the shared-middlewares file. The `00-` prefix forces\n * alphabetic-sort ordering ahead of per-backend dispatch files. */\nexport const CORS_MIDDLEWARE_FILENAME = '00-shared-middlewares.yml';\n\n/** Render the permissive shared-CORS middleware as a Traefik\n * file-provider YAML body. Backends that set `cors: true` reference\n * this middleware by name (`devstack-cors`) from their per-backend\n * routers section. */\nexport const renderCorsMiddlewareYaml = (): string => {\n\t// The YAML body below is hand-rolled (no yaml lib dep) because:\n\t// - it is a static, fully-controlled string,\n\t// - the values are constants known at compile time,\n\t// - we explicitly want byte-identical output cycle-to-cycle so\n\t// the file-provider watcher doesn't wake on no-op rewrites.\n\treturn [\n\t\t`# Devstack shared CORS middleware. Permissive — dev-only.`,\n\t\t`# Architecture distilled-doc §\"Shared CORS middleware\".`,\n\t\t`http:`,\n\t\t` middlewares:`,\n\t\t` ${CORS_MIDDLEWARE_NAME}:`,\n\t\t` headers:`,\n\t\t` accessControlAllowOriginList: [\"*\"]`,\n\t\t` accessControlAllowMethods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\", \"PATCH\", \"HEAD\"]`,\n\t\t` accessControlAllowHeaders: [\"*\"]`,\n\t\t` accessControlExposeHeaders: [\"*\"]`,\n\t\t` accessControlAllowCredentials: true`,\n\t\t` accessControlMaxAge: 86400`,\n\t\t` addVaryHeader: true`,\n\t\t``, // trailing newline; Traefik tolerates either way\n\t].join('\\n');\n};\n"],"mappings":";AAoBA,MAAa,uBAAuB;;;AAIpC,MAAa,2BAA2B;;;;;AAMxC,MAAa,iCAAyC;CAMrD,OAAO;EACN;EACA;EACA;EACA;EACA,OAAO,qBAAqB;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACD,CAAC,CAAC,KAAK,IAAI;AACZ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entrypoints.mjs","names":[],"sources":["../../../src/orchestrators/router/entrypoints.ts"],"sourcesContent":["// Entrypoint registry — a process-global allowlist of named host\n// ports the Traefik container will listen on.\n//\n// Architecture invariants:\n// #6 — Routables select an entrypoint by *name* from a closed set\n// populated at module-load time. The set is read ONCE at\n// router launch.\n// #12 — Every entrypoint is plain HTTP / h2c / TCP; no TLS surface.\n// TCP entrypoints bind a host port and dispatch raw bytes to a\n// single backend (postgres, redis, …); they cannot share the\n// port across parallel stacks because TCP has no virtual-host\n// concept. HTTP/h2c entrypoints DO share ports via Host-header\n// dispatch.\n//\n// The registry is intentionally a typed Context service (Effect v4\n// idiom) rather than a module-mutable map: that lets tests build a\n// fresh registry per scenario, lets the orchestrator carry the\n// registration set forward immutably, and removes the\n// `ATTACHED_FOLLOWERS`-style hidden globals the architecture's\n// \"What's NOT in it\" section forbids.\n//\n// Construction surface:\n// `makeEntrypointRegistry(...)` — pure constructor; throws synchronously\n// on conflict registration (architecture invariant #6 is \"throw at\n// registration time\", not at acquire time).\n//\n// Lookup surface:\n// `EntrypointRegistry.byName(name)` — typed yieldable that fails\n// `UnknownEntrypoint` if the caller spliced an unregistered name.\n\nimport { Context, Effect, Layer } from 'effect';\n\nimport type { EntrypointDecl } from '../../contracts/routable.ts';\nimport { EntrypointConflict, UnknownEntrypoint } from './errors.ts';\n\n/** A named entrypoint — a host port the Traefik container binds and\n * listens on. `http`/`h2c` entrypoints support Host-header dispatch\n * (one port → many backends). `tcp` entrypoints are point-to-point\n * (one port → one backend) — siblings dial them directly. */\nexport type Entrypoint = EntrypointDecl;\n\n/** Registry shape. Closed map from declared name → canonical listener.\n * HTTP-family declarations may share a port; Traefik still needs one\n * listener name for that port, so aliases resolve to the first\n * registered listener. */\nexport interface EntrypointRegistryShape {\n\treadonly byName: (name: string) => Effect.Effect<Entrypoint, UnknownEntrypoint>;\n\treadonly all: () => ReadonlyArray<Entrypoint>;\n}\n\nexport class EntrypointRegistry extends Context.Service<\n\tEntrypointRegistry,\n\tEntrypointRegistryShape\n>()('@devstack/orchestrators/router/EntrypointRegistry') {}\n\nconst entrypointFamily = (protocol: Entrypoint['protocol']): 'http' | 'tcp' =>\n\tprotocol === 'tcp' ? 'tcp' : 'http';\n\n/** Build a registry from a literal seed. Idempotent on identical\n * `(name, port, protocol)` triples; throws synchronously on conflict\n * per architecture invariant #6. The reason this is synchronous\n * rather than yielded: registration is module-load wiring, not a\n * runtime effect — the failure mode is \"two callers wired the same\n * name to different ports\", which is a build-time bug.\n *\n * Traefik cannot bind multiple entrypoints to the same container port.\n * For HTTP-family aliases on one port, keep every declared name\n * lookupable but resolve those aliases to the first listener registered\n * for the port. */\nexport const makeEntrypointRegistry = (\n\tseed: ReadonlyArray<Entrypoint>,\n): EntrypointRegistryShape => {\n\tconst declaredByName = new Map<string, Entrypoint>();\n\tconst lookupByName = new Map<string, Entrypoint>();\n\tconst listenerByPort = new Map<number, Entrypoint>();\n\tfor (const e of seed) {\n\t\tconst existing = declaredByName.get(e.name);\n\t\tif (existing) {\n\t\t\tif (existing.port === e.port && existing.protocol === e.protocol) continue;\n\t\t\tthrow new EntrypointConflict({\n\t\t\t\tname: e.name,\n\t\t\t\texisting: { port: existing.port, protocol: existing.protocol },\n\t\t\t\tattempted: { port: e.port, protocol: e.protocol },\n\t\t\t});\n\t\t}\n\t\tdeclaredByName.set(e.name, e);\n\n\t\tconst listener = listenerByPort.get(e.port);\n\t\tif (listener) {\n\t\t\tconst listenerFamily = entrypointFamily(listener.protocol);\n\t\t\tconst attemptedFamily = entrypointFamily(e.protocol);\n\t\t\tif (listenerFamily === 'tcp' || listenerFamily !== attemptedFamily) {\n\t\t\t\tthrow new EntrypointConflict({\n\t\t\t\t\tname: e.name,\n\t\t\t\t\texisting: { port: listener.port, protocol: listener.protocol },\n\t\t\t\t\tattempted: { port: e.port, protocol: e.protocol },\n\t\t\t\t});\n\t\t\t}\n\t\t\tlookupByName.set(e.name, listener);\n\t\t\tcontinue;\n\t\t}\n\n\t\tlistenerByPort.set(e.port, e);\n\t\tlookupByName.set(e.name, e);\n\t}\n\tconst frozenListeners: ReadonlyArray<Entrypoint> = Array.from(listenerByPort.values());\n\tconst knownNames = Array.from(declaredByName.keys());\n\treturn {\n\t\tbyName: (name) => {\n\t\t\tconst hit = lookupByName.get(name);\n\t\t\tif (hit) return Effect.succeed(hit);\n\t\t\treturn Effect.fail(\n\t\t\t\tnew UnknownEntrypoint({\n\t\t\t\t\tname,\n\t\t\t\t\tknown: knownNames,\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t\tall: () => frozenListeners,\n\t};\n};\n\n/** Layer that pins the registry from a literal seed. The orchestrator\n * takes this as a substrate dependency; plugins do NOT mutate the\n * registry — they pick names from it via their Routable contributions. */\nexport const layerEntrypointRegistry = (\n\tseed: ReadonlyArray<Entrypoint>,\n): Layer.Layer<EntrypointRegistry> =>\n\tLayer.succeed(EntrypointRegistry)(makeEntrypointRegistry(seed));\n"],"mappings":";;;AAkDA,IAAa,qBAAb,cAAwC,QAAQ,
|
|
1
|
+
{"version":3,"file":"entrypoints.mjs","names":[],"sources":["../../../src/orchestrators/router/entrypoints.ts"],"sourcesContent":["// Entrypoint registry — a process-global allowlist of named host\n// ports the Traefik container will listen on.\n//\n// Architecture invariants:\n// #6 — Routables select an entrypoint by *name* from a closed set\n// populated at module-load time. The set is read ONCE at\n// router launch.\n// #12 — Every entrypoint is plain HTTP / h2c / TCP; no TLS surface.\n// TCP entrypoints bind a host port and dispatch raw bytes to a\n// single backend (postgres, redis, …); they cannot share the\n// port across parallel stacks because TCP has no virtual-host\n// concept. HTTP/h2c entrypoints DO share ports via Host-header\n// dispatch.\n//\n// The registry is intentionally a typed Context service (Effect v4\n// idiom) rather than a module-mutable map: that lets tests build a\n// fresh registry per scenario, lets the orchestrator carry the\n// registration set forward immutably, and removes the\n// `ATTACHED_FOLLOWERS`-style hidden globals the architecture's\n// \"What's NOT in it\" section forbids.\n//\n// Construction surface:\n// `makeEntrypointRegistry(...)` — pure constructor; throws synchronously\n// on conflict registration (architecture invariant #6 is \"throw at\n// registration time\", not at acquire time).\n//\n// Lookup surface:\n// `EntrypointRegistry.byName(name)` — typed yieldable that fails\n// `UnknownEntrypoint` if the caller spliced an unregistered name.\n\nimport { Context, Effect, Layer } from 'effect';\n\nimport type { EntrypointDecl } from '../../contracts/routable.ts';\nimport { EntrypointConflict, UnknownEntrypoint } from './errors.ts';\n\n/** A named entrypoint — a host port the Traefik container binds and\n * listens on. `http`/`h2c` entrypoints support Host-header dispatch\n * (one port → many backends). `tcp` entrypoints are point-to-point\n * (one port → one backend) — siblings dial them directly. */\nexport type Entrypoint = EntrypointDecl;\n\n/** Registry shape. Closed map from declared name → canonical listener.\n * HTTP-family declarations may share a port; Traefik still needs one\n * listener name for that port, so aliases resolve to the first\n * registered listener. */\nexport interface EntrypointRegistryShape {\n\treadonly byName: (name: string) => Effect.Effect<Entrypoint, UnknownEntrypoint>;\n\treadonly all: () => ReadonlyArray<Entrypoint>;\n}\n\nexport class EntrypointRegistry extends Context.Service<\n\tEntrypointRegistry,\n\tEntrypointRegistryShape\n>()('@devstack/orchestrators/router/EntrypointRegistry') {}\n\nconst entrypointFamily = (protocol: Entrypoint['protocol']): 'http' | 'tcp' =>\n\tprotocol === 'tcp' ? 'tcp' : 'http';\n\n/** Build a registry from a literal seed. Idempotent on identical\n * `(name, port, protocol)` triples; throws synchronously on conflict\n * per architecture invariant #6. The reason this is synchronous\n * rather than yielded: registration is module-load wiring, not a\n * runtime effect — the failure mode is \"two callers wired the same\n * name to different ports\", which is a build-time bug.\n *\n * Traefik cannot bind multiple entrypoints to the same container port.\n * For HTTP-family aliases on one port, keep every declared name\n * lookupable but resolve those aliases to the first listener registered\n * for the port. */\nexport const makeEntrypointRegistry = (\n\tseed: ReadonlyArray<Entrypoint>,\n): EntrypointRegistryShape => {\n\tconst declaredByName = new Map<string, Entrypoint>();\n\tconst lookupByName = new Map<string, Entrypoint>();\n\tconst listenerByPort = new Map<number, Entrypoint>();\n\tfor (const e of seed) {\n\t\tconst existing = declaredByName.get(e.name);\n\t\tif (existing) {\n\t\t\tif (existing.port === e.port && existing.protocol === e.protocol) continue;\n\t\t\tthrow new EntrypointConflict({\n\t\t\t\tname: e.name,\n\t\t\t\texisting: { port: existing.port, protocol: existing.protocol },\n\t\t\t\tattempted: { port: e.port, protocol: e.protocol },\n\t\t\t});\n\t\t}\n\t\tdeclaredByName.set(e.name, e);\n\n\t\tconst listener = listenerByPort.get(e.port);\n\t\tif (listener) {\n\t\t\tconst listenerFamily = entrypointFamily(listener.protocol);\n\t\t\tconst attemptedFamily = entrypointFamily(e.protocol);\n\t\t\tif (listenerFamily === 'tcp' || listenerFamily !== attemptedFamily) {\n\t\t\t\tthrow new EntrypointConflict({\n\t\t\t\t\tname: e.name,\n\t\t\t\t\texisting: { port: listener.port, protocol: listener.protocol },\n\t\t\t\t\tattempted: { port: e.port, protocol: e.protocol },\n\t\t\t\t});\n\t\t\t}\n\t\t\tlookupByName.set(e.name, listener);\n\t\t\tcontinue;\n\t\t}\n\n\t\tlistenerByPort.set(e.port, e);\n\t\tlookupByName.set(e.name, e);\n\t}\n\tconst frozenListeners: ReadonlyArray<Entrypoint> = Array.from(listenerByPort.values());\n\tconst knownNames = Array.from(declaredByName.keys());\n\treturn {\n\t\tbyName: (name) => {\n\t\t\tconst hit = lookupByName.get(name);\n\t\t\tif (hit) return Effect.succeed(hit);\n\t\t\treturn Effect.fail(\n\t\t\t\tnew UnknownEntrypoint({\n\t\t\t\t\tname,\n\t\t\t\t\tknown: knownNames,\n\t\t\t\t}),\n\t\t\t);\n\t\t},\n\t\tall: () => frozenListeners,\n\t};\n};\n\n/** Layer that pins the registry from a literal seed. The orchestrator\n * takes this as a substrate dependency; plugins do NOT mutate the\n * registry — they pick names from it via their Routable contributions. */\nexport const layerEntrypointRegistry = (\n\tseed: ReadonlyArray<Entrypoint>,\n): Layer.Layer<EntrypointRegistry> =>\n\tLayer.succeed(EntrypointRegistry)(makeEntrypointRegistry(seed));\n"],"mappings":";;;AAkDA,IAAa,qBAAb,cAAwC,QAAQ,QAG9C,CAAC,CAAC,mDAAmD,CAAC,CAAC,CAAC;AAE1D,MAAM,oBAAoB,aACzB,aAAa,QAAQ,QAAQ;;;;;;;;;;;;AAa9B,MAAa,0BACZ,SAC6B;CAC7B,MAAM,iCAAiB,IAAI,IAAwB;CACnD,MAAM,+BAAe,IAAI,IAAwB;CACjD,MAAM,iCAAiB,IAAI,IAAwB;CACnD,KAAK,MAAM,KAAK,MAAM;EACrB,MAAM,WAAW,eAAe,IAAI,EAAE,IAAI;EAC1C,IAAI,UAAU;GACb,IAAI,SAAS,SAAS,EAAE,QAAQ,SAAS,aAAa,EAAE,UAAU;GAClE,MAAM,IAAI,mBAAmB;IAC5B,MAAM,EAAE;IACR,UAAU;KAAE,MAAM,SAAS;KAAM,UAAU,SAAS;IAAS;IAC7D,WAAW;KAAE,MAAM,EAAE;KAAM,UAAU,EAAE;IAAS;GACjD,CAAC;EACF;EACA,eAAe,IAAI,EAAE,MAAM,CAAC;EAE5B,MAAM,WAAW,eAAe,IAAI,EAAE,IAAI;EAC1C,IAAI,UAAU;GACb,MAAM,iBAAiB,iBAAiB,SAAS,QAAQ;GACzD,MAAM,kBAAkB,iBAAiB,EAAE,QAAQ;GACnD,IAAI,mBAAmB,SAAS,mBAAmB,iBAClD,MAAM,IAAI,mBAAmB;IAC5B,MAAM,EAAE;IACR,UAAU;KAAE,MAAM,SAAS;KAAM,UAAU,SAAS;IAAS;IAC7D,WAAW;KAAE,MAAM,EAAE;KAAM,UAAU,EAAE;IAAS;GACjD,CAAC;GAEF,aAAa,IAAI,EAAE,MAAM,QAAQ;GACjC;EACD;EAEA,eAAe,IAAI,EAAE,MAAM,CAAC;EAC5B,aAAa,IAAI,EAAE,MAAM,CAAC;CAC3B;CACA,MAAM,kBAA6C,MAAM,KAAK,eAAe,OAAO,CAAC;CACrF,MAAM,aAAa,MAAM,KAAK,eAAe,KAAK,CAAC;CACnD,OAAO;EACN,SAAS,SAAS;GACjB,MAAM,MAAM,aAAa,IAAI,IAAI;GACjC,IAAI,KAAK,OAAO,OAAO,QAAQ,GAAG;GAClC,OAAO,OAAO,KACb,IAAI,kBAAkB;IACrB;IACA,OAAO;GACR,CAAC,CACF;EACD;EACA,WAAW;CACZ;AACD;;;;AAKA,MAAa,2BACZ,SAEA,MAAM,QAAQ,kBAAkB,CAAC,CAAC,uBAAuB,IAAI,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../src/orchestrators/router/errors.ts"],"sourcesContent":["// Typed errors emitted by the router orchestrator.\n//\n// One tagged class per discriminated failure surface; the router is\n// the only producer so the shapes are tight. Effect v4 `Schema.TaggedErrorClass`\n// pattern matches the rest of the substrate (see\n// `substrate/runtime/errors.ts`).\n\nimport { Schema } from 'effect';\n\n/** Caller registered (or referenced) an entrypoint that isn't in the\n * process-global registry. Architecture invariant #6:\n * registrations are read once at router launch; this surface is the\n * programming-error path for callers that splice an unregistered\n * name into a Routable. */\nexport class UnknownEntrypoint extends Schema.TaggedErrorClass<UnknownEntrypoint>()(\n\t'UnknownEntrypoint',\n\t{\n\t\tname: Schema.String,\n\t\tknown: Schema.Array(Schema.String),\n\t},\n) {}\n\n/** Two callers tried to register the same entrypoint *name* with\n * conflicting `(port, protocol)`. Architecture invariant #6:\n * re-registering identically is idempotent; re-registering with a\n * conflict throws synchronously. */\nexport class EntrypointConflict extends Schema.TaggedErrorClass<EntrypointConflict>()(\n\t'EntrypointConflict',\n\t{\n\t\tname: Schema.String,\n\t\texisting: Schema.Struct({\n\t\t\tport: Schema.Number,\n\t\t\tprotocol: Schema.String,\n\t\t}),\n\t\tattempted: Schema.Struct({\n\t\t\tport: Schema.Number,\n\t\t\tprotocol: Schema.String,\n\t\t}),\n\t},\n) {}\n\n/** A user-influenceable string (dispatch id / hostname / upstream URL\n * / entrypoint name) didn't pass the validator. Architecture\n * invariant #13. Always a programming error in the caller, NOT a\n * transient. */\nexport class RouterValidationError extends Schema.TaggedErrorClass<RouterValidationError>()(\n\t'RouterValidationError',\n\t{\n\t\tfield: Schema.Literals(['hostname', 'dispatchId', 'entrypointName', 'upstreamUrl']),\n\t\tvalue: Schema.String,\n\t\tdetail: Schema.String,\n\t},\n) {}\n\n/** Two `Routable` contributions minted the same `(hostname, entrypoint)`\n * pair — i.e. the dispatch-id contract is violated upstream. Hard\n * failure: routing would be ambiguous. */\nexport class RouteCollision extends Schema.TaggedErrorClass<RouteCollision>()('RouteCollision', {\n\tmessage: Schema.String,\n\thostname: Schema.String,\n\tentrypoint: Schema.String,\n\tdispatchIds: Schema.Array(Schema.String),\n}) {}\n\n/** The router upstream URL could not be resolved within the bounded\n * retry budget — for container backends the network-connect's\n * async IP-allocation never settled. Architecture invariant #3. */\nexport class UpstreamResolveTimeout extends Schema.TaggedErrorClass<UpstreamResolveTimeout>()(\n\t'UpstreamResolveTimeout',\n\t{\n\t\tdispatchId: Schema.String,\n\t\tupstreamKind: Schema.Literals(['container', 'host-loopback']),\n\t\twaitedMillis: Schema.Number,\n\t},\n) {}\n\n/** The router boot failed (image-pull, network-create, container-run\n * all roll up here). Distinct tag from `UnknownEntrypoint` so callers\n * can `Effect.catchTag('RouterBootFailed', …)` and report proxy\n * infrastructure failures separately from route validation. */\nexport class RouterBootFailed extends Schema.TaggedErrorClass<RouterBootFailed>()(\n\t'RouterBootFailed',\n\t{\n\t\tstage: Schema.Literals([\n\t\t\t'ensure-network',\n\t\t\t'ensure-container',\n\t\t\t'write-shared-config',\n\t\t\t'inspect',\n\t\t]),\n\t\tdetail: Schema.String,\n\t\tcause: Schema.optional(Schema.Defect),\n\t},\n) {}\n\n/** Router opt-out only supports endpoints that are already reachable\n * from the host without Traefik. Container upstreams require the\n * router's Docker network + proxy entrypoint, so disabled mode must\n * fail explicitly instead of fabricating a localhost URL. */\nexport class RouterDisabledRouteUnsupported extends Schema.TaggedErrorClass<RouterDisabledRouteUnsupported>()(\n\t'RouterDisabledRouteUnsupported',\n\t{\n\t\tendpointName: Schema.String,\n\t\tupstreamKind: Schema.Literals(['container', 'host-loopback']),\n\t\tdetail: Schema.String,\n\t},\n) {}\n\n/** Writing a per-backend dispatch file failed. Distinct from\n * `RouterBootFailed` so callers can log/skip an individual route\n * without failing boot, per distilled-doc:\n * \"the per-primitive route is silently absent; the route's hostname\n * returns Traefik's default 404. Warning is logged…\". */\nexport class DispatchWriteFailed extends Schema.TaggedErrorClass<DispatchWriteFailed>()(\n\t'DispatchWriteFailed',\n\t{\n\t\tdispatchFileId: Schema.String,\n\t\tpath: Schema.String,\n\t\tdetail: Schema.String,\n\t\tcause: Schema.optional(Schema.Defect),\n\t},\n) {}\n\n/** The per-backend dispatch file was written, but the public router\n * entrypoint did not start serving that route within the readiness\n * budget. This catches Traefik file-provider reload races before the\n * endpoint is surfaced to users. */\nexport class RouteReadinessProbeFailed extends Schema.TaggedErrorClass<RouteReadinessProbeFailed>()(\n\t'RouteReadinessProbeFailed',\n\t{\n\t\tdispatchFileId: Schema.String,\n\t\turl: Schema.String,\n\t\ttimeoutMs: Schema.Number,\n\t\tdetail: Schema.String,\n\t\tcause: Schema.optional(Schema.Defect),\n\t},\n) {}\n\nexport type RouterError =\n\t| UnknownEntrypoint\n\t| EntrypointConflict\n\t| RouterValidationError\n\t| RouteCollision\n\t| UpstreamResolveTimeout\n\t| RouterBootFailed\n\t| RouterDisabledRouteUnsupported\n\t| DispatchWriteFailed\n\t| RouteReadinessProbeFailed;\n"],"mappings":";;;;;;;AAcA,IAAa,oBAAb,cAAuC,OAAO,
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../src/orchestrators/router/errors.ts"],"sourcesContent":["// Typed errors emitted by the router orchestrator.\n//\n// One tagged class per discriminated failure surface; the router is\n// the only producer so the shapes are tight. Effect v4 `Schema.TaggedErrorClass`\n// pattern matches the rest of the substrate (see\n// `substrate/runtime/errors.ts`).\n\nimport { Schema } from 'effect';\n\n/** Caller registered (or referenced) an entrypoint that isn't in the\n * process-global registry. Architecture invariant #6:\n * registrations are read once at router launch; this surface is the\n * programming-error path for callers that splice an unregistered\n * name into a Routable. */\nexport class UnknownEntrypoint extends Schema.TaggedErrorClass<UnknownEntrypoint>()(\n\t'UnknownEntrypoint',\n\t{\n\t\tname: Schema.String,\n\t\tknown: Schema.Array(Schema.String),\n\t},\n) {}\n\n/** Two callers tried to register the same entrypoint *name* with\n * conflicting `(port, protocol)`. Architecture invariant #6:\n * re-registering identically is idempotent; re-registering with a\n * conflict throws synchronously. */\nexport class EntrypointConflict extends Schema.TaggedErrorClass<EntrypointConflict>()(\n\t'EntrypointConflict',\n\t{\n\t\tname: Schema.String,\n\t\texisting: Schema.Struct({\n\t\t\tport: Schema.Number,\n\t\t\tprotocol: Schema.String,\n\t\t}),\n\t\tattempted: Schema.Struct({\n\t\t\tport: Schema.Number,\n\t\t\tprotocol: Schema.String,\n\t\t}),\n\t},\n) {}\n\n/** A user-influenceable string (dispatch id / hostname / upstream URL\n * / entrypoint name) didn't pass the validator. Architecture\n * invariant #13. Always a programming error in the caller, NOT a\n * transient. */\nexport class RouterValidationError extends Schema.TaggedErrorClass<RouterValidationError>()(\n\t'RouterValidationError',\n\t{\n\t\tfield: Schema.Literals(['hostname', 'dispatchId', 'entrypointName', 'upstreamUrl']),\n\t\tvalue: Schema.String,\n\t\tdetail: Schema.String,\n\t},\n) {}\n\n/** Two `Routable` contributions minted the same `(hostname, entrypoint)`\n * pair — i.e. the dispatch-id contract is violated upstream. Hard\n * failure: routing would be ambiguous. */\nexport class RouteCollision extends Schema.TaggedErrorClass<RouteCollision>()('RouteCollision', {\n\tmessage: Schema.String,\n\thostname: Schema.String,\n\tentrypoint: Schema.String,\n\tdispatchIds: Schema.Array(Schema.String),\n}) {}\n\n/** The router upstream URL could not be resolved within the bounded\n * retry budget — for container backends the network-connect's\n * async IP-allocation never settled. Architecture invariant #3. */\nexport class UpstreamResolveTimeout extends Schema.TaggedErrorClass<UpstreamResolveTimeout>()(\n\t'UpstreamResolveTimeout',\n\t{\n\t\tdispatchId: Schema.String,\n\t\tupstreamKind: Schema.Literals(['container', 'host-loopback']),\n\t\twaitedMillis: Schema.Number,\n\t},\n) {}\n\n/** The router boot failed (image-pull, network-create, container-run\n * all roll up here). Distinct tag from `UnknownEntrypoint` so callers\n * can `Effect.catchTag('RouterBootFailed', …)` and report proxy\n * infrastructure failures separately from route validation. */\nexport class RouterBootFailed extends Schema.TaggedErrorClass<RouterBootFailed>()(\n\t'RouterBootFailed',\n\t{\n\t\tstage: Schema.Literals([\n\t\t\t'ensure-network',\n\t\t\t'ensure-container',\n\t\t\t'write-shared-config',\n\t\t\t'inspect',\n\t\t]),\n\t\tdetail: Schema.String,\n\t\tcause: Schema.optional(Schema.Defect),\n\t},\n) {}\n\n/** Router opt-out only supports endpoints that are already reachable\n * from the host without Traefik. Container upstreams require the\n * router's Docker network + proxy entrypoint, so disabled mode must\n * fail explicitly instead of fabricating a localhost URL. */\nexport class RouterDisabledRouteUnsupported extends Schema.TaggedErrorClass<RouterDisabledRouteUnsupported>()(\n\t'RouterDisabledRouteUnsupported',\n\t{\n\t\tendpointName: Schema.String,\n\t\tupstreamKind: Schema.Literals(['container', 'host-loopback']),\n\t\tdetail: Schema.String,\n\t},\n) {}\n\n/** Writing a per-backend dispatch file failed. Distinct from\n * `RouterBootFailed` so callers can log/skip an individual route\n * without failing boot, per distilled-doc:\n * \"the per-primitive route is silently absent; the route's hostname\n * returns Traefik's default 404. Warning is logged…\". */\nexport class DispatchWriteFailed extends Schema.TaggedErrorClass<DispatchWriteFailed>()(\n\t'DispatchWriteFailed',\n\t{\n\t\tdispatchFileId: Schema.String,\n\t\tpath: Schema.String,\n\t\tdetail: Schema.String,\n\t\tcause: Schema.optional(Schema.Defect),\n\t},\n) {}\n\n/** The per-backend dispatch file was written, but the public router\n * entrypoint did not start serving that route within the readiness\n * budget. This catches Traefik file-provider reload races before the\n * endpoint is surfaced to users. */\nexport class RouteReadinessProbeFailed extends Schema.TaggedErrorClass<RouteReadinessProbeFailed>()(\n\t'RouteReadinessProbeFailed',\n\t{\n\t\tdispatchFileId: Schema.String,\n\t\turl: Schema.String,\n\t\ttimeoutMs: Schema.Number,\n\t\tdetail: Schema.String,\n\t\tcause: Schema.optional(Schema.Defect),\n\t},\n) {}\n\nexport type RouterError =\n\t| UnknownEntrypoint\n\t| EntrypointConflict\n\t| RouterValidationError\n\t| RouteCollision\n\t| UpstreamResolveTimeout\n\t| RouterBootFailed\n\t| RouterDisabledRouteUnsupported\n\t| DispatchWriteFailed\n\t| RouteReadinessProbeFailed;\n"],"mappings":";;;;;;;AAcA,IAAa,oBAAb,cAAuC,OAAO,iBAAoC,CAAC,CAClF,qBACA;CACC,MAAM,OAAO;CACb,OAAO,OAAO,MAAM,OAAO,MAAM;AAClC,CACD,CAAC,CAAC,CAAC;;;;;AAMH,IAAa,qBAAb,cAAwC,OAAO,iBAAqC,CAAC,CACpF,sBACA;CACC,MAAM,OAAO;CACb,UAAU,OAAO,OAAO;EACvB,MAAM,OAAO;EACb,UAAU,OAAO;CAClB,CAAC;CACD,WAAW,OAAO,OAAO;EACxB,MAAM,OAAO;EACb,UAAU,OAAO;CAClB,CAAC;AACF,CACD,CAAC,CAAC,CAAC;;;;;AAMH,IAAa,wBAAb,cAA2C,OAAO,iBAAwC,CAAC,CAC1F,yBACA;CACC,OAAO,OAAO,SAAS;EAAC;EAAY;EAAc;EAAkB;CAAa,CAAC;CAClF,OAAO,OAAO;CACd,QAAQ,OAAO;AAChB,CACD,CAAC,CAAC,CAAC;;;;AAKH,IAAa,iBAAb,cAAoC,OAAO,iBAAiC,CAAC,CAAC,kBAAkB;CAC/F,SAAS,OAAO;CAChB,UAAU,OAAO;CACjB,YAAY,OAAO;CACnB,aAAa,OAAO,MAAM,OAAO,MAAM;AACxC,CAAC,CAAC,CAAC,CAAC;AAKwC,OAAO,iBAAyC,CAAC,CAC5F,0BACA;CACC,YAAY,OAAO;CACnB,cAAc,OAAO,SAAS,CAAC,aAAa,eAAe,CAAC;CAC5D,cAAc,OAAO;AACtB,CACD;;;;;AAMA,IAAa,mBAAb,cAAsC,OAAO,iBAAmC,CAAC,CAChF,oBACA;CACC,OAAO,OAAO,SAAS;EACtB;EACA;EACA;EACA;CACD,CAAC;CACD,QAAQ,OAAO;CACf,OAAO,OAAO,SAAS,OAAO,MAAM;AACrC,CACD,CAAC,CAAC,CAAC;;;;;AAMH,IAAa,iCAAb,cAAoD,OAAO,iBAAiD,CAAC,CAC5G,kCACA;CACC,cAAc,OAAO;CACrB,cAAc,OAAO,SAAS,CAAC,aAAa,eAAe,CAAC;CAC5D,QAAQ,OAAO;AAChB,CACD,CAAC,CAAC,CAAC;;;;;;AAOH,IAAa,sBAAb,cAAyC,OAAO,iBAAsC,CAAC,CACtF,uBACA;CACC,gBAAgB,OAAO;CACvB,MAAM,OAAO;CACb,QAAQ,OAAO;CACf,OAAO,OAAO,SAAS,OAAO,MAAM;AACrC,CACD,CAAC,CAAC,CAAC;;;;;AAMH,IAAa,4BAAb,cAA+C,OAAO,iBAA4C,CAAC,CAClG,6BACA;CACC,gBAAgB,OAAO;CACvB,KAAK,OAAO;CACZ,WAAW,OAAO;CAClB,QAAQ,OAAO;CACf,OAAO,OAAO,SAAS,OAAO,MAAM;AACrC,CACD,CAAC,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-provider.mjs","names":[],"sources":["../../../src/orchestrators/router/file-provider.ts"],"sourcesContent":["// File-provider config generator.\n//\n// Architecture invariant #1: file-provider ONLY, never docker-provider.\n// We materialize per-backend dispatch entries as YAML files in a\n// watched directory; Traefik's file-provider polls + reloads on\n// change. The docker-provider was rejected because the container IP\n// we need is the *shared-network* IP that lands AFTER `network connect`,\n// and the docker-provider captures the per-stack IP at first event\n// and never refreshes.\n//\n// One file per canonical route identity. Each file carries exactly\n// one router + one service (and references the shared CORS middleware\n// when `cors: true`). This layout means atomic add/remove of a route\n// is one file create/unlink — no merge step, no race.\n//\n// Two YAML shapes live under this renderer, discriminated by the\n// resolved route's `wireProtocol`:\n//\n// HTTP / h2c (Host-header dispatch on a shared port):\n//\n// http:\n// routers:\n// <id>-router:\n// rule: \"Host(`<hostname>`)\"\n// entryPoints: [\"<entrypointName>\"]\n// service: \"<id>-svc\"\n// middlewares: [\"devstack-cors\"] # when cors: true\n// services:\n// <id>-svc:\n// loadBalancer:\n// servers:\n// - url: \"http://<upstream-host>:<upstream-port>\"\n//\n// TCP (per-entrypoint-port dispatch; ONE backend per entrypoint):\n//\n// tcp:\n// routers:\n// <id>-router:\n// rule: \"HostSNI(`*`)\"\n// entryPoints: [\"<entrypointName>\"]\n// service: \"<id>-svc\"\n// services:\n// <id>-svc:\n// loadBalancer:\n// servers:\n// - address: \"<upstream-host>:<upstream-port>\"\n//\n// HostSNI(`*`) matches any incoming TCP connection on the entrypoint\n// (Traefik requires every TCP router to have a rule; `*` is the wildcard\n// that means \"any client\"). No CORS section — TCP isn't HTTP and the\n// shared CORS middleware lives under `http.middlewares`.\n\nimport { Effect } from 'effect';\n\nimport type { RoutableDecl } from '../../contracts/routable.ts';\nimport type { RosterHolder } from '../../substrate/cross-process.ts';\nimport type { Identity } from '../../substrate/identity.ts';\nimport { CORS_MIDDLEWARE_NAME } from './cors.ts';\nimport type { EntrypointRegistryShape } from './entrypoints.ts';\nimport { RouteCollision, RouterValidationError, type UnknownEntrypoint } from './errors.ts';\nimport { dispatchFileId, renderUrl, routerHostname } from './hostname.ts';\n\nexport const ROUTE_READINESS_HEADER = 'X-Devstack-Route-Id';\n\n/** Dispatch-file filename prefix. `10-` sorts behind the shared CORS\n * middleware (`00-`) so Traefik's file-provider loads the middleware\n * before any router that references it. Single source of truth for\n * the file-write side (`dispatchFilename`) and the file-read side\n * (`dispatchFileIdFromFilename`) — keep them in lockstep here. */\nconst DISPATCH_FILENAME_PREFIX = '10-';\n\n// ---------------------------------------------------------------------------\n// Resolved-route data structure\n// ---------------------------------------------------------------------------\n\n/** Resolved wire-protocol. `'tcp'` carries no hostname matcher and no\n * CORS. The router orchestrator's renderer + collision-detector branch\n * on this. */\nexport type ResolvedWireProtocol = 'http' | 'h2c' | 'tcp';\n\n/** A `RoutableDecl` resolved by the orchestrator: hostname minted,\n * dispatch-id stringified, entrypoint port resolved, upstream URL\n * resolved. This is the shape the file-provider renderer consumes —\n * decoupled from the resolution path so testing the renderer is\n * pure. */\nexport interface ResolvedRoute {\n\treadonly dispatchFileId: string;\n\t/** Minted hostname for HTTP routes. For TCP routes this is still\n\t * computed (it's the host string the URL surfaces to consumers),\n\t * but the Traefik router rule is `HostSNI(\\`*\\`)` — TCP dispatches\n\t * by entrypoint port, not Host. */\n\treadonly hostname: string;\n\treadonly entrypointName: string;\n\treadonly entrypointPort: number;\n\t/** Upstream URL string. For HTTP this is `http://<host>:<port>`; for\n\t * TCP it's still rendered as `tcp://<host>:<port>` so the renderer\n\t * can pull host+port back out for the `address:` field. */\n\treadonly upstreamUrl: string;\n\treadonly cors: boolean;\n\treadonly wireProtocol: ResolvedWireProtocol;\n}\n\nexport interface RouteCollisionMetadata {\n\treadonly dispatchFileId: string;\n\treadonly hostname: string;\n\treadonly entrypointName: string;\n\treadonly entrypointPort: number | null;\n\treadonly wireProtocol: ResolvedWireProtocol;\n}\n\nexport const ROUTER_ROUTE_LEASE_VERSION = 1;\n\nexport interface RouteLeaseMetadata {\n\treadonly version: typeof ROUTER_ROUTE_LEASE_VERSION;\n\treadonly routerProfileId: string;\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly owner: RosterHolder;\n}\n\nexport interface DispatchRouteMetadata extends RouteCollisionMetadata {\n\treadonly lease: RouteLeaseMetadata | null;\n}\n\nexport type DispatchRouteDecodeReason =\n\t| 'missing-required-route-metadata'\n\t| 'unknown-route-lease-version'\n\t| 'invalid-route-lease-metadata';\n\nexport interface DispatchRouteDecodeDiagnostic {\n\treadonly _tag: 'DispatchRouteDecodeDiagnostic';\n\treadonly dispatchFileId: string;\n\treadonly reason: DispatchRouteDecodeReason;\n\treadonly detail: string;\n}\n\nexport type DispatchRouteParseResult =\n\t| {\n\t\t\treadonly _tag: 'valid';\n\t\t\treadonly route: DispatchRouteMetadata;\n\t\t\treadonly diagnostics: ReadonlyArray<DispatchRouteDecodeDiagnostic>;\n\t }\n\t| {\n\t\t\treadonly _tag: 'invalid';\n\t\t\treadonly dispatchFileId: string;\n\t\t\treadonly diagnostics: ReadonlyArray<DispatchRouteDecodeDiagnostic>;\n\t };\n\n/** Filename within the dispatch directory for a given file-id. The\n * prefix (`DISPATCH_FILENAME_PREFIX`) sorts behind the shared CORS\n * middleware (`00-`) so Traefik picks up the middleware before any\n * router referencing it. */\nexport const dispatchFilename = (fileId: string): string =>\n\t`${DISPATCH_FILENAME_PREFIX}${fileId}.yml`;\n\n// ---------------------------------------------------------------------------\n// Resolution — Routable + Identity + EntrypointRegistry + upstream → ResolvedRoute\n// ---------------------------------------------------------------------------\n\n/** Upstream-resolution interface the file-provider needs. The\n * orchestrator threads in concrete resolvers (`runtime/docker` for\n * containers, the port broker for host-loopback) without leaking\n * service knowledge. Architecture: \"router has no compiled-in\n * awareness of which services exist.\" */\nexport interface UpstreamResolver {\n\t/** For a container-kind upstream, return `(host, port)` where host\n\t * is the container's shared-network IP and port is the container\n\t * port from the plugin. Bounded retry lives inside the resolver\n\t * (architecture invariant #3). */\n\treadonly resolveContainer: (target: {\n\t\treadonly containerName: string;\n\t\treadonly containerPort: number;\n\t}) => Effect.Effect<{ readonly host: string; readonly port: number }, RouterValidationError>;\n\t/** For a host-loopback-kind upstream, return the bound loopback\n\t * port. The plugin owns port allocation and stamps the resolved\n\t * port into its Routable decl after acquire. */\n\treadonly resolveHostLoopback: (target: {\n\t\treadonly port: number;\n\t}) => Effect.Effect<{ readonly host: string; readonly port: number }, RouterValidationError>;\n}\n\n/** Resolve a single Routable into a ResolvedRoute. Pure-ish: the only\n * effectful bit is the upstream resolver (which talks to docker /\n * the port broker) and the entrypoint lookup.\n *\n * Takes the registry as a parameter rather than yielding the service\n * tag so the calling orchestrator can pre-bind it once at layer-\n * construction time, keeping the per-Routable hot path free of\n * Context lookups. */\nexport const resolveRoute = (\n\tidentity: Identity,\n\tdecl: RoutableDecl,\n\tregistry: EntrypointRegistryShape,\n\tupstreams: UpstreamResolver,\n): Effect.Effect<ResolvedRoute, RouterValidationError | UnknownEntrypoint> =>\n\tEffect.gen(function* () {\n\t\tconst entrypoint = yield* registry.byName(decl.endpointName);\n\t\tconst hostname = yield* routerHostname(identity, decl.dispatchId.role);\n\t\tconst fileId = yield* dispatchFileId({ identity, dispatch: decl.dispatchId });\n\t\tconst upstream =\n\t\t\tdecl.upstream.type === 'container'\n\t\t\t\t? yield* upstreams.resolveContainer({\n\t\t\t\t\t\tcontainerName: decl.upstream.containerName,\n\t\t\t\t\t\tcontainerPort: decl.upstream.containerPort,\n\t\t\t\t\t})\n\t\t\t\t: yield* upstreams.resolveHostLoopback({ port: decl.upstream.port });\n\t\tconst wireProtocol: ResolvedWireProtocol =\n\t\t\tdecl.wireProtocol === 'tcp' ? 'tcp' : decl.wireProtocol === 'h2c' ? 'h2c' : 'http';\n\t\t// The router contract says: a `wireProtocol: 'tcp'` decl MUST\n\t\t// reference an entrypoint whose protocol is also `'tcp'`. The\n\t\t// converse holds — HTTP/h2c decls must reference HTTP-family\n\t\t// entrypoints. Mismatch is a programming error in the plugin\n\t\t// author (caught here once, not at every render).\n\t\tconst expectFamily: 'tcp' | 'http' = wireProtocol === 'tcp' ? 'tcp' : 'http';\n\t\tconst entrypointFamily: 'tcp' | 'http' = entrypoint.protocol === 'tcp' ? 'tcp' : 'http';\n\t\tif (expectFamily !== entrypointFamily) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterValidationError({\n\t\t\t\t\tfield: 'entrypointName',\n\t\t\t\t\tvalue: entrypoint.name,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t`wireProtocol family mismatch: decl is '${wireProtocol}' but ` +\n\t\t\t\t\t\t`entrypoint '${entrypoint.name}' is '${entrypoint.protocol}'`,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\t// Validate the upstream URL — defense in depth (the resolvers\n\t\t// already produce safe values, but this stops a future resolver\n\t\t// that returns junk from corrupting the YAML). TCP carries the\n\t\t// `tcp://` scheme so we differentiate from the HTTP path; the\n\t\t// renderer pulls host+port back out either way.\n\t\tconst upstreamUrl = renderUrl({\n\t\t\tprotocol: wireProtocol === 'tcp' ? 'tcp' : 'http',\n\t\t\thostname: upstream.host,\n\t\t\tport: upstream.port,\n\t\t});\n\t\tif (!/^(?:http|tcp):\\/\\/[A-Za-z0-9_.:-]+:\\d+$/.test(upstreamUrl)) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterValidationError({\n\t\t\t\t\tfield: 'upstreamUrl',\n\t\t\t\t\tvalue: upstreamUrl,\n\t\t\t\t\tdetail: 'expected http://<host>:<port> or tcp://<host>:<port>',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\t// CORS is HTTP-only — TCP decls don't carry the field. Render\n\t\t// uses this; the resolved shape carries `false` for TCP.\n\t\tconst cors = decl.wireProtocol === 'tcp' ? false : decl.cors;\n\t\treturn {\n\t\t\tdispatchFileId: fileId,\n\t\t\thostname,\n\t\t\tentrypointName: entrypoint.name,\n\t\t\tentrypointPort: entrypoint.port,\n\t\t\tupstreamUrl,\n\t\t\tcors,\n\t\t\twireProtocol,\n\t\t};\n\t});\n\n// ---------------------------------------------------------------------------\n// Render — ResolvedRoute → YAML body\n// ---------------------------------------------------------------------------\n\n/** Pull host+port out of an `<scheme>://<host>:<port>` URL. Used by the\n * TCP renderer to write the `address:` field. Safe because\n * `resolveRoute` validates the URL shape before we get here. */\nconst splitUpstream = (url: string): { host: string; port: string } => {\n\tconst stripped = url.replace(/^(?:http|tcp):\\/\\//, '');\n\tconst lastColon = stripped.lastIndexOf(':');\n\treturn {\n\t\thost: stripped.slice(0, lastColon),\n\t\tport: stripped.slice(lastColon + 1),\n\t};\n};\n\nconst renderLeaseHeader = (lease: RouteLeaseMetadata): ReadonlyArray<string> => [\n\t`# routeLeaseVersion: ${lease.version}`,\n\t`# routerProfileId: ${lease.routerProfileId}`,\n\t`# ownerApp: ${lease.app}`,\n\t`# ownerStack: ${lease.stack}`,\n\t`# ownerPid: ${lease.owner.pid}`,\n\t`# ownerStartTime: ${lease.owner.startTime}`,\n\t`# ownerHostname: ${lease.owner.hostname}`,\n\t`# ownerClaimedAt: ${lease.owner.claimedAt}`,\n\t`# ownerHeartbeatAt: ${lease.owner.heartbeatAt}`,\n\t`# ownerIntent: ${lease.owner.intent}`,\n];\n\n/** Render the YAML body for a single resolved route. Hand-rolled\n * (same rationale as `cors.ts`): static, controlled, byte-stable so\n * no-op rewrites don't wake the watcher. */\nexport const renderRouteYaml = (route: ResolvedRoute, lease: RouteLeaseMetadata): string => {\n\tif (route.wireProtocol === 'tcp') return renderTcpRouteYaml(route, lease);\n\treturn renderHttpRouteYaml(route, lease);\n};\n\nconst routeReadinessMiddlewareName = (route: ResolvedRoute): string =>\n\t`${route.dispatchFileId}-route-ready`;\n\nconst renderHttpRouteYaml = (route: ResolvedRoute, lease: RouteLeaseMetadata): string => {\n\tconst middlewares = [\n\t\trouteReadinessMiddlewareName(route),\n\t\t...(route.cors ? [CORS_MIDDLEWARE_NAME] : []),\n\t];\n\tconst schemeHint =\n\t\troute.wireProtocol === 'h2c'\n\t\t\t? ` # h2c upstream — gRPC-friendly cleartext HTTP/2.\\n`\n\t\t\t: '';\n\treturn [\n\t\t`# Auto-generated by devstack router orchestrator. Do not edit by hand.`,\n\t\t`# dispatchFileId: ${route.dispatchFileId}`,\n\t\t`# wireProtocol: ${route.wireProtocol}`,\n\t\t`# entrypointName: ${route.entrypointName}`,\n\t\t`# entrypointPort: ${route.entrypointPort}`,\n\t\t`# hostname: ${route.hostname}`,\n\t\t...renderLeaseHeader(lease),\n\t\t`http:`,\n\t\t` routers:`,\n\t\t` ${route.dispatchFileId}-router:`,\n\t\t` rule: \"Host(\\`${route.hostname}\\`)\"`,\n\t\t` entryPoints: [\"${route.entrypointName}\"]`,\n\t\t` service: \"${route.dispatchFileId}-svc\"`,\n\t\t` middlewares: [${middlewares.map((name) => `\"${name}\"`).join(', ')}]`,\n\t\t` middlewares:`,\n\t\t` ${routeReadinessMiddlewareName(route)}:`,\n\t\t` headers:`,\n\t\t` customResponseHeaders:`,\n\t\t` ${ROUTE_READINESS_HEADER}: \"${route.dispatchFileId}\"`,\n\t\t` services:`,\n\t\t` ${route.dispatchFileId}-svc:`,\n\t\t` loadBalancer:`,\n\t\tschemeHint.length > 0 ? schemeHint.trimEnd() : null,\n\t\t` servers:`,\n\t\t` - url: \"${route.upstreamUrl}\"`,\n\t\t``,\n\t]\n\t\t.filter((line): line is string => line !== null)\n\t\t.join('\\n');\n};\n\nconst renderTcpRouteYaml = (route: ResolvedRoute, lease: RouteLeaseMetadata): string => {\n\tconst { host, port } = splitUpstream(route.upstreamUrl);\n\treturn [\n\t\t`# Auto-generated by devstack router orchestrator. Do not edit by hand.`,\n\t\t`# dispatchFileId: ${route.dispatchFileId}`,\n\t\t`# wireProtocol: tcp`,\n\t\t`# entrypointName: ${route.entrypointName}`,\n\t\t`# entrypointPort: ${route.entrypointPort}`,\n\t\t`# hostname: ${route.hostname}`,\n\t\t`# tcpDispatch: entrypoint-port dispatch; HostSNI wildcard`,\n\t\t...renderLeaseHeader(lease),\n\t\t`tcp:`,\n\t\t` routers:`,\n\t\t` ${route.dispatchFileId}-router:`,\n\t\t` rule: \"HostSNI(\\`*\\`)\"`,\n\t\t` entryPoints: [\"${route.entrypointName}\"]`,\n\t\t` service: \"${route.dispatchFileId}-svc\"`,\n\t\t` services:`,\n\t\t` ${route.dispatchFileId}-svc:`,\n\t\t` loadBalancer:`,\n\t\t` servers:`,\n\t\t` - address: \"${host}:${port}\"`,\n\t\t``,\n\t].join('\\n');\n};\n\nexport const dispatchFileIdFromFilename = (filename: string): string | null => {\n\t// Escape `DISPATCH_FILENAME_PREFIX` for use inside a RegExp literal —\n\t// today it's a plain `10-` so no regex metacharacters are at play,\n\t// but the escape makes the read side robust to future prefix changes.\n\tconst escapedPrefix = DISPATCH_FILENAME_PREFIX.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\tconst match = new RegExp(`^${escapedPrefix}(.+)\\\\.yml$`).exec(filename);\n\treturn match?.[1] ?? null;\n};\n\nconst commentValue = (body: string, key: string): string | null => {\n\tconst escaped = key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\tconst match = new RegExp(`^# ${escaped}: (.*)$`, 'm').exec(body);\n\treturn match?.[1]?.trim() ?? null;\n};\n\nconst matchValue = (body: string, pattern: RegExp): string | null =>\n\tpattern.exec(body)?.[1]?.trim() ?? null;\n\nconst commentNumber = (body: string, key: string): number | null => {\n\tconst raw = commentValue(body, key);\n\tif (raw === null || !/^\\d+$/.test(raw)) return null;\n\treturn Number.parseInt(raw, 10);\n};\n\nconst parseWireProtocol = (body: string): ResolvedWireProtocol | null => {\n\tconst fromComment = commentValue(body, 'wireProtocol');\n\tif (fromComment === 'http' || fromComment === 'h2c' || fromComment === 'tcp') {\n\t\treturn fromComment;\n\t}\n\tif (/^tcp:/m.test(body)) return 'tcp';\n\tif (/^http:/m.test(body)) return 'http';\n\treturn null;\n};\n\nconst hasRouteLeaseMetadata = (body: string): boolean =>\n\t[\n\t\t'routeLeaseVersion',\n\t\t'routerProfileId',\n\t\t'ownerApp',\n\t\t'ownerStack',\n\t\t'ownerPid',\n\t\t'ownerStartTime',\n\t\t'ownerHostname',\n\t\t'ownerClaimedAt',\n\t\t'ownerHeartbeatAt',\n\t\t'ownerIntent',\n\t].some((key) => commentValue(body, key) !== null);\n\nconst parseRouteLeaseMetadata = (\n\tbody: string,\n\tdispatchFileId: string,\n): {\n\treadonly lease: RouteLeaseMetadata | null;\n\treadonly diagnostic: DispatchRouteDecodeDiagnostic | null;\n} => {\n\tif (!hasRouteLeaseMetadata(body)) return { lease: null, diagnostic: null };\n\tconst rawVersion = commentValue(body, 'routeLeaseVersion');\n\tconst version = commentNumber(body, 'routeLeaseVersion');\n\tconst routerProfileId = commentValue(body, 'routerProfileId');\n\tconst app = commentValue(body, 'ownerApp');\n\tconst stack = commentValue(body, 'ownerStack');\n\tconst pid = commentNumber(body, 'ownerPid');\n\tconst startTime = commentNumber(body, 'ownerStartTime');\n\tconst hostname = commentValue(body, 'ownerHostname');\n\tconst claimedAt = commentNumber(body, 'ownerClaimedAt');\n\tconst heartbeatAt = commentNumber(body, 'ownerHeartbeatAt');\n\tconst intent = commentValue(body, 'ownerIntent');\n\tif (version !== ROUTER_ROUTE_LEASE_VERSION) {\n\t\treturn {\n\t\t\tlease: null,\n\t\t\tdiagnostic: {\n\t\t\t\t_tag: 'DispatchRouteDecodeDiagnostic',\n\t\t\t\tdispatchFileId,\n\t\t\t\treason: 'unknown-route-lease-version',\n\t\t\t\tdetail: `expected routeLeaseVersion ${ROUTER_ROUTE_LEASE_VERSION}, got ${rawVersion ?? '<missing>'}`,\n\t\t\t},\n\t\t};\n\t}\n\tif (\n\t\trouterProfileId === null ||\n\t\tapp === null ||\n\t\tstack === null ||\n\t\tpid === null ||\n\t\tstartTime === null ||\n\t\thostname === null ||\n\t\tclaimedAt === null ||\n\t\theartbeatAt === null ||\n\t\t(intent !== 'normal' && intent !== 'snapshot')\n\t) {\n\t\treturn {\n\t\t\tlease: null,\n\t\t\tdiagnostic: {\n\t\t\t\t_tag: 'DispatchRouteDecodeDiagnostic',\n\t\t\t\tdispatchFileId,\n\t\t\t\treason: 'invalid-route-lease-metadata',\n\t\t\t\tdetail: 'route lease metadata is incomplete or malformed',\n\t\t\t},\n\t\t};\n\t}\n\treturn {\n\t\tlease: {\n\t\t\tversion: ROUTER_ROUTE_LEASE_VERSION,\n\t\t\trouterProfileId,\n\t\t\tapp,\n\t\t\tstack,\n\t\t\towner: {\n\t\t\t\tpid,\n\t\t\t\tstartTime,\n\t\t\t\thostname,\n\t\t\t\tclaimedAt,\n\t\t\t\theartbeatAt,\n\t\t\t\tintent,\n\t\t\t},\n\t\t},\n\t\tdiagnostic: null,\n\t};\n};\n\nexport const parseDispatchRouteFile = (\n\tbody: string,\n\tfallbackDispatchFileId: string | null = null,\n): DispatchRouteParseResult => {\n\tconst dispatchFileId = commentValue(body, 'dispatchFileId') ?? fallbackDispatchFileId;\n\tconst wireProtocol = parseWireProtocol(body);\n\tconst entrypointName =\n\t\tcommentValue(body, 'entrypointName') ?? matchValue(body, /entryPoints: \\[\"([^\"]+)\"\\]/);\n\tconst hostname =\n\t\tcommentValue(body, 'hostname') ??\n\t\tmatchValue(body, /Host\\(`([^`]+)`\\)/) ??\n\t\t(wireProtocol === 'tcp' ? '' : null);\n\tconst portRaw = commentValue(body, 'entrypointPort');\n\tconst entrypointPort =\n\t\tportRaw === null || !/^\\d+$/.test(portRaw) ? null : Number.parseInt(portRaw, 10);\n\n\tif (\n\t\tdispatchFileId === null ||\n\t\twireProtocol === null ||\n\t\tentrypointName === null ||\n\t\thostname === null\n\t) {\n\t\tconst protectedDispatchFileId = dispatchFileId ?? '<unknown>';\n\t\treturn {\n\t\t\t_tag: 'invalid',\n\t\t\tdispatchFileId: protectedDispatchFileId,\n\t\t\tdiagnostics: [\n\t\t\t\t{\n\t\t\t\t\t_tag: 'DispatchRouteDecodeDiagnostic',\n\t\t\t\t\tdispatchFileId: protectedDispatchFileId,\n\t\t\t\t\treason: 'missing-required-route-metadata',\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t'route file is missing dispatchFileId, wireProtocol/http|tcp block, entrypointName, or hostname metadata',\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\tconst leaseResult = parseRouteLeaseMetadata(body, dispatchFileId);\n\n\treturn {\n\t\t_tag: 'valid',\n\t\troute: {\n\t\t\tdispatchFileId,\n\t\t\thostname,\n\t\t\tentrypointName,\n\t\t\tentrypointPort,\n\t\t\twireProtocol,\n\t\t\tlease: leaseResult.lease,\n\t\t},\n\t\tdiagnostics: leaseResult.diagnostic === null ? [] : [leaseResult.diagnostic],\n\t};\n};\n\nexport const parseDispatchRouteMetadata = (\n\tbody: string,\n\tfallbackDispatchFileId: string | null = null,\n): DispatchRouteMetadata | null => {\n\tconst parsed = parseDispatchRouteFile(body, fallbackDispatchFileId);\n\treturn parsed._tag === 'valid' ? parsed.route : null;\n};\n\n// ---------------------------------------------------------------------------\n// Collision detection\n// ---------------------------------------------------------------------------\n\n/** Assert no two resolved routes share dispatch keys.\n *\n * - HTTP routes collide on `(entrypoint, hostname)` — two HTTP backends\n * on the same entrypoint port can coexist via different Host headers,\n * so the Host *is* part of the key.\n * - TCP routes collide on `entrypoint` alone — TCP has no Host header,\n * so an entrypoint can serve exactly ONE backend. Two TCP decls on\n * the same entrypoint are an unambiguous error (parallel stacks of\n * a TCP service share the host port and would clobber each other).\n *\n * Architecture invariant #7 — distinct identity → distinct dispatch\n * URL. The TCP arm is the new clause; HTTP arm unchanged. */\nexport const detectCollisions = (\n\troutes: ReadonlyArray<RouteCollisionMetadata>,\n): RouteCollision | null => {\n\tconst seen = new Map<string, Array<RouteCollisionMetadata>>();\n\tfor (const r of routes) {\n\t\tconst portKey =\n\t\t\tr.entrypointPort === null ? `entrypoint:${r.entrypointName}` : `port:${r.entrypointPort}`;\n\t\tconst key = r.wireProtocol === 'tcp' ? `tcp@${portKey}` : `http@${portKey}@${r.hostname}`;\n\t\tconst acc = seen.get(key);\n\t\tif (acc) acc.push(r);\n\t\telse seen.set(key, [r]);\n\t}\n\tfor (const [key, colliding] of seen) {\n\t\tif (colliding.length > 1) {\n\t\t\tconst ids = colliding.map((route) => route.dispatchFileId);\n\t\t\tconst first = colliding[0];\n\t\t\tif (key.startsWith('tcp@')) {\n\t\t\t\treturn new RouteCollision({\n\t\t\t\t\tmessage: routeCollisionMessage({\n\t\t\t\t\t\thostname: '',\n\t\t\t\t\t\tentrypoint: first?.entrypointName ?? '',\n\t\t\t\t\t\tdispatchIds: ids,\n\t\t\t\t\t\twireProtocol: 'tcp',\n\t\t\t\t\t}),\n\t\t\t\t\thostname: '',\n\t\t\t\t\tentrypoint: first?.entrypointName ?? '',\n\t\t\t\t\tdispatchIds: ids,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn new RouteCollision({\n\t\t\t\tmessage: routeCollisionMessage({\n\t\t\t\t\thostname: first?.hostname ?? '',\n\t\t\t\t\tentrypoint: first?.entrypointName ?? '',\n\t\t\t\t\tdispatchIds: ids,\n\t\t\t\t\twireProtocol: 'http',\n\t\t\t\t}),\n\t\t\t\thostname: first?.hostname ?? '',\n\t\t\t\tentrypoint: first?.entrypointName ?? '',\n\t\t\t\tdispatchIds: ids,\n\t\t\t});\n\t\t}\n\t}\n\treturn null;\n};\n\nconst routeCollisionMessage = (collision: {\n\treadonly hostname: string;\n\treadonly entrypoint: string;\n\treadonly dispatchIds: ReadonlyArray<string>;\n\treadonly wireProtocol: ResolvedWireProtocol;\n}): string => {\n\tconst ids = collision.dispatchIds.join(', ');\n\tif (collision.wireProtocol === 'tcp') {\n\t\treturn `router TCP route collision on entrypoint '${collision.entrypoint}' for dispatch ids: ${ids}`;\n\t}\n\treturn (\n\t\t`router route collision on entrypoint '${collision.entrypoint}' ` +\n\t\t`and hostname '${collision.hostname}' for dispatch ids: ${ids}`\n\t);\n};\n\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-provider.mjs","names":[],"sources":["../../../src/orchestrators/router/file-provider.ts"],"sourcesContent":["// File-provider config generator.\n//\n// Architecture invariant #1: file-provider ONLY, never docker-provider.\n// We materialize per-backend dispatch entries as YAML files in a\n// watched directory; Traefik's file-provider polls + reloads on\n// change. The docker-provider was rejected because the container IP\n// we need is the *shared-network* IP that lands AFTER `network connect`,\n// and the docker-provider captures the per-stack IP at first event\n// and never refreshes.\n//\n// One file per canonical route identity. Each file carries exactly\n// one router + one service (and references the shared CORS middleware\n// when `cors: true`). This layout means atomic add/remove of a route\n// is one file create/unlink — no merge step, no race.\n//\n// Two YAML shapes live under this renderer, discriminated by the\n// resolved route's `wireProtocol`:\n//\n// HTTP / h2c (Host-header dispatch on a shared port):\n//\n// http:\n// routers:\n// <id>-router:\n// rule: \"Host(`<hostname>`)\"\n// entryPoints: [\"<entrypointName>\"]\n// service: \"<id>-svc\"\n// middlewares: [\"devstack-cors\"] # when cors: true\n// services:\n// <id>-svc:\n// loadBalancer:\n// servers:\n// - url: \"http://<upstream-host>:<upstream-port>\"\n//\n// TCP (per-entrypoint-port dispatch; ONE backend per entrypoint):\n//\n// tcp:\n// routers:\n// <id>-router:\n// rule: \"HostSNI(`*`)\"\n// entryPoints: [\"<entrypointName>\"]\n// service: \"<id>-svc\"\n// services:\n// <id>-svc:\n// loadBalancer:\n// servers:\n// - address: \"<upstream-host>:<upstream-port>\"\n//\n// HostSNI(`*`) matches any incoming TCP connection on the entrypoint\n// (Traefik requires every TCP router to have a rule; `*` is the wildcard\n// that means \"any client\"). No CORS section — TCP isn't HTTP and the\n// shared CORS middleware lives under `http.middlewares`.\n\nimport { Effect } from 'effect';\n\nimport type { RoutableDecl } from '../../contracts/routable.ts';\nimport type { RosterHolder } from '../../substrate/cross-process.ts';\nimport type { Identity } from '../../substrate/identity.ts';\nimport { CORS_MIDDLEWARE_NAME } from './cors.ts';\nimport type { EntrypointRegistryShape } from './entrypoints.ts';\nimport { RouteCollision, RouterValidationError, type UnknownEntrypoint } from './errors.ts';\nimport { dispatchFileId, renderUrl, routerHostname } from './hostname.ts';\n\nexport const ROUTE_READINESS_HEADER = 'X-Devstack-Route-Id';\n\n/** Dispatch-file filename prefix. `10-` sorts behind the shared CORS\n * middleware (`00-`) so Traefik's file-provider loads the middleware\n * before any router that references it. Single source of truth for\n * the file-write side (`dispatchFilename`) and the file-read side\n * (`dispatchFileIdFromFilename`) — keep them in lockstep here. */\nconst DISPATCH_FILENAME_PREFIX = '10-';\n\n// ---------------------------------------------------------------------------\n// Resolved-route data structure\n// ---------------------------------------------------------------------------\n\n/** Resolved wire-protocol. `'tcp'` carries no hostname matcher and no\n * CORS. The router orchestrator's renderer + collision-detector branch\n * on this. */\nexport type ResolvedWireProtocol = 'http' | 'h2c' | 'tcp';\n\n/** A `RoutableDecl` resolved by the orchestrator: hostname minted,\n * dispatch-id stringified, entrypoint port resolved, upstream URL\n * resolved. This is the shape the file-provider renderer consumes —\n * decoupled from the resolution path so testing the renderer is\n * pure. */\nexport interface ResolvedRoute {\n\treadonly dispatchFileId: string;\n\t/** Minted hostname for HTTP routes. For TCP routes this is still\n\t * computed (it's the host string the URL surfaces to consumers),\n\t * but the Traefik router rule is `HostSNI(\\`*\\`)` — TCP dispatches\n\t * by entrypoint port, not Host. */\n\treadonly hostname: string;\n\treadonly entrypointName: string;\n\treadonly entrypointPort: number;\n\t/** Upstream URL string. For HTTP this is `http://<host>:<port>`; for\n\t * TCP it's still rendered as `tcp://<host>:<port>` so the renderer\n\t * can pull host+port back out for the `address:` field. */\n\treadonly upstreamUrl: string;\n\treadonly cors: boolean;\n\treadonly wireProtocol: ResolvedWireProtocol;\n}\n\nexport interface RouteCollisionMetadata {\n\treadonly dispatchFileId: string;\n\treadonly hostname: string;\n\treadonly entrypointName: string;\n\treadonly entrypointPort: number | null;\n\treadonly wireProtocol: ResolvedWireProtocol;\n}\n\nexport const ROUTER_ROUTE_LEASE_VERSION = 1;\n\nexport interface RouteLeaseMetadata {\n\treadonly version: typeof ROUTER_ROUTE_LEASE_VERSION;\n\treadonly routerProfileId: string;\n\treadonly app: string;\n\treadonly stack: string;\n\treadonly owner: RosterHolder;\n}\n\nexport interface DispatchRouteMetadata extends RouteCollisionMetadata {\n\treadonly lease: RouteLeaseMetadata | null;\n}\n\nexport type DispatchRouteDecodeReason =\n\t| 'missing-required-route-metadata'\n\t| 'unknown-route-lease-version'\n\t| 'invalid-route-lease-metadata';\n\nexport interface DispatchRouteDecodeDiagnostic {\n\treadonly _tag: 'DispatchRouteDecodeDiagnostic';\n\treadonly dispatchFileId: string;\n\treadonly reason: DispatchRouteDecodeReason;\n\treadonly detail: string;\n}\n\nexport type DispatchRouteParseResult =\n\t| {\n\t\t\treadonly _tag: 'valid';\n\t\t\treadonly route: DispatchRouteMetadata;\n\t\t\treadonly diagnostics: ReadonlyArray<DispatchRouteDecodeDiagnostic>;\n\t }\n\t| {\n\t\t\treadonly _tag: 'invalid';\n\t\t\treadonly dispatchFileId: string;\n\t\t\treadonly diagnostics: ReadonlyArray<DispatchRouteDecodeDiagnostic>;\n\t };\n\n/** Filename within the dispatch directory for a given file-id. The\n * prefix (`DISPATCH_FILENAME_PREFIX`) sorts behind the shared CORS\n * middleware (`00-`) so Traefik picks up the middleware before any\n * router referencing it. */\nexport const dispatchFilename = (fileId: string): string =>\n\t`${DISPATCH_FILENAME_PREFIX}${fileId}.yml`;\n\n// ---------------------------------------------------------------------------\n// Resolution — Routable + Identity + EntrypointRegistry + upstream → ResolvedRoute\n// ---------------------------------------------------------------------------\n\n/** Upstream-resolution interface the file-provider needs. The\n * orchestrator threads in concrete resolvers (`runtime/docker` for\n * containers, the port broker for host-loopback) without leaking\n * service knowledge. Architecture: \"router has no compiled-in\n * awareness of which services exist.\" */\nexport interface UpstreamResolver {\n\t/** For a container-kind upstream, return `(host, port)` where host\n\t * is the container's shared-network IP and port is the container\n\t * port from the plugin. Bounded retry lives inside the resolver\n\t * (architecture invariant #3). */\n\treadonly resolveContainer: (target: {\n\t\treadonly containerName: string;\n\t\treadonly containerPort: number;\n\t}) => Effect.Effect<{ readonly host: string; readonly port: number }, RouterValidationError>;\n\t/** For a host-loopback-kind upstream, return the bound loopback\n\t * port. The plugin owns port allocation and stamps the resolved\n\t * port into its Routable decl after acquire. */\n\treadonly resolveHostLoopback: (target: {\n\t\treadonly port: number;\n\t}) => Effect.Effect<{ readonly host: string; readonly port: number }, RouterValidationError>;\n}\n\n/** Resolve a single Routable into a ResolvedRoute. Pure-ish: the only\n * effectful bit is the upstream resolver (which talks to docker /\n * the port broker) and the entrypoint lookup.\n *\n * Takes the registry as a parameter rather than yielding the service\n * tag so the calling orchestrator can pre-bind it once at layer-\n * construction time, keeping the per-Routable hot path free of\n * Context lookups. */\nexport const resolveRoute = (\n\tidentity: Identity,\n\tdecl: RoutableDecl,\n\tregistry: EntrypointRegistryShape,\n\tupstreams: UpstreamResolver,\n): Effect.Effect<ResolvedRoute, RouterValidationError | UnknownEntrypoint> =>\n\tEffect.gen(function* () {\n\t\tconst entrypoint = yield* registry.byName(decl.endpointName);\n\t\tconst hostname = yield* routerHostname(identity, decl.dispatchId.role);\n\t\tconst fileId = yield* dispatchFileId({ identity, dispatch: decl.dispatchId });\n\t\tconst upstream =\n\t\t\tdecl.upstream.type === 'container'\n\t\t\t\t? yield* upstreams.resolveContainer({\n\t\t\t\t\t\tcontainerName: decl.upstream.containerName,\n\t\t\t\t\t\tcontainerPort: decl.upstream.containerPort,\n\t\t\t\t\t})\n\t\t\t\t: yield* upstreams.resolveHostLoopback({ port: decl.upstream.port });\n\t\tconst wireProtocol: ResolvedWireProtocol =\n\t\t\tdecl.wireProtocol === 'tcp' ? 'tcp' : decl.wireProtocol === 'h2c' ? 'h2c' : 'http';\n\t\t// The router contract says: a `wireProtocol: 'tcp'` decl MUST\n\t\t// reference an entrypoint whose protocol is also `'tcp'`. The\n\t\t// converse holds — HTTP/h2c decls must reference HTTP-family\n\t\t// entrypoints. Mismatch is a programming error in the plugin\n\t\t// author (caught here once, not at every render).\n\t\tconst expectFamily: 'tcp' | 'http' = wireProtocol === 'tcp' ? 'tcp' : 'http';\n\t\tconst entrypointFamily: 'tcp' | 'http' = entrypoint.protocol === 'tcp' ? 'tcp' : 'http';\n\t\tif (expectFamily !== entrypointFamily) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterValidationError({\n\t\t\t\t\tfield: 'entrypointName',\n\t\t\t\t\tvalue: entrypoint.name,\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t`wireProtocol family mismatch: decl is '${wireProtocol}' but ` +\n\t\t\t\t\t\t`entrypoint '${entrypoint.name}' is '${entrypoint.protocol}'`,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\t// Validate the upstream URL — defense in depth (the resolvers\n\t\t// already produce safe values, but this stops a future resolver\n\t\t// that returns junk from corrupting the YAML). TCP carries the\n\t\t// `tcp://` scheme so we differentiate from the HTTP path; the\n\t\t// renderer pulls host+port back out either way.\n\t\tconst upstreamUrl = renderUrl({\n\t\t\tprotocol: wireProtocol === 'tcp' ? 'tcp' : 'http',\n\t\t\thostname: upstream.host,\n\t\t\tport: upstream.port,\n\t\t});\n\t\tif (!/^(?:http|tcp):\\/\\/[A-Za-z0-9_.:-]+:\\d+$/.test(upstreamUrl)) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterValidationError({\n\t\t\t\t\tfield: 'upstreamUrl',\n\t\t\t\t\tvalue: upstreamUrl,\n\t\t\t\t\tdetail: 'expected http://<host>:<port> or tcp://<host>:<port>',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\t// CORS is HTTP-only — TCP decls don't carry the field. Render\n\t\t// uses this; the resolved shape carries `false` for TCP.\n\t\tconst cors = decl.wireProtocol === 'tcp' ? false : decl.cors;\n\t\treturn {\n\t\t\tdispatchFileId: fileId,\n\t\t\thostname,\n\t\t\tentrypointName: entrypoint.name,\n\t\t\tentrypointPort: entrypoint.port,\n\t\t\tupstreamUrl,\n\t\t\tcors,\n\t\t\twireProtocol,\n\t\t};\n\t});\n\n// ---------------------------------------------------------------------------\n// Render — ResolvedRoute → YAML body\n// ---------------------------------------------------------------------------\n\n/** Pull host+port out of an `<scheme>://<host>:<port>` URL. Used by the\n * TCP renderer to write the `address:` field. Safe because\n * `resolveRoute` validates the URL shape before we get here. */\nconst splitUpstream = (url: string): { host: string; port: string } => {\n\tconst stripped = url.replace(/^(?:http|tcp):\\/\\//, '');\n\tconst lastColon = stripped.lastIndexOf(':');\n\treturn {\n\t\thost: stripped.slice(0, lastColon),\n\t\tport: stripped.slice(lastColon + 1),\n\t};\n};\n\nconst renderLeaseHeader = (lease: RouteLeaseMetadata): ReadonlyArray<string> => [\n\t`# routeLeaseVersion: ${lease.version}`,\n\t`# routerProfileId: ${lease.routerProfileId}`,\n\t`# ownerApp: ${lease.app}`,\n\t`# ownerStack: ${lease.stack}`,\n\t`# ownerPid: ${lease.owner.pid}`,\n\t`# ownerStartTime: ${lease.owner.startTime}`,\n\t`# ownerHostname: ${lease.owner.hostname}`,\n\t`# ownerClaimedAt: ${lease.owner.claimedAt}`,\n\t`# ownerHeartbeatAt: ${lease.owner.heartbeatAt}`,\n\t`# ownerIntent: ${lease.owner.intent}`,\n];\n\n/** Render the YAML body for a single resolved route. Hand-rolled\n * (same rationale as `cors.ts`): static, controlled, byte-stable so\n * no-op rewrites don't wake the watcher. */\nexport const renderRouteYaml = (route: ResolvedRoute, lease: RouteLeaseMetadata): string => {\n\tif (route.wireProtocol === 'tcp') return renderTcpRouteYaml(route, lease);\n\treturn renderHttpRouteYaml(route, lease);\n};\n\nconst routeReadinessMiddlewareName = (route: ResolvedRoute): string =>\n\t`${route.dispatchFileId}-route-ready`;\n\nconst renderHttpRouteYaml = (route: ResolvedRoute, lease: RouteLeaseMetadata): string => {\n\tconst middlewares = [\n\t\trouteReadinessMiddlewareName(route),\n\t\t...(route.cors ? [CORS_MIDDLEWARE_NAME] : []),\n\t];\n\tconst schemeHint =\n\t\troute.wireProtocol === 'h2c'\n\t\t\t? ` # h2c upstream — gRPC-friendly cleartext HTTP/2.\\n`\n\t\t\t: '';\n\treturn [\n\t\t`# Auto-generated by devstack router orchestrator. Do not edit by hand.`,\n\t\t`# dispatchFileId: ${route.dispatchFileId}`,\n\t\t`# wireProtocol: ${route.wireProtocol}`,\n\t\t`# entrypointName: ${route.entrypointName}`,\n\t\t`# entrypointPort: ${route.entrypointPort}`,\n\t\t`# hostname: ${route.hostname}`,\n\t\t...renderLeaseHeader(lease),\n\t\t`http:`,\n\t\t` routers:`,\n\t\t` ${route.dispatchFileId}-router:`,\n\t\t` rule: \"Host(\\`${route.hostname}\\`)\"`,\n\t\t` entryPoints: [\"${route.entrypointName}\"]`,\n\t\t` service: \"${route.dispatchFileId}-svc\"`,\n\t\t` middlewares: [${middlewares.map((name) => `\"${name}\"`).join(', ')}]`,\n\t\t` middlewares:`,\n\t\t` ${routeReadinessMiddlewareName(route)}:`,\n\t\t` headers:`,\n\t\t` customResponseHeaders:`,\n\t\t` ${ROUTE_READINESS_HEADER}: \"${route.dispatchFileId}\"`,\n\t\t` services:`,\n\t\t` ${route.dispatchFileId}-svc:`,\n\t\t` loadBalancer:`,\n\t\tschemeHint.length > 0 ? schemeHint.trimEnd() : null,\n\t\t` servers:`,\n\t\t` - url: \"${route.upstreamUrl}\"`,\n\t\t``,\n\t]\n\t\t.filter((line): line is string => line !== null)\n\t\t.join('\\n');\n};\n\nconst renderTcpRouteYaml = (route: ResolvedRoute, lease: RouteLeaseMetadata): string => {\n\tconst { host, port } = splitUpstream(route.upstreamUrl);\n\treturn [\n\t\t`# Auto-generated by devstack router orchestrator. Do not edit by hand.`,\n\t\t`# dispatchFileId: ${route.dispatchFileId}`,\n\t\t`# wireProtocol: tcp`,\n\t\t`# entrypointName: ${route.entrypointName}`,\n\t\t`# entrypointPort: ${route.entrypointPort}`,\n\t\t`# hostname: ${route.hostname}`,\n\t\t`# tcpDispatch: entrypoint-port dispatch; HostSNI wildcard`,\n\t\t...renderLeaseHeader(lease),\n\t\t`tcp:`,\n\t\t` routers:`,\n\t\t` ${route.dispatchFileId}-router:`,\n\t\t` rule: \"HostSNI(\\`*\\`)\"`,\n\t\t` entryPoints: [\"${route.entrypointName}\"]`,\n\t\t` service: \"${route.dispatchFileId}-svc\"`,\n\t\t` services:`,\n\t\t` ${route.dispatchFileId}-svc:`,\n\t\t` loadBalancer:`,\n\t\t` servers:`,\n\t\t` - address: \"${host}:${port}\"`,\n\t\t``,\n\t].join('\\n');\n};\n\nexport const dispatchFileIdFromFilename = (filename: string): string | null => {\n\t// Escape `DISPATCH_FILENAME_PREFIX` for use inside a RegExp literal —\n\t// today it's a plain `10-` so no regex metacharacters are at play,\n\t// but the escape makes the read side robust to future prefix changes.\n\tconst escapedPrefix = DISPATCH_FILENAME_PREFIX.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\tconst match = new RegExp(`^${escapedPrefix}(.+)\\\\.yml$`).exec(filename);\n\treturn match?.[1] ?? null;\n};\n\nconst commentValue = (body: string, key: string): string | null => {\n\tconst escaped = key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\tconst match = new RegExp(`^# ${escaped}: (.*)$`, 'm').exec(body);\n\treturn match?.[1]?.trim() ?? null;\n};\n\nconst matchValue = (body: string, pattern: RegExp): string | null =>\n\tpattern.exec(body)?.[1]?.trim() ?? null;\n\nconst commentNumber = (body: string, key: string): number | null => {\n\tconst raw = commentValue(body, key);\n\tif (raw === null || !/^\\d+$/.test(raw)) return null;\n\treturn Number.parseInt(raw, 10);\n};\n\nconst parseWireProtocol = (body: string): ResolvedWireProtocol | null => {\n\tconst fromComment = commentValue(body, 'wireProtocol');\n\tif (fromComment === 'http' || fromComment === 'h2c' || fromComment === 'tcp') {\n\t\treturn fromComment;\n\t}\n\tif (/^tcp:/m.test(body)) return 'tcp';\n\tif (/^http:/m.test(body)) return 'http';\n\treturn null;\n};\n\nconst hasRouteLeaseMetadata = (body: string): boolean =>\n\t[\n\t\t'routeLeaseVersion',\n\t\t'routerProfileId',\n\t\t'ownerApp',\n\t\t'ownerStack',\n\t\t'ownerPid',\n\t\t'ownerStartTime',\n\t\t'ownerHostname',\n\t\t'ownerClaimedAt',\n\t\t'ownerHeartbeatAt',\n\t\t'ownerIntent',\n\t].some((key) => commentValue(body, key) !== null);\n\nconst parseRouteLeaseMetadata = (\n\tbody: string,\n\tdispatchFileId: string,\n): {\n\treadonly lease: RouteLeaseMetadata | null;\n\treadonly diagnostic: DispatchRouteDecodeDiagnostic | null;\n} => {\n\tif (!hasRouteLeaseMetadata(body)) return { lease: null, diagnostic: null };\n\tconst rawVersion = commentValue(body, 'routeLeaseVersion');\n\tconst version = commentNumber(body, 'routeLeaseVersion');\n\tconst routerProfileId = commentValue(body, 'routerProfileId');\n\tconst app = commentValue(body, 'ownerApp');\n\tconst stack = commentValue(body, 'ownerStack');\n\tconst pid = commentNumber(body, 'ownerPid');\n\tconst startTime = commentNumber(body, 'ownerStartTime');\n\tconst hostname = commentValue(body, 'ownerHostname');\n\tconst claimedAt = commentNumber(body, 'ownerClaimedAt');\n\tconst heartbeatAt = commentNumber(body, 'ownerHeartbeatAt');\n\tconst intent = commentValue(body, 'ownerIntent');\n\tif (version !== ROUTER_ROUTE_LEASE_VERSION) {\n\t\treturn {\n\t\t\tlease: null,\n\t\t\tdiagnostic: {\n\t\t\t\t_tag: 'DispatchRouteDecodeDiagnostic',\n\t\t\t\tdispatchFileId,\n\t\t\t\treason: 'unknown-route-lease-version',\n\t\t\t\tdetail: `expected routeLeaseVersion ${ROUTER_ROUTE_LEASE_VERSION}, got ${rawVersion ?? '<missing>'}`,\n\t\t\t},\n\t\t};\n\t}\n\tif (\n\t\trouterProfileId === null ||\n\t\tapp === null ||\n\t\tstack === null ||\n\t\tpid === null ||\n\t\tstartTime === null ||\n\t\thostname === null ||\n\t\tclaimedAt === null ||\n\t\theartbeatAt === null ||\n\t\t(intent !== 'normal' && intent !== 'snapshot')\n\t) {\n\t\treturn {\n\t\t\tlease: null,\n\t\t\tdiagnostic: {\n\t\t\t\t_tag: 'DispatchRouteDecodeDiagnostic',\n\t\t\t\tdispatchFileId,\n\t\t\t\treason: 'invalid-route-lease-metadata',\n\t\t\t\tdetail: 'route lease metadata is incomplete or malformed',\n\t\t\t},\n\t\t};\n\t}\n\treturn {\n\t\tlease: {\n\t\t\tversion: ROUTER_ROUTE_LEASE_VERSION,\n\t\t\trouterProfileId,\n\t\t\tapp,\n\t\t\tstack,\n\t\t\towner: {\n\t\t\t\tpid,\n\t\t\t\tstartTime,\n\t\t\t\thostname,\n\t\t\t\tclaimedAt,\n\t\t\t\theartbeatAt,\n\t\t\t\tintent,\n\t\t\t},\n\t\t},\n\t\tdiagnostic: null,\n\t};\n};\n\nexport const parseDispatchRouteFile = (\n\tbody: string,\n\tfallbackDispatchFileId: string | null = null,\n): DispatchRouteParseResult => {\n\tconst dispatchFileId = commentValue(body, 'dispatchFileId') ?? fallbackDispatchFileId;\n\tconst wireProtocol = parseWireProtocol(body);\n\tconst entrypointName =\n\t\tcommentValue(body, 'entrypointName') ?? matchValue(body, /entryPoints: \\[\"([^\"]+)\"\\]/);\n\tconst hostname =\n\t\tcommentValue(body, 'hostname') ??\n\t\tmatchValue(body, /Host\\(`([^`]+)`\\)/) ??\n\t\t(wireProtocol === 'tcp' ? '' : null);\n\tconst portRaw = commentValue(body, 'entrypointPort');\n\tconst entrypointPort =\n\t\tportRaw === null || !/^\\d+$/.test(portRaw) ? null : Number.parseInt(portRaw, 10);\n\n\tif (\n\t\tdispatchFileId === null ||\n\t\twireProtocol === null ||\n\t\tentrypointName === null ||\n\t\thostname === null\n\t) {\n\t\tconst protectedDispatchFileId = dispatchFileId ?? '<unknown>';\n\t\treturn {\n\t\t\t_tag: 'invalid',\n\t\t\tdispatchFileId: protectedDispatchFileId,\n\t\t\tdiagnostics: [\n\t\t\t\t{\n\t\t\t\t\t_tag: 'DispatchRouteDecodeDiagnostic',\n\t\t\t\t\tdispatchFileId: protectedDispatchFileId,\n\t\t\t\t\treason: 'missing-required-route-metadata',\n\t\t\t\t\tdetail:\n\t\t\t\t\t\t'route file is missing dispatchFileId, wireProtocol/http|tcp block, entrypointName, or hostname metadata',\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\tconst leaseResult = parseRouteLeaseMetadata(body, dispatchFileId);\n\n\treturn {\n\t\t_tag: 'valid',\n\t\troute: {\n\t\t\tdispatchFileId,\n\t\t\thostname,\n\t\t\tentrypointName,\n\t\t\tentrypointPort,\n\t\t\twireProtocol,\n\t\t\tlease: leaseResult.lease,\n\t\t},\n\t\tdiagnostics: leaseResult.diagnostic === null ? [] : [leaseResult.diagnostic],\n\t};\n};\n\nexport const parseDispatchRouteMetadata = (\n\tbody: string,\n\tfallbackDispatchFileId: string | null = null,\n): DispatchRouteMetadata | null => {\n\tconst parsed = parseDispatchRouteFile(body, fallbackDispatchFileId);\n\treturn parsed._tag === 'valid' ? parsed.route : null;\n};\n\n// ---------------------------------------------------------------------------\n// Collision detection\n// ---------------------------------------------------------------------------\n\n/** Assert no two resolved routes share dispatch keys.\n *\n * - HTTP routes collide on `(entrypoint, hostname)` — two HTTP backends\n * on the same entrypoint port can coexist via different Host headers,\n * so the Host *is* part of the key.\n * - TCP routes collide on `entrypoint` alone — TCP has no Host header,\n * so an entrypoint can serve exactly ONE backend. Two TCP decls on\n * the same entrypoint are an unambiguous error (parallel stacks of\n * a TCP service share the host port and would clobber each other).\n *\n * Architecture invariant #7 — distinct identity → distinct dispatch\n * URL. The TCP arm is the new clause; HTTP arm unchanged. */\nexport const detectCollisions = (\n\troutes: ReadonlyArray<RouteCollisionMetadata>,\n): RouteCollision | null => {\n\tconst seen = new Map<string, Array<RouteCollisionMetadata>>();\n\tfor (const r of routes) {\n\t\tconst portKey =\n\t\t\tr.entrypointPort === null ? `entrypoint:${r.entrypointName}` : `port:${r.entrypointPort}`;\n\t\tconst key = r.wireProtocol === 'tcp' ? `tcp@${portKey}` : `http@${portKey}@${r.hostname}`;\n\t\tconst acc = seen.get(key);\n\t\tif (acc) acc.push(r);\n\t\telse seen.set(key, [r]);\n\t}\n\tfor (const [key, colliding] of seen) {\n\t\tif (colliding.length > 1) {\n\t\t\tconst ids = colliding.map((route) => route.dispatchFileId);\n\t\t\tconst first = colliding[0];\n\t\t\tif (key.startsWith('tcp@')) {\n\t\t\t\treturn new RouteCollision({\n\t\t\t\t\tmessage: routeCollisionMessage({\n\t\t\t\t\t\thostname: '',\n\t\t\t\t\t\tentrypoint: first?.entrypointName ?? '',\n\t\t\t\t\t\tdispatchIds: ids,\n\t\t\t\t\t\twireProtocol: 'tcp',\n\t\t\t\t\t}),\n\t\t\t\t\thostname: '',\n\t\t\t\t\tentrypoint: first?.entrypointName ?? '',\n\t\t\t\t\tdispatchIds: ids,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn new RouteCollision({\n\t\t\t\tmessage: routeCollisionMessage({\n\t\t\t\t\thostname: first?.hostname ?? '',\n\t\t\t\t\tentrypoint: first?.entrypointName ?? '',\n\t\t\t\t\tdispatchIds: ids,\n\t\t\t\t\twireProtocol: 'http',\n\t\t\t\t}),\n\t\t\t\thostname: first?.hostname ?? '',\n\t\t\t\tentrypoint: first?.entrypointName ?? '',\n\t\t\t\tdispatchIds: ids,\n\t\t\t});\n\t\t}\n\t}\n\treturn null;\n};\n\nconst routeCollisionMessage = (collision: {\n\treadonly hostname: string;\n\treadonly entrypoint: string;\n\treadonly dispatchIds: ReadonlyArray<string>;\n\treadonly wireProtocol: ResolvedWireProtocol;\n}): string => {\n\tconst ids = collision.dispatchIds.join(', ');\n\tif (collision.wireProtocol === 'tcp') {\n\t\treturn `router TCP route collision on entrypoint '${collision.entrypoint}' for dispatch ids: ${ids}`;\n\t}\n\treturn (\n\t\t`router route collision on entrypoint '${collision.entrypoint}' ` +\n\t\t`and hostname '${collision.hostname}' for dispatch ids: ${ids}`\n\t);\n};\n"],"mappings":";;;;;;AA8DA,MAAa,yBAAyB;;;;;;AAOtC,MAAM,2BAA2B;;;;;AAmFjC,MAAa,oBAAoB,WAChC,GAAG,2BAA2B,OAAO;;;;;;;;;AAoCtC,MAAa,gBACZ,UACA,MACA,UACA,cAEA,OAAO,IAAI,aAAa;CACvB,MAAM,aAAa,OAAO,SAAS,OAAO,KAAK,YAAY;CAC3D,MAAM,WAAW,OAAO,eAAe,UAAU,KAAK,WAAW,IAAI;CACrE,MAAM,SAAS,OAAO,eAAe;EAAE;EAAU,UAAU,KAAK;CAAW,CAAC;CAC5E,MAAM,WACL,KAAK,SAAS,SAAS,cACpB,OAAO,UAAU,iBAAiB;EAClC,eAAe,KAAK,SAAS;EAC7B,eAAe,KAAK,SAAS;CAC9B,CAAC,IACA,OAAO,UAAU,oBAAoB,EAAE,MAAM,KAAK,SAAS,KAAK,CAAC;CACrE,MAAM,eACL,KAAK,iBAAiB,QAAQ,QAAQ,KAAK,iBAAiB,QAAQ,QAAQ;CAQ7E,KAFqC,iBAAiB,QAAQ,QAAQ,aAC7B,WAAW,aAAa,QAAQ,QAAQ,SAEhF,OAAO,OAAO,OAAO,KACpB,IAAI,sBAAsB;EACzB,OAAO;EACP,OAAO,WAAW;EAClB,QACC,0CAA0C,aAAa,oBACxC,WAAW,KAAK,QAAQ,WAAW,SAAS;CAC7D,CAAC,CACF;CAOD,MAAM,cAAc,UAAU;EAC7B,UAAU,iBAAiB,QAAQ,QAAQ;EAC3C,UAAU,SAAS;EACnB,MAAM,SAAS;CAChB,CAAC;CACD,IAAI,CAAC,0CAA0C,KAAK,WAAW,GAC9D,OAAO,OAAO,OAAO,KACpB,IAAI,sBAAsB;EACzB,OAAO;EACP,OAAO;EACP,QAAQ;CACT,CAAC,CACF;CAID,MAAM,OAAO,KAAK,iBAAiB,QAAQ,QAAQ,KAAK;CACxD,OAAO;EACN,gBAAgB;EAChB;EACA,gBAAgB,WAAW;EAC3B,gBAAgB,WAAW;EAC3B;EACA;EACA;CACD;AACD,CAAC;;;;AASF,MAAM,iBAAiB,QAAgD;CACtE,MAAM,WAAW,IAAI,QAAQ,sBAAsB,EAAE;CACrD,MAAM,YAAY,SAAS,YAAY,GAAG;CAC1C,OAAO;EACN,MAAM,SAAS,MAAM,GAAG,SAAS;EACjC,MAAM,SAAS,MAAM,YAAY,CAAC;CACnC;AACD;AAEA,MAAM,qBAAqB,UAAqD;CAC/E,wBAAwB,MAAM;CAC9B,sBAAsB,MAAM;CAC5B,eAAe,MAAM;CACrB,iBAAiB,MAAM;CACvB,eAAe,MAAM,MAAM;CAC3B,qBAAqB,MAAM,MAAM;CACjC,oBAAoB,MAAM,MAAM;CAChC,qBAAqB,MAAM,MAAM;CACjC,uBAAuB,MAAM,MAAM;CACnC,kBAAkB,MAAM,MAAM;AAC/B;;;;AAKA,MAAa,mBAAmB,OAAsB,UAAsC;CAC3F,IAAI,MAAM,iBAAiB,OAAO,OAAO,mBAAmB,OAAO,KAAK;CACxE,OAAO,oBAAoB,OAAO,KAAK;AACxC;AAEA,MAAM,gCAAgC,UACrC,GAAG,MAAM,eAAe;AAEzB,MAAM,uBAAuB,OAAsB,UAAsC;CACxF,MAAM,cAAc,CACnB,6BAA6B,KAAK,GAClC,GAAI,MAAM,OAAO,CAAC,oBAAoB,IAAI,CAAC,CAC5C;CACA,MAAM,aACL,MAAM,iBAAiB,QACpB,+DACA;CACJ,OAAO;EACN;EACA,qBAAqB,MAAM;EAC3B,mBAAmB,MAAM;EACzB,qBAAqB,MAAM;EAC3B,qBAAqB,MAAM;EAC3B,eAAe,MAAM;EACrB,GAAG,kBAAkB,KAAK;EAC1B;EACA;EACA,OAAO,MAAM,eAAe;EAC5B,uBAAuB,MAAM,SAAS;EACtC,wBAAwB,MAAM,eAAe;EAC7C,mBAAmB,MAAM,eAAe;EACxC,uBAAuB,YAAY,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;EACzE;EACA,OAAO,6BAA6B,KAAK,EAAE;EAC3C;EACA;EACA,aAAa,uBAAuB,KAAK,MAAM,eAAe;EAC9D;EACA,OAAO,MAAM,eAAe;EAC5B;EACA,WAAW,SAAS,IAAI,WAAW,QAAQ,IAAI;EAC/C;EACA,qBAAqB,MAAM,YAAY;EACvC;CACD,CAAC,CACC,QAAQ,SAAyB,SAAS,IAAI,CAAC,CAC/C,KAAK,IAAI;AACZ;AAEA,MAAM,sBAAsB,OAAsB,UAAsC;CACvF,MAAM,EAAE,MAAM,SAAS,cAAc,MAAM,WAAW;CACtD,OAAO;EACN;EACA,qBAAqB,MAAM;EAC3B;EACA,qBAAqB,MAAM;EAC3B,qBAAqB,MAAM;EAC3B,eAAe,MAAM;EACrB;EACA,GAAG,kBAAkB,KAAK;EAC1B;EACA;EACA,OAAO,MAAM,eAAe;EAC5B;EACA,wBAAwB,MAAM,eAAe;EAC7C,mBAAmB,MAAM,eAAe;EACxC;EACA,OAAO,MAAM,eAAe;EAC5B;EACA;EACA,yBAAyB,KAAK,GAAG,KAAK;EACtC;CACD,CAAC,CAAC,KAAK,IAAI;AACZ;AAEA,MAAa,8BAA8B,aAAoC;CAI9E,MAAM,gBAAgB,yBAAyB,QAAQ,uBAAuB,MAAM;CAEpF,OADc,IAAI,OAAO,IAAI,cAAc,YAAY,CAAC,CAAC,KAAK,QACnD,CAAC,GAAG,MAAM;AACtB;AAEA,MAAM,gBAAgB,MAAc,QAA+B;CAClE,MAAM,UAAU,IAAI,QAAQ,uBAAuB,MAAM;CAEzD,OADc,IAAI,OAAO,MAAM,QAAQ,UAAU,GAAG,CAAC,CAAC,KAAK,IAChD,CAAC,GAAG,EAAE,EAAE,KAAK,KAAK;AAC9B;AAEA,MAAM,cAAc,MAAc,YACjC,QAAQ,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,KAAK;AAEpC,MAAM,iBAAiB,MAAc,QAA+B;CACnE,MAAM,MAAM,aAAa,MAAM,GAAG;CAClC,IAAI,QAAQ,QAAQ,CAAC,QAAQ,KAAK,GAAG,GAAG,OAAO;CAC/C,OAAO,OAAO,SAAS,KAAK,EAAE;AAC/B;AAEA,MAAM,qBAAqB,SAA8C;CACxE,MAAM,cAAc,aAAa,MAAM,cAAc;CACrD,IAAI,gBAAgB,UAAU,gBAAgB,SAAS,gBAAgB,OACtE,OAAO;CAER,IAAI,SAAS,KAAK,IAAI,GAAG,OAAO;CAChC,IAAI,UAAU,KAAK,IAAI,GAAG,OAAO;CACjC,OAAO;AACR;AAEA,MAAM,yBAAyB,SAC9B;CACC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD,CAAC,CAAC,MAAM,QAAQ,aAAa,MAAM,GAAG,MAAM,IAAI;AAEjD,MAAM,2BACL,MACA,mBAII;CACJ,IAAI,CAAC,sBAAsB,IAAI,GAAG,OAAO;EAAE,OAAO;EAAM,YAAY;CAAK;CACzE,MAAM,aAAa,aAAa,MAAM,mBAAmB;CACzD,MAAM,UAAU,cAAc,MAAM,mBAAmB;CACvD,MAAM,kBAAkB,aAAa,MAAM,iBAAiB;CAC5D,MAAM,MAAM,aAAa,MAAM,UAAU;CACzC,MAAM,QAAQ,aAAa,MAAM,YAAY;CAC7C,MAAM,MAAM,cAAc,MAAM,UAAU;CAC1C,MAAM,YAAY,cAAc,MAAM,gBAAgB;CACtD,MAAM,WAAW,aAAa,MAAM,eAAe;CACnD,MAAM,YAAY,cAAc,MAAM,gBAAgB;CACtD,MAAM,cAAc,cAAc,MAAM,kBAAkB;CAC1D,MAAM,SAAS,aAAa,MAAM,aAAa;CAC/C,IAAI,YAAA,GACH,OAAO;EACN,OAAO;EACP,YAAY;GACX,MAAM;GACN;GACA,QAAQ;GACR,QAAQ,qCAAiE,cAAc;EACxF;CACD;CAED,IACC,oBAAoB,QACpB,QAAQ,QACR,UAAU,QACV,QAAQ,QACR,cAAc,QACd,aAAa,QACb,cAAc,QACd,gBAAgB,QACf,WAAW,YAAY,WAAW,YAEnC,OAAO;EACN,OAAO;EACP,YAAY;GACX,MAAM;GACN;GACA,QAAQ;GACR,QAAQ;EACT;CACD;CAED,OAAO;EACN,OAAO;GACN,SAAA;GACA;GACA;GACA;GACA,OAAO;IACN;IACA;IACA;IACA;IACA;IACA;GACD;EACD;EACA,YAAY;CACb;AACD;AAEA,MAAa,0BACZ,MACA,yBAAwC,SACV;CAC9B,MAAM,iBAAiB,aAAa,MAAM,gBAAgB,KAAK;CAC/D,MAAM,eAAe,kBAAkB,IAAI;CAC3C,MAAM,iBACL,aAAa,MAAM,gBAAgB,KAAK,WAAW,MAAM,4BAA4B;CACtF,MAAM,WACL,aAAa,MAAM,UAAU,KAC7B,WAAW,MAAM,mBAAmB,MACnC,iBAAiB,QAAQ,KAAK;CAChC,MAAM,UAAU,aAAa,MAAM,gBAAgB;CACnD,MAAM,iBACL,YAAY,QAAQ,CAAC,QAAQ,KAAK,OAAO,IAAI,OAAO,OAAO,SAAS,SAAS,EAAE;CAEhF,IACC,mBAAmB,QACnB,iBAAiB,QACjB,mBAAmB,QACnB,aAAa,MACZ;EACD,MAAM,0BAA0B,kBAAkB;EAClD,OAAO;GACN,MAAM;GACN,gBAAgB;GAChB,aAAa,CACZ;IACC,MAAM;IACN,gBAAgB;IAChB,QAAQ;IACR,QACC;GACF,CACD;EACD;CACD;CACA,MAAM,cAAc,wBAAwB,MAAM,cAAc;CAEhE,OAAO;EACN,MAAM;EACN,OAAO;GACN;GACA;GACA;GACA;GACA;GACA,OAAO,YAAY;EACpB;EACA,aAAa,YAAY,eAAe,OAAO,CAAC,IAAI,CAAC,YAAY,UAAU;CAC5E;AACD;;;;;;;;;;;;;AA0BA,MAAa,oBACZ,WAC2B;CAC3B,MAAM,uBAAO,IAAI,IAA2C;CAC5D,KAAK,MAAM,KAAK,QAAQ;EACvB,MAAM,UACL,EAAE,mBAAmB,OAAO,cAAc,EAAE,mBAAmB,QAAQ,EAAE;EAC1E,MAAM,MAAM,EAAE,iBAAiB,QAAQ,OAAO,YAAY,QAAQ,QAAQ,GAAG,EAAE;EAC/E,MAAM,MAAM,KAAK,IAAI,GAAG;EACxB,IAAI,KAAK,IAAI,KAAK,CAAC;OACd,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC;CACvB;CACA,KAAK,MAAM,CAAC,KAAK,cAAc,MAC9B,IAAI,UAAU,SAAS,GAAG;EACzB,MAAM,MAAM,UAAU,KAAK,UAAU,MAAM,cAAc;EACzD,MAAM,QAAQ,UAAU;EACxB,IAAI,IAAI,WAAW,MAAM,GACxB,OAAO,IAAI,eAAe;GACzB,SAAS,sBAAsB;IAC9B,UAAU;IACV,YAAY,OAAO,kBAAkB;IACrC,aAAa;IACb,cAAc;GACf,CAAC;GACD,UAAU;GACV,YAAY,OAAO,kBAAkB;GACrC,aAAa;EACd,CAAC;EAEF,OAAO,IAAI,eAAe;GACzB,SAAS,sBAAsB;IAC9B,UAAU,OAAO,YAAY;IAC7B,YAAY,OAAO,kBAAkB;IACrC,aAAa;IACb,cAAc;GACf,CAAC;GACD,UAAU,OAAO,YAAY;GAC7B,YAAY,OAAO,kBAAkB;GACrC,aAAa;EACd,CAAC;CACF;CAED,OAAO;AACR;AAEA,MAAM,yBAAyB,cAKjB;CACb,MAAM,MAAM,UAAU,YAAY,KAAK,IAAI;CAC3C,IAAI,UAAU,iBAAiB,OAC9B,OAAO,6CAA6C,UAAU,WAAW,sBAAsB;CAEhG,OACC,yCAAyC,UAAU,WAAW,kBAC7C,UAAU,SAAS,sBAAsB;AAE5D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hostname.mjs","names":[],"sources":["../../../src/orchestrators/router/hostname.ts"],"sourcesContent":["// Hostname + dispatch-id minting helpers.\n//\n// Architecture distilled-doc §\"Outputs / capabilities provided\":\n//\n// - default (\"main\") stack: `<service>.<app>.localhost`\n// - every other stack: `<service>.<stack>.<app>.localhost`\n//\n// \"service\" here is the dispatch-id's `role` segment — NOT the\n// plugin's name. Plugins emit a `(serviceKey, role)` DispatchId; we\n// fold the role into the hostname, since the role is the\n// user-meaningful side (\"api\", \"key-server\", \"indexer-metrics\") and\n// the service key already encodes plugin + app + stack on the\n// dispatch-file side.\n//\n// Architecture invariants:\n// #7 — Distinct `(app, stack, service)` triples MUST produce\n// distinct hostnames AND distinct dispatch ids. The default-\n// stack omission is intentional UX; every other stack\n// includes the stack segment so parallel stacks of the same\n// app never collide.\n// #8 — Hostname service labels fold dots to a label-safe separator.\n// Dispatch ids are minted from a canonical source tuple plus a\n// SHA-256 digest, so lossy readable folding cannot collide.\n// #13 — User-influenceable strings are validated against a\n// conservative character set before render.\n//\n// As of B1 (boundary-repair): pure URL composition + hostname minting\n// live in `substrate/runtime/routed-url.ts` so L2 plugins can call\n// them without reaching across the L2→L3 boundary. This module re-\n// exports the substrate primitives, owns dispatch-file id minting\n// (router-orchestrator-specific), and provides a `routerHostname`\n// adapter that projects substrate's `HostnameValidationError` into the\n// router's `RouterValidationError` union for intra-L3 callers\n// (file-provider, resolveRoute).\n\nimport { createHash } from 'node:crypto';\n\nimport { Effect } from 'effect';\nimport type { DispatchId } from '../../contracts/routable.ts';\nimport type { Identity } from '../../substrate/identity.ts';\nimport {\n\tDEFAULT_STACK,\n\tHostnameValidationError,\n\tnormalizeServiceSegment,\n\trenderUrl,\n\troutedHostname,\n} from '../../substrate/runtime/routed-url.ts';\nimport { RouterValidationError } from './errors.ts';\n\n// Re-export substrate-blind primitives for intra-L3 callers that\n// continue to import from this module path.\nexport { DEFAULT_STACK, normalizeServiceSegment, renderUrl };\n\n// The readable prefix is capped, and the SHA-256 suffix is fixed size.\nconst MAX_DISPATCH_ID_LEN = 140;\nconst MAX_DISPATCH_ID_READABLE_LEN = 48;\n\n/** Build the human-readable prefix of a dispatch id. This is not the\n * identity key; uniqueness comes from the hash over the canonical\n * tuple in `dispatchFileId`. */\nexport const normalizeDispatchSegment = (raw: string): string =>\n\traw\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t.replace(/-+/g, '-')\n\t\t.replace(/^-|-$/g, '') || 'route';\n\n// ---------------------------------------------------------------------------\n// Hostname minting — orchestrator-internal adapter\n// ---------------------------------------------------------------------------\n\n/** Project the substrate-blind hostname error into the router error\n * union. The shape is identical (tagged class with field/value/detail);\n * this exists so intra-L3 composers (file-provider, resolveRoute) can\n * keep returning the existing `RouterError` union without a per-call\n * `mapError` at every call site. */\nconst liftHostnameError = (cause: HostnameValidationError): RouterValidationError =>\n\tnew RouterValidationError({\n\t\tfield: cause.field,\n\t\tvalue: cause.value,\n\t\tdetail: cause.detail,\n\t});\n\n/** Intra-L3 alias for substrate's `routedHostname` that maps the\n * substrate error onto the router orchestrator's `RouterError` union.\n *\n * L2 plugins MUST NOT import this — they import `routedHostname` from\n * `substrate/runtime/routed-url.ts` directly. This adapter exists only\n * for the orchestrator's own composers. */\nexport const routerHostname = (\n\tidentity: Identity,\n\trole: string,\n): Effect.Effect<string, RouterValidationError> =>\n\troutedHostname(identity, role).pipe(Effect.mapError(liftHostnameError));\n\n// ---------------------------------------------------------------------------\n// Dispatch-file id minting\n// ---------------------------------------------------------------------------\n\nexport interface DispatchFileIdInputs {\n\treadonly identity: Identity;\n\treadonly dispatch: DispatchId;\n}\n\nconst canonicalDispatchTuple = (inputs: DispatchFileIdInputs): string =>\n\tJSON.stringify([\n\t\t'devstack-router-dispatch-v1',\n\t\tinputs.identity.app,\n\t\tinputs.identity.stack,\n\t\tinputs.dispatch.serviceKey,\n\t\tinputs.dispatch.role,\n\t]);\n\nconst truncateReadable = (value: string): string =>\n\t(value.length <= MAX_DISPATCH_ID_READABLE_LEN\n\t\t? value\n\t\t: value.slice(0, MAX_DISPATCH_ID_READABLE_LEN).replace(/-+$/g, '')) || 'route';\n\n/** Mint a dispatch-file id from the full route identity tuple. The id\n * is used as the file name in the global file-provider directory\n * (one file per backend), and as the Traefik router/service name\n * inside the file.\n *\n * Format: `r1-<readable-prefix>-<sha256>`.\n *\n * The readable prefix is intentionally lossy; it exists only to make\n * directory listings diagnosable. The SHA-256 suffix is over the\n * canonical `(version, app, stack, serviceKey, role)` tuple, so\n * raw underscores, separator strings, case, and dot/hyphen folding do\n * not affect identity. */\nexport const dispatchFileId = (\n\tinputs: DispatchFileIdInputs,\n): Effect.Effect<string, RouterValidationError> =>\n\tEffect.gen(function* () {\n\t\tconst readable = truncateReadable(\n\t\t\tnormalizeDispatchSegment(\n\t\t\t\t[\n\t\t\t\t\tinputs.identity.app,\n\t\t\t\t\tinputs.identity.stack,\n\t\t\t\t\tinputs.dispatch.serviceKey,\n\t\t\t\t\tinputs.dispatch.role,\n\t\t\t\t].join('-'),\n\t\t\t),\n\t\t);\n\t\tconst hash = createHash('sha256').update(canonicalDispatchTuple(inputs)).digest('hex');\n\t\tconst id = `r1-${readable}-${hash}`;\n\t\tif (id.length > MAX_DISPATCH_ID_LEN) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterValidationError({\n\t\t\t\t\tfield: 'dispatchId',\n\t\t\t\t\tvalue: id,\n\t\t\t\t\tdetail: `exceeds ${MAX_DISPATCH_ID_LEN}-char limit`,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tif (!/^r1-[a-z0-9][a-z0-9-]*-[a-f0-9]{64}$/.test(id)) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterValidationError({\n\t\t\t\t\tfield: 'dispatchId',\n\t\t\t\t\tvalue: id,\n\t\t\t\t\tdetail: 'expected r1-<lower-case-slug>-<sha256-hex>',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn id;\n\t});\n"],"mappings":";;;;;AAsDA,MAAM,sBAAsB;AAC5B,MAAM,+BAA+B;;;;AAKrC,MAAa,4BAA4B,QACxC,IACE,
|
|
1
|
+
{"version":3,"file":"hostname.mjs","names":[],"sources":["../../../src/orchestrators/router/hostname.ts"],"sourcesContent":["// Hostname + dispatch-id minting helpers.\n//\n// Architecture distilled-doc §\"Outputs / capabilities provided\":\n//\n// - default (\"main\") stack: `<service>.<app>.localhost`\n// - every other stack: `<service>.<stack>.<app>.localhost`\n//\n// \"service\" here is the dispatch-id's `role` segment — NOT the\n// plugin's name. Plugins emit a `(serviceKey, role)` DispatchId; we\n// fold the role into the hostname, since the role is the\n// user-meaningful side (\"api\", \"key-server\", \"indexer-metrics\") and\n// the service key already encodes plugin + app + stack on the\n// dispatch-file side.\n//\n// Architecture invariants:\n// #7 — Distinct `(app, stack, service)` triples MUST produce\n// distinct hostnames AND distinct dispatch ids. The default-\n// stack omission is intentional UX; every other stack\n// includes the stack segment so parallel stacks of the same\n// app never collide.\n// #8 — Hostname service labels fold dots to a label-safe separator.\n// Dispatch ids are minted from a canonical source tuple plus a\n// SHA-256 digest, so lossy readable folding cannot collide.\n// #13 — User-influenceable strings are validated against a\n// conservative character set before render.\n//\n// As of B1 (boundary-repair): pure URL composition + hostname minting\n// live in `substrate/runtime/routed-url.ts` so L2 plugins can call\n// them without reaching across the L2→L3 boundary. This module re-\n// exports the substrate primitives, owns dispatch-file id minting\n// (router-orchestrator-specific), and provides a `routerHostname`\n// adapter that projects substrate's `HostnameValidationError` into the\n// router's `RouterValidationError` union for intra-L3 callers\n// (file-provider, resolveRoute).\n\nimport { createHash } from 'node:crypto';\n\nimport { Effect } from 'effect';\nimport type { DispatchId } from '../../contracts/routable.ts';\nimport type { Identity } from '../../substrate/identity.ts';\nimport {\n\tDEFAULT_STACK,\n\tHostnameValidationError,\n\tnormalizeServiceSegment,\n\trenderUrl,\n\troutedHostname,\n} from '../../substrate/runtime/routed-url.ts';\nimport { RouterValidationError } from './errors.ts';\n\n// Re-export substrate-blind primitives for intra-L3 callers that\n// continue to import from this module path.\nexport { DEFAULT_STACK, normalizeServiceSegment, renderUrl };\n\n// The readable prefix is capped, and the SHA-256 suffix is fixed size.\nconst MAX_DISPATCH_ID_LEN = 140;\nconst MAX_DISPATCH_ID_READABLE_LEN = 48;\n\n/** Build the human-readable prefix of a dispatch id. This is not the\n * identity key; uniqueness comes from the hash over the canonical\n * tuple in `dispatchFileId`. */\nexport const normalizeDispatchSegment = (raw: string): string =>\n\traw\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t.replace(/-+/g, '-')\n\t\t.replace(/^-|-$/g, '') || 'route';\n\n// ---------------------------------------------------------------------------\n// Hostname minting — orchestrator-internal adapter\n// ---------------------------------------------------------------------------\n\n/** Project the substrate-blind hostname error into the router error\n * union. The shape is identical (tagged class with field/value/detail);\n * this exists so intra-L3 composers (file-provider, resolveRoute) can\n * keep returning the existing `RouterError` union without a per-call\n * `mapError` at every call site. */\nconst liftHostnameError = (cause: HostnameValidationError): RouterValidationError =>\n\tnew RouterValidationError({\n\t\tfield: cause.field,\n\t\tvalue: cause.value,\n\t\tdetail: cause.detail,\n\t});\n\n/** Intra-L3 alias for substrate's `routedHostname` that maps the\n * substrate error onto the router orchestrator's `RouterError` union.\n *\n * L2 plugins MUST NOT import this — they import `routedHostname` from\n * `substrate/runtime/routed-url.ts` directly. This adapter exists only\n * for the orchestrator's own composers. */\nexport const routerHostname = (\n\tidentity: Identity,\n\trole: string,\n): Effect.Effect<string, RouterValidationError> =>\n\troutedHostname(identity, role).pipe(Effect.mapError(liftHostnameError));\n\n// ---------------------------------------------------------------------------\n// Dispatch-file id minting\n// ---------------------------------------------------------------------------\n\nexport interface DispatchFileIdInputs {\n\treadonly identity: Identity;\n\treadonly dispatch: DispatchId;\n}\n\nconst canonicalDispatchTuple = (inputs: DispatchFileIdInputs): string =>\n\tJSON.stringify([\n\t\t'devstack-router-dispatch-v1',\n\t\tinputs.identity.app,\n\t\tinputs.identity.stack,\n\t\tinputs.dispatch.serviceKey,\n\t\tinputs.dispatch.role,\n\t]);\n\nconst truncateReadable = (value: string): string =>\n\t(value.length <= MAX_DISPATCH_ID_READABLE_LEN\n\t\t? value\n\t\t: value.slice(0, MAX_DISPATCH_ID_READABLE_LEN).replace(/-+$/g, '')) || 'route';\n\n/** Mint a dispatch-file id from the full route identity tuple. The id\n * is used as the file name in the global file-provider directory\n * (one file per backend), and as the Traefik router/service name\n * inside the file.\n *\n * Format: `r1-<readable-prefix>-<sha256>`.\n *\n * The readable prefix is intentionally lossy; it exists only to make\n * directory listings diagnosable. The SHA-256 suffix is over the\n * canonical `(version, app, stack, serviceKey, role)` tuple, so\n * raw underscores, separator strings, case, and dot/hyphen folding do\n * not affect identity. */\nexport const dispatchFileId = (\n\tinputs: DispatchFileIdInputs,\n): Effect.Effect<string, RouterValidationError> =>\n\tEffect.gen(function* () {\n\t\tconst readable = truncateReadable(\n\t\t\tnormalizeDispatchSegment(\n\t\t\t\t[\n\t\t\t\t\tinputs.identity.app,\n\t\t\t\t\tinputs.identity.stack,\n\t\t\t\t\tinputs.dispatch.serviceKey,\n\t\t\t\t\tinputs.dispatch.role,\n\t\t\t\t].join('-'),\n\t\t\t),\n\t\t);\n\t\tconst hash = createHash('sha256').update(canonicalDispatchTuple(inputs)).digest('hex');\n\t\tconst id = `r1-${readable}-${hash}`;\n\t\tif (id.length > MAX_DISPATCH_ID_LEN) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterValidationError({\n\t\t\t\t\tfield: 'dispatchId',\n\t\t\t\t\tvalue: id,\n\t\t\t\t\tdetail: `exceeds ${MAX_DISPATCH_ID_LEN}-char limit`,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tif (!/^r1-[a-z0-9][a-z0-9-]*-[a-f0-9]{64}$/.test(id)) {\n\t\t\treturn yield* Effect.fail(\n\t\t\t\tnew RouterValidationError({\n\t\t\t\t\tfield: 'dispatchId',\n\t\t\t\t\tvalue: id,\n\t\t\t\t\tdetail: 'expected r1-<lower-case-slug>-<sha256-hex>',\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn id;\n\t});\n"],"mappings":";;;;;AAsDA,MAAM,sBAAsB;AAC5B,MAAM,+BAA+B;;;;AAKrC,MAAa,4BAA4B,QACxC,IACE,YAAY,CAAC,CACb,QAAQ,eAAe,GAAG,CAAC,CAC3B,QAAQ,OAAO,GAAG,CAAC,CACnB,QAAQ,UAAU,EAAE,KAAK;;;;;;AAW5B,MAAM,qBAAqB,UAC1B,IAAI,sBAAsB;CACzB,OAAO,MAAM;CACb,OAAO,MAAM;CACb,QAAQ,MAAM;AACf,CAAC;;;;;;;AAQF,MAAa,kBACZ,UACA,SAEA,eAAe,UAAU,IAAI,CAAC,CAAC,KAAK,OAAO,SAAS,iBAAiB,CAAC;AAWvE,MAAM,0BAA0B,WAC/B,KAAK,UAAU;CACd;CACA,OAAO,SAAS;CAChB,OAAO,SAAS;CAChB,OAAO,SAAS;CAChB,OAAO,SAAS;AACjB,CAAC;AAEF,MAAM,oBAAoB,WACxB,MAAM,UAAU,+BACd,QACA,MAAM,MAAM,GAAG,4BAA4B,CAAC,CAAC,QAAQ,QAAQ,EAAE,MAAM;;;;;;;;;;;;;AAczE,MAAa,kBACZ,WAEA,OAAO,IAAI,aAAa;CAYvB,MAAM,KAAK,MAXM,iBAChB,yBACC;EACC,OAAO,SAAS;EAChB,OAAO,SAAS;EAChB,OAAO,SAAS;EAChB,OAAO,SAAS;CACjB,CAAC,CAAC,KAAK,GAAG,CACX,CAGuB,EAAE,GADb,WAAW,QAAQ,CAAC,CAAC,OAAO,uBAAuB,MAAM,CAAC,CAAC,CAAC,OAAO,KAChD;CAChC,IAAI,GAAG,SAAS,qBACf,OAAO,OAAO,OAAO,KACpB,IAAI,sBAAsB;EACzB,OAAO;EACP,OAAO;EACP,QAAQ,WAAW,oBAAoB;CACxC,CAAC,CACF;CAED,IAAI,CAAC,uCAAuC,KAAK,EAAE,GAClD,OAAO,OAAO,OAAO,KACpB,IAAI,sBAAsB;EACzB,OAAO;EACP,OAAO;EACP,QAAQ;CACT,CAAC,CACF;CAED,OAAO;AACR,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import { join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { homedir, userInfo } from "node:os";
|
|
6
6
|
const shortHash = (input) => createHash("sha256").update(input).digest("hex").slice(0, 12);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.mjs","names":[],"sources":["../../../src/orchestrators/router/profile.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { execFileSync } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport { homedir, userInfo } from 'node:os';\nimport { join } from 'node:path';\n\nimport type { DockerHostShape } from '../../runtime/docker/client.ts';\n\nexport const ROUTER_PROFILE_VERSION = 1;\n\nexport interface RouterProfile {\n\treadonly version: typeof ROUTER_PROFILE_VERSION;\n\treadonly id: string;\n\treadonly userId: string;\n\treadonly dockerContextId: string;\n\treadonly stateDir: string;\n\treadonly dispatchDir: string;\n\treadonly containerName: string;\n\treadonly networkName: string;\n\treadonly bootstrapLockFile: string;\n\treadonly dispatchLockFile: string;\n}\n\nexport interface RouterProfileOptions {\n\treadonly userId: string;\n\treadonly dockerContextId: string;\n\treadonly stateRoot?: string;\n\treadonly namePrefix?: string;\n}\n\nexport interface DefaultRouterProfileOptions {\n\treadonly dockerHost?: DockerHostShape;\n\treadonly env?: Readonly<Record<string, string | undefined>>;\n\treadonly stateRoot?: string;\n}\n\nconst shortHash = (input: string): string =>\n\tcreateHash('sha256').update(input).digest('hex').slice(0, 12);\n\nconst safeSegment = (input: string): string =>\n\tinput\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9_.-]+/g, '-')\n\t\t.replace(/^[^a-z0-9]+/, '')\n\t\t.slice(0, 24)\n\t\t.replace(/[-_.]+$/, '');\n\nexport const makeRouterProfile = (options: RouterProfileOptions): RouterProfile => {\n\tconst prefix = options.namePrefix ?? 'devstack';\n\tconst fingerprint = shortHash(`${options.userId}\\0${options.dockerContextId}`);\n\tconst userSegment = safeSegment(options.userId) || 'user';\n\tconst id = `${userSegment}-${fingerprint}`;\n\tconst stateRoot = options.stateRoot ?? join(homedir(), '.devstack', 'router');\n\tconst stateDir = join(stateRoot, id);\n\treturn {\n\t\tversion: ROUTER_PROFILE_VERSION,\n\t\tid,\n\t\tuserId: options.userId,\n\t\tdockerContextId: options.dockerContextId,\n\t\tstateDir,\n\t\tdispatchDir: join(stateDir, 'dispatch'),\n\t\tcontainerName: `${prefix}-router-${fingerprint}`,\n\t\tnetworkName: `${prefix}-router-${fingerprint}`,\n\t\tbootstrapLockFile: join(stateDir, 'locks', 'bootstrap.lock'),\n\t\tdispatchLockFile: join(stateDir, 'locks', 'dispatch.lock'),\n\t};\n};\n\nexport const currentRouterUserId = (): string => {\n\tif (typeof process.getuid === 'function') return `uid-${process.getuid()}`;\n\ttry {\n\t\treturn `user-${userInfo().username}`;\n\t} catch {\n\t\treturn `user-${process.env.USER ?? 'unknown'}`;\n\t}\n};\n\nconst dockerContextFromCli = (\n\tbin: string,\n\tenv: Readonly<Record<string, string | undefined>>,\n): string | null => {\n\ttry {\n\t\tconst out = execFileSync(bin, ['context', 'show'], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: 1000,\n\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\tenv: { ...process.env, ...env },\n\t\t});\n\t\tconst trimmed = out.trim();\n\t\treturn trimmed.length > 0 ? trimmed : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst dockerContextFromConfig = (\n\tenv: Readonly<Record<string, string | undefined>>,\n): string | null => {\n\tconst configDir = env.DOCKER_CONFIG ?? join(homedir(), '.docker');\n\ttry {\n\t\tconst raw = JSON.parse(readFileSync(join(configDir, 'config.json'), 'utf8')) as {\n\t\t\treadonly currentContext?: unknown;\n\t\t};\n\t\treturn typeof raw.currentContext === 'string' && raw.currentContext.trim().length > 0\n\t\t\t? raw.currentContext.trim()\n\t\t\t: null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst dockerDaemonIdFromCli = (\n\tbin: string,\n\tenv: Readonly<Record<string, string | undefined>>,\n): string | null => {\n\ttry {\n\t\tconst out = execFileSync(bin, ['info', '--format', '{{.ID}}'], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: 1000,\n\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\tenv: { ...process.env, ...env },\n\t\t});\n\t\tconst trimmed = out.trim();\n\t\treturn trimmed.length > 0 ? trimmed : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nexport const resolveDockerContextId = (\n\tdockerHost: DockerHostShape = {},\n\tenv: Readonly<Record<string, string | undefined>> = process.env,\n): string => {\n\tconst bin = dockerHost.bin ?? 'docker';\n\tconst host = dockerHost.dockerHost ?? env.DOCKER_HOST ?? 'default';\n\tconst dockerEnv: Record<string, string | undefined> = {\n\t\t...(env.DOCKER_CONTEXT === undefined ? {} : { DOCKER_CONTEXT: env.DOCKER_CONTEXT }),\n\t\t...(host === 'default' ? {} : { DOCKER_HOST: host }),\n\t};\n\tconst envContext = env.DOCKER_CONTEXT?.trim();\n\tconst context =\n\t\tenvContext && envContext.length > 0\n\t\t\t? envContext\n\t\t\t: (dockerContextFromConfig(env) ?? dockerContextFromCli(bin, dockerEnv));\n\tif (context !== null) return `context:${context}|host:${host}`;\n\tconst daemonId = dockerDaemonIdFromCli(bin, dockerEnv);\n\tif (daemonId !== null) return `daemon:${daemonId}`;\n\treturn `context:default|host:${host}`;\n};\n\nexport const makeDefaultRouterProfile = (\n\toptions: DefaultRouterProfileOptions = {},\n): RouterProfile =>\n\tmakeRouterProfile({\n\t\tuserId: currentRouterUserId(),\n\t\tdockerContextId: resolveDockerContextId(options.dockerHost, options.env),\n\t\t...(options.stateRoot === undefined ? {} : { stateRoot: options.stateRoot }),\n\t});\n"],"mappings":";;;;;AAoCA,MAAM,aAAa,UAClB,WAAW,
|
|
1
|
+
{"version":3,"file":"profile.mjs","names":[],"sources":["../../../src/orchestrators/router/profile.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { execFileSync } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport { homedir, userInfo } from 'node:os';\nimport { join } from 'node:path';\n\nimport type { DockerHostShape } from '../../runtime/docker/client.ts';\n\nexport const ROUTER_PROFILE_VERSION = 1;\n\nexport interface RouterProfile {\n\treadonly version: typeof ROUTER_PROFILE_VERSION;\n\treadonly id: string;\n\treadonly userId: string;\n\treadonly dockerContextId: string;\n\treadonly stateDir: string;\n\treadonly dispatchDir: string;\n\treadonly containerName: string;\n\treadonly networkName: string;\n\treadonly bootstrapLockFile: string;\n\treadonly dispatchLockFile: string;\n}\n\nexport interface RouterProfileOptions {\n\treadonly userId: string;\n\treadonly dockerContextId: string;\n\treadonly stateRoot?: string;\n\treadonly namePrefix?: string;\n}\n\nexport interface DefaultRouterProfileOptions {\n\treadonly dockerHost?: DockerHostShape;\n\treadonly env?: Readonly<Record<string, string | undefined>>;\n\treadonly stateRoot?: string;\n}\n\nconst shortHash = (input: string): string =>\n\tcreateHash('sha256').update(input).digest('hex').slice(0, 12);\n\nconst safeSegment = (input: string): string =>\n\tinput\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9_.-]+/g, '-')\n\t\t.replace(/^[^a-z0-9]+/, '')\n\t\t.slice(0, 24)\n\t\t.replace(/[-_.]+$/, '');\n\nexport const makeRouterProfile = (options: RouterProfileOptions): RouterProfile => {\n\tconst prefix = options.namePrefix ?? 'devstack';\n\tconst fingerprint = shortHash(`${options.userId}\\0${options.dockerContextId}`);\n\tconst userSegment = safeSegment(options.userId) || 'user';\n\tconst id = `${userSegment}-${fingerprint}`;\n\tconst stateRoot = options.stateRoot ?? join(homedir(), '.devstack', 'router');\n\tconst stateDir = join(stateRoot, id);\n\treturn {\n\t\tversion: ROUTER_PROFILE_VERSION,\n\t\tid,\n\t\tuserId: options.userId,\n\t\tdockerContextId: options.dockerContextId,\n\t\tstateDir,\n\t\tdispatchDir: join(stateDir, 'dispatch'),\n\t\tcontainerName: `${prefix}-router-${fingerprint}`,\n\t\tnetworkName: `${prefix}-router-${fingerprint}`,\n\t\tbootstrapLockFile: join(stateDir, 'locks', 'bootstrap.lock'),\n\t\tdispatchLockFile: join(stateDir, 'locks', 'dispatch.lock'),\n\t};\n};\n\nexport const currentRouterUserId = (): string => {\n\tif (typeof process.getuid === 'function') return `uid-${process.getuid()}`;\n\ttry {\n\t\treturn `user-${userInfo().username}`;\n\t} catch {\n\t\treturn `user-${process.env.USER ?? 'unknown'}`;\n\t}\n};\n\nconst dockerContextFromCli = (\n\tbin: string,\n\tenv: Readonly<Record<string, string | undefined>>,\n): string | null => {\n\ttry {\n\t\tconst out = execFileSync(bin, ['context', 'show'], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: 1000,\n\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\tenv: { ...process.env, ...env },\n\t\t});\n\t\tconst trimmed = out.trim();\n\t\treturn trimmed.length > 0 ? trimmed : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst dockerContextFromConfig = (\n\tenv: Readonly<Record<string, string | undefined>>,\n): string | null => {\n\tconst configDir = env.DOCKER_CONFIG ?? join(homedir(), '.docker');\n\ttry {\n\t\tconst raw = JSON.parse(readFileSync(join(configDir, 'config.json'), 'utf8')) as {\n\t\t\treadonly currentContext?: unknown;\n\t\t};\n\t\treturn typeof raw.currentContext === 'string' && raw.currentContext.trim().length > 0\n\t\t\t? raw.currentContext.trim()\n\t\t\t: null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst dockerDaemonIdFromCli = (\n\tbin: string,\n\tenv: Readonly<Record<string, string | undefined>>,\n): string | null => {\n\ttry {\n\t\tconst out = execFileSync(bin, ['info', '--format', '{{.ID}}'], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: 1000,\n\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\tenv: { ...process.env, ...env },\n\t\t});\n\t\tconst trimmed = out.trim();\n\t\treturn trimmed.length > 0 ? trimmed : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nexport const resolveDockerContextId = (\n\tdockerHost: DockerHostShape = {},\n\tenv: Readonly<Record<string, string | undefined>> = process.env,\n): string => {\n\tconst bin = dockerHost.bin ?? 'docker';\n\tconst host = dockerHost.dockerHost ?? env.DOCKER_HOST ?? 'default';\n\tconst dockerEnv: Record<string, string | undefined> = {\n\t\t...(env.DOCKER_CONTEXT === undefined ? {} : { DOCKER_CONTEXT: env.DOCKER_CONTEXT }),\n\t\t...(host === 'default' ? {} : { DOCKER_HOST: host }),\n\t};\n\tconst envContext = env.DOCKER_CONTEXT?.trim();\n\tconst context =\n\t\tenvContext && envContext.length > 0\n\t\t\t? envContext\n\t\t\t: (dockerContextFromConfig(env) ?? dockerContextFromCli(bin, dockerEnv));\n\tif (context !== null) return `context:${context}|host:${host}`;\n\tconst daemonId = dockerDaemonIdFromCli(bin, dockerEnv);\n\tif (daemonId !== null) return `daemon:${daemonId}`;\n\treturn `context:default|host:${host}`;\n};\n\nexport const makeDefaultRouterProfile = (\n\toptions: DefaultRouterProfileOptions = {},\n): RouterProfile =>\n\tmakeRouterProfile({\n\t\tuserId: currentRouterUserId(),\n\t\tdockerContextId: resolveDockerContextId(options.dockerHost, options.env),\n\t\t...(options.stateRoot === undefined ? {} : { stateRoot: options.stateRoot }),\n\t});\n"],"mappings":";;;;;AAoCA,MAAM,aAAa,UAClB,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;AAE7D,MAAM,eAAe,UACpB,MACE,YAAY,CAAC,CACb,QAAQ,kBAAkB,GAAG,CAAC,CAC9B,QAAQ,eAAe,EAAE,CAAC,CAC1B,MAAM,GAAG,EAAE,CAAC,CACZ,QAAQ,WAAW,EAAE;AAExB,MAAa,qBAAqB,YAAiD;CAClF,MAAM,SAAS,QAAQ,cAAc;CACrC,MAAM,cAAc,UAAU,GAAG,QAAQ,OAAO,IAAI,QAAQ,iBAAiB;CAE7E,MAAM,KAAK,GADS,YAAY,QAAQ,MAAM,KAAK,OACzB,GAAG;CAE7B,MAAM,WAAW,KADC,QAAQ,aAAa,KAAK,QAAQ,GAAG,aAAa,QAAQ,GAC3C,EAAE;CACnC,OAAO;EACN,SAAA;EACA;EACA,QAAQ,QAAQ;EAChB,iBAAiB,QAAQ;EACzB;EACA,aAAa,KAAK,UAAU,UAAU;EACtC,eAAe,GAAG,OAAO,UAAU;EACnC,aAAa,GAAG,OAAO,UAAU;EACjC,mBAAmB,KAAK,UAAU,SAAS,gBAAgB;EAC3D,kBAAkB,KAAK,UAAU,SAAS,eAAe;CAC1D;AACD;AAEA,MAAa,4BAAoC;CAChD,IAAI,OAAO,QAAQ,WAAW,YAAY,OAAO,OAAO,QAAQ,OAAO;CACvE,IAAI;EACH,OAAO,QAAQ,SAAS,CAAC,CAAC;CAC3B,QAAQ;EACP,OAAO,QAAQ,QAAQ,IAAI,QAAQ;CACpC;AACD;AAEA,MAAM,wBACL,KACA,QACmB;CACnB,IAAI;EAOH,MAAM,UANM,aAAa,KAAK,CAAC,WAAW,MAAM,GAAG;GAClD,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;GAAQ;GAClC,KAAK;IAAE,GAAG,QAAQ;IAAK,GAAG;GAAI;EAC/B,CACkB,CAAC,CAAC,KAAK;EACzB,OAAO,QAAQ,SAAS,IAAI,UAAU;CACvC,QAAQ;EACP,OAAO;CACR;AACD;AAEA,MAAM,2BACL,QACmB;CACnB,MAAM,YAAY,IAAI,iBAAiB,KAAK,QAAQ,GAAG,SAAS;CAChE,IAAI;EACH,MAAM,MAAM,KAAK,MAAM,aAAa,KAAK,WAAW,aAAa,GAAG,MAAM,CAAC;EAG3E,OAAO,OAAO,IAAI,mBAAmB,YAAY,IAAI,eAAe,KAAK,CAAC,CAAC,SAAS,IACjF,IAAI,eAAe,KAAK,IACxB;CACJ,QAAQ;EACP,OAAO;CACR;AACD;AAEA,MAAM,yBACL,KACA,QACmB;CACnB,IAAI;EAOH,MAAM,UANM,aAAa,KAAK;GAAC;GAAQ;GAAY;EAAS,GAAG;GAC9D,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;GAAQ;GAClC,KAAK;IAAE,GAAG,QAAQ;IAAK,GAAG;GAAI;EAC/B,CACkB,CAAC,CAAC,KAAK;EACzB,OAAO,QAAQ,SAAS,IAAI,UAAU;CACvC,QAAQ;EACP,OAAO;CACR;AACD;AAEA,MAAa,0BACZ,aAA8B,CAAC,GAC/B,MAAoD,QAAQ,QAChD;CACZ,MAAM,MAAM,WAAW,OAAO;CAC9B,MAAM,OAAO,WAAW,cAAc,IAAI,eAAe;CACzD,MAAM,YAAgD;EACrD,GAAI,IAAI,mBAAmB,KAAA,IAAY,CAAC,IAAI,EAAE,gBAAgB,IAAI,eAAe;EACjF,GAAI,SAAS,YAAY,CAAC,IAAI,EAAE,aAAa,KAAK;CACnD;CACA,MAAM,aAAa,IAAI,gBAAgB,KAAK;CAC5C,MAAM,UACL,cAAc,WAAW,SAAS,IAC/B,aACC,wBAAwB,GAAG,KAAK,qBAAqB,KAAK,SAAS;CACxE,IAAI,YAAY,MAAM,OAAO,WAAW,QAAQ,QAAQ;CACxD,MAAM,WAAW,sBAAsB,KAAK,SAAS;CACrD,IAAI,aAAa,MAAM,OAAO,UAAU;CACxC,OAAO,wBAAwB;AAChC;AAEA,MAAa,4BACZ,UAAuC,CAAC,MAExC,kBAAkB;CACjB,QAAQ,oBAAoB;CAC5B,iBAAiB,uBAAuB,QAAQ,YAAY,QAAQ,GAAG;CACvE,GAAI,QAAQ,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,QAAQ,UAAU;AAC3E,CAAC"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { RoutableDecl } from "../../contracts/routable.mjs";
|
|
2
1
|
import { Identity } from "../../substrate/identity.mjs";
|
|
3
|
-
import {
|
|
2
|
+
import { RoutableDecl } from "../../contracts/routable.mjs";
|
|
3
|
+
import { Context, Effect, FileSystem, Layer, SubscriptionRef } from "effect";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { IdentityContext } from "../../substrate/runtime/paths.mjs";
|
|
2
1
|
import { atomicWriteFile } from "../../substrate/runtime/atomic-write.mjs";
|
|
3
|
-
import { logWarningAndIgnore } from "../../substrate/runtime/observability/ignore-with-log.mjs";
|
|
4
2
|
import { LivenessProbeScope, checkHolderLiveness, layerLivenessProbeScope, ownHolder } from "../../substrate/runtime/cross-process/liveness.mjs";
|
|
5
3
|
import { acquireStackLock } from "../../substrate/runtime/cross-process/stack-lock.mjs";
|
|
4
|
+
import { logWarningAndIgnore } from "../../substrate/runtime/observability/ignore-with-log.mjs";
|
|
6
5
|
import "../../substrate/runtime/observability/index.mjs";
|
|
6
|
+
import { IdentityContext } from "../../substrate/runtime/paths.mjs";
|
|
7
7
|
import { DockerHost, DockerSpawner } from "../../runtime/docker/client.mjs";
|
|
8
8
|
import { connect, waitForIp } from "../../runtime/docker/network.mjs";
|
|
9
9
|
import "../../runtime/docker/index.mjs";
|
|
@@ -21,7 +21,7 @@ import { request } from "node:http";
|
|
|
21
21
|
const ROUTER_LOCK_TIMEOUT_MILLIS = 12e4;
|
|
22
22
|
var RouterConfig = class extends Context.Service()("@devstack/orchestrators/router/RouterConfig") {};
|
|
23
23
|
/** Default-config layer for tests. Production wires this from
|
|
24
|
-
* `
|
|
24
|
+
* `orchestrators/boot.ts` at the engine boundary. */
|
|
25
25
|
const layerRouterConfigLiteral = (cfg) => Layer.succeed(RouterConfig)(cfg);
|
|
26
26
|
var UpstreamResolverService = class extends Context.Service()("@devstack/orchestrators/router/UpstreamResolver") {};
|
|
27
27
|
const layerDockerUpstreamResolver = (profile) => Layer.effect(UpstreamResolverService, Effect.gen(function* () {
|
|
@@ -46,11 +46,6 @@ const layerDockerUpstreamResolver = (profile) => Layer.effect(UpstreamResolverSe
|
|
|
46
46
|
}));
|
|
47
47
|
var RouterService = class extends Context.Service()("@devstack/orchestrators/router/Router") {};
|
|
48
48
|
const warnDispatchDecodeDiagnostic = (diagnostic) => Effect.gen(function* () {
|
|
49
|
-
yield* Effect.annotateCurrentSpan({
|
|
50
|
-
"router.dispatch.path": diagnostic.path,
|
|
51
|
-
"router.dispatch.file_id": diagnostic.dispatchFileId,
|
|
52
|
-
"router.dispatch.decode_reason": diagnostic.reason
|
|
53
|
-
});
|
|
54
49
|
yield* Effect.logWarning(`router dispatch route file ${diagnostic.path} could not be fully decoded; reason=${diagnostic.reason}; treating it as an unknown route lease where destructive bootstrap safety matters`);
|
|
55
50
|
});
|
|
56
51
|
const readDispatchRouteScan = (fs, dispatchDir, dispatchFileId, options = {}) => Effect.gen(function* () {
|
|
@@ -246,24 +241,15 @@ const resolveDisabledDirectRoute = (identity, decl, registry) => Effect.gen(func
|
|
|
246
241
|
wireProtocol
|
|
247
242
|
};
|
|
248
243
|
});
|
|
249
|
-
const endpointFromResolvedRoute = (decl, resolved) => {
|
|
250
|
-
const url = resolved.wireProtocol === "tcp" ? `tcp://127.0.0.1:${resolved.entrypointPort}` : `http://${resolved.hostname}:${resolved.entrypointPort}`;
|
|
251
|
-
return {
|
|
252
|
-
endpointName: decl.endpointName,
|
|
253
|
-
hostname: resolved.hostname,
|
|
254
|
-
entrypointPort: resolved.entrypointPort,
|
|
255
|
-
url,
|
|
256
|
-
wireProtocol: resolved.wireProtocol
|
|
257
|
-
};
|
|
258
|
-
};
|
|
259
244
|
const removeDispatchFile = (fs, profile, resolved) => fs.remove(path.join(profile.dispatchDir, dispatchFilename(resolved.dispatchFileId))).pipe(Effect.ignore);
|
|
260
|
-
const waitForPublicRouteReadiness = (cfg, decl,
|
|
245
|
+
const waitForPublicRouteReadiness = (cfg, decl, resolved) => {
|
|
261
246
|
const options = cfg.routeReadinessProbe;
|
|
262
247
|
if (options?.enabled !== true || cfg.disabled || resolved.wireProtocol === "tcp" || decl.readiness === "deferred") return Effect.void;
|
|
263
248
|
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
264
249
|
const intervalMs = options.intervalMs ?? DEFAULT_ROUTE_READINESS_INTERVAL_MS;
|
|
265
250
|
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_ROUTE_READINESS_REQUEST_TIMEOUT_MS;
|
|
266
251
|
const probeUrl = `http://${directLoopbackHost}:${resolved.entrypointPort}`;
|
|
252
|
+
const publicUrl = `http://${resolved.hostname}:${resolved.entrypointPort}`;
|
|
267
253
|
return waitForHttpEndpoint({
|
|
268
254
|
endpoint: probeUrl,
|
|
269
255
|
timeoutMs,
|
|
@@ -274,9 +260,9 @@ const waitForPublicRouteReadiness = (cfg, decl, endpoint, resolved) => {
|
|
|
274
260
|
validate: (response) => responseHasReadyRoute(response, resolved)
|
|
275
261
|
}).pipe(Effect.mapError((cause) => new RouteReadinessProbeFailed({
|
|
276
262
|
dispatchFileId: resolved.dispatchFileId,
|
|
277
|
-
url:
|
|
263
|
+
url: publicUrl,
|
|
278
264
|
timeoutMs,
|
|
279
|
-
detail: `public router endpoint ${
|
|
265
|
+
detail: `public router endpoint ${publicUrl} did not serve route ${resolved.dispatchFileId} within ${timeoutMs}ms (probeUrl=${probeUrl}, hostHeader=${resolved.hostname})`,
|
|
280
266
|
cause
|
|
281
267
|
})));
|
|
282
268
|
};
|
|
@@ -345,12 +331,11 @@ const layerRouterService = Layer.effect(RouterService, Effect.gen(function* () {
|
|
|
345
331
|
yield* Ref.set(bootRef, booted);
|
|
346
332
|
return booted;
|
|
347
333
|
}));
|
|
348
|
-
})
|
|
334
|
+
});
|
|
349
335
|
const contributeRoute = (decl) => Effect.gen(function* () {
|
|
350
336
|
const profile = cfg.profile;
|
|
351
337
|
const resolved = cfg.disabled ? yield* resolveDisabledDirectRoute(identity, decl, registry) : yield* resolveRoute(identity, decl, registry, upstreams);
|
|
352
338
|
const lease = makeRouteLease(profile, identity);
|
|
353
|
-
const endpoint = endpointFromResolvedRoute(decl, resolved);
|
|
354
339
|
const publishRouteFile = Effect.gen(function* () {
|
|
355
340
|
if (cfg.disabled) return "direct";
|
|
356
341
|
if (!cfg.disabled) yield* fs.makeDirectory(profile.dispatchDir, { recursive: true }).pipe(Effect.mapError((cause) => new DispatchWriteFailed({
|
|
@@ -389,7 +374,7 @@ const layerRouterService = Layer.effect(RouterService, Effect.gen(function* () {
|
|
|
389
374
|
let publishOwnership;
|
|
390
375
|
if (cfg.disabled) {
|
|
391
376
|
publishOwnership = yield* publishRouteFile;
|
|
392
|
-
yield* waitForPublicRouteReadiness(cfg, decl,
|
|
377
|
+
yield* waitForPublicRouteReadiness(cfg, decl, resolved);
|
|
393
378
|
if (publishOwnership !== "reused-live") yield* SubscriptionRef.update(applied, (arr) => [...arr, resolved]);
|
|
394
379
|
} else publishOwnership = yield* Effect.scoped(Effect.gen(function* () {
|
|
395
380
|
yield* acquireStackLock(profile.dispatchLockFile, ROUTER_LOCK_TIMEOUT_MILLIS).pipe(Effect.mapError((cause) => new DispatchWriteFailed({
|
|
@@ -399,7 +384,7 @@ const layerRouterService = Layer.effect(RouterService, Effect.gen(function* () {
|
|
|
399
384
|
cause
|
|
400
385
|
})));
|
|
401
386
|
const ownership = yield* publishRouteFile;
|
|
402
|
-
yield* waitForPublicRouteReadiness(cfg, decl,
|
|
387
|
+
yield* waitForPublicRouteReadiness(cfg, decl, resolved).pipe(Effect.onError(() => ownership !== "owned" ? Effect.void : removeDispatchFile(fs, profile, resolved)));
|
|
403
388
|
if (ownership !== "reused-live") yield* SubscriptionRef.update(applied, (arr) => [...arr, resolved]);
|
|
404
389
|
return ownership;
|
|
405
390
|
}));
|
|
@@ -410,8 +395,8 @@ const layerRouterService = Layer.effect(RouterService, Effect.gen(function* () {
|
|
|
410
395
|
})).pipe(logWarningAndIgnore("router scope-close cleanup failed", { dispatchFileId: resolved.dispatchFileId }));
|
|
411
396
|
yield* SubscriptionRef.update(applied, (arr) => arr.filter((r) => r.dispatchFileId !== resolved.dispatchFileId));
|
|
412
397
|
}));
|
|
413
|
-
return
|
|
414
|
-
})
|
|
398
|
+
return resolved;
|
|
399
|
+
});
|
|
415
400
|
return RouterService.of({
|
|
416
401
|
boot,
|
|
417
402
|
contributeRoute,
|