@mysten-incubation/devstack 0.7.1 → 0.7.2

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.
@@ -1,6 +1,5 @@
1
1
  import { RouteCollision, RouterValidationError } from "./errors.mjs";
2
2
  import { CORS_MIDDLEWARE_NAME } from "./cors.mjs";
3
- import { renderUrl } from "../../substrate/runtime/routed-url.mjs";
4
3
  import { dispatchFileId, routerHostname } from "./hostname.mjs";
5
4
  import { Effect } from "effect";
6
5
  //#region src/orchestrators/router/file-provider.ts
@@ -41,15 +40,11 @@ const resolveRoute = (identity, decl, registry, upstreams) => Effect.gen(functio
41
40
  value: entrypoint.name,
42
41
  detail: `wireProtocol family mismatch: decl is '${wireProtocol}' but entrypoint '${entrypoint.name}' is '${entrypoint.protocol}'`
43
42
  }));
44
- const upstreamUrl = renderUrl({
45
- protocol: wireProtocol === "tcp" ? "tcp" : wireProtocol === "https" ? "https" : "http",
46
- hostname: upstream.host,
47
- port: upstream.port
48
- });
49
- if (!/^(?:http|https|tcp):\/\/[A-Za-z0-9_.:-]+:\d+$/.test(upstreamUrl)) return yield* Effect.fail(new RouterValidationError({
43
+ const upstreamUrl = `${wireProtocol === "tcp" ? "tcp" : wireProtocol === "https" ? "https" : wireProtocol === "h2c" ? "h2c" : "http"}://${upstream.host}:${upstream.port}`;
44
+ if (!/^(?:http|h2c|https|tcp):\/\/[A-Za-z0-9_.:-]+:\d+$/.test(upstreamUrl)) return yield* Effect.fail(new RouterValidationError({
50
45
  field: "upstreamUrl",
51
46
  value: upstreamUrl,
52
- detail: "expected http(s)://<host>:<port> or tcp://<host>:<port>"
47
+ detail: "expected http(s)://<host>:<port>, h2c://<host>:<port>, or tcp://<host>:<port>"
53
48
  }));
54
49
  const cors = decl.wireProtocol === "tcp" ? false : decl.cors;
55
50
  return {
@@ -66,7 +61,7 @@ const resolveRoute = (identity, decl, registry, upstreams) => Effect.gen(functio
66
61
  * TCP renderer to write the `address:` field. Safe because
67
62
  * `resolveRoute` validates the URL shape before we get here. */
68
63
  const splitUpstream = (url) => {
69
- const stripped = url.replace(/^(?:http|https|tcp):\/\//, "");
64
+ const stripped = url.replace(/^(?:http|h2c|https|tcp):\/\//, "");
70
65
  const lastColon = stripped.lastIndexOf(":");
71
66
  return {
72
67
  host: stripped.slice(0, lastColon),
@@ -90,6 +85,7 @@ const renderLeaseHeader = (lease) => [
90
85
  * no-op rewrites don't wake the watcher. */
91
86
  const renderRouteYaml = (route, lease) => {
92
87
  if (route.wireProtocol === "tcp") return renderTcpRouteYaml(route, lease);
88
+ if (route.wireProtocol === "h2c") return renderH2cRouteYaml(route, lease);
93
89
  return renderHttpRouteYaml(route, lease);
94
90
  };
95
91
  const routeReadinessMiddlewareName = (route) => `${route.dispatchFileId}-route-ready`;
@@ -128,6 +124,52 @@ const renderHttpRouteYaml = (route, lease) => {
128
124
  ``
129
125
  ].filter((line) => line !== null).join("\n");
130
126
  };
127
+ const renderH2cRouteYaml = (route, lease) => {
128
+ const middlewares = [routeReadinessMiddlewareName(route), ...route.cors ? [CORS_MIDDLEWARE_NAME] : []];
129
+ const httpFallbackUrl = route.upstreamUrl.replace(/^h2c:\/\//, "http://");
130
+ return [
131
+ `# Auto-generated by devstack router orchestrator. Do not edit by hand.`,
132
+ `# dispatchFileId: ${route.dispatchFileId}`,
133
+ `# wireProtocol: h2c`,
134
+ `# entrypointName: ${route.entrypointName}`,
135
+ `# entrypointPort: ${route.entrypointPort}`,
136
+ `# hostname: ${route.hostname}`,
137
+ ...renderLeaseHeader(lease),
138
+ `http:`,
139
+ ` routers:`,
140
+ ` ${route.dispatchFileId}-grpc-router:`,
141
+ ` rule: "Host(\`${route.hostname}\`) && Header(\`Content-Type\`, \`application/grpc\`)"`,
142
+ ` entryPoints: ["${route.entrypointName}"]`,
143
+ ` service: "${route.dispatchFileId}-grpc-svc"`,
144
+ ` middlewares: [${middlewares.map((name) => `"${name}"`).join(", ")}]`,
145
+ ` priority: 20`,
146
+ ` ${route.dispatchFileId}-router:`,
147
+ ` rule: "Host(\`${route.hostname}\`)"`,
148
+ ` entryPoints: ["${route.entrypointName}"]`,
149
+ ` service: "${route.dispatchFileId}-svc"`,
150
+ ` middlewares: [${middlewares.map((name) => `"${name}"`).join(", ")}]`,
151
+ ` priority: 10`,
152
+ ` middlewares:`,
153
+ ` ${routeReadinessMiddlewareName(route)}:`,
154
+ ` headers:`,
155
+ ` customResponseHeaders:`,
156
+ ` ${ROUTE_READINESS_HEADER}: "${route.dispatchFileId}"`,
157
+ ` services:`,
158
+ ` ${route.dispatchFileId}-grpc-svc:`,
159
+ ` loadBalancer:`,
160
+ ` passHostHeader: false`,
161
+ ` # h2c upstream — native gRPC cleartext HTTP/2.`,
162
+ ` servers:`,
163
+ ` - url: "${route.upstreamUrl}"`,
164
+ ` ${route.dispatchFileId}-svc:`,
165
+ ` loadBalancer:`,
166
+ ` passHostHeader: false`,
167
+ ` # HTTP fallback — JSON-RPC and grpc-web.`,
168
+ ` servers:`,
169
+ ` - url: "${httpFallbackUrl}"`,
170
+ ``
171
+ ].join("\n");
172
+ };
131
173
  const renderTcpRouteYaml = (route, lease) => {
132
174
  const { host, port } = splitUpstream(route.upstreamUrl);
133
175
  return [
@@ -254,7 +296,7 @@ const parseDispatchRouteFile = (body, fallbackDispatchFileId = null) => {
254
296
  _tag: "DispatchRouteDecodeDiagnostic",
255
297
  dispatchFileId: protectedDispatchFileId,
256
298
  reason: "missing-required-route-metadata",
257
- detail: "route file is missing dispatchFileId, wireProtocol/http|https|tcp block, entrypointName, or hostname metadata"
299
+ detail: "route file is missing dispatchFileId, wireProtocol/http|h2c|https|tcp block, entrypointName, or hostname metadata"
258
300
  }]
259
301
  };
260
302
  }
@@ -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 / HTTPS upstream (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' | 'https' | '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-family routes this is\n\t * `<scheme>://<host>:<port>`; for TCP it's still rendered as\n\t * `tcp://<host>:<port>` so the renderer can pull host+port back out\n\t * 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'\n\t\t\t\t? 'tcp'\n\t\t\t\t: decl.wireProtocol === 'h2c'\n\t\t\t\t\t? 'h2c'\n\t\t\t\t\t: decl.wireProtocol === 'https'\n\t\t\t\t\t\t? 'https'\n\t\t\t\t\t\t: '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' : wireProtocol === 'https' ? 'https' : 'http',\n\t\t\thostname: upstream.host,\n\t\t\tport: upstream.port,\n\t\t});\n\t\tif (!/^(?:http|https|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(s)://<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|https|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: route.wireProtocol === 'https'\n\t\t\t\t? ` # HTTPS upstream — local self-signed backend TLS.\\n`\n\t\t\t\t: '';\n\tconst serversTransport = route.wireProtocol === 'https' ? 'devstack-insecure-local-tls' : null;\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\tserversTransport === null ? null : ` serversTransport: \"${serversTransport}\"`,\n\t\tschemeHint.length > 0 ? schemeHint.trimEnd() : null,\n\t\t` servers:`,\n\t\t` - url: \"${route.upstreamUrl}\"`,\n\t\tserversTransport === null\n\t\t\t? null\n\t\t\t: ` serversTransports:\\n ${serversTransport}:\\n insecureSkipVerify: true`,\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 (\n\t\tfromComment === 'http' ||\n\t\tfromComment === 'h2c' ||\n\t\tfromComment === 'https' ||\n\t\tfromComment === 'tcp'\n\t) {\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|https|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":"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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 / HTTPS upstream (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// # or h2c:// / https:// for those upstream protocols\n//\n// h2c routes render an additional higher-priority router keyed on native gRPC's\n// `Content-Type: application/grpc...`; the default Host router stays on HTTP so\n// grpc-web and JSON-RPC keep working against mixed-protocol services like Sui.\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, 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' | 'https' | '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-family routes this is\n\t * `<scheme>://<host>:<port>` (including `h2c://` for cleartext\n\t * HTTP/2); for TCP it's still rendered as `tcp://<host>:<port>` so\n\t * the renderer 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'\n\t\t\t\t? 'tcp'\n\t\t\t\t: decl.wireProtocol === 'h2c'\n\t\t\t\t\t? 'h2c'\n\t\t\t\t\t: decl.wireProtocol === 'https'\n\t\t\t\t\t\t? 'https'\n\t\t\t\t\t\t: '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 upstreamScheme =\n\t\t\twireProtocol === 'tcp'\n\t\t\t\t? 'tcp'\n\t\t\t\t: wireProtocol === 'https'\n\t\t\t\t\t? 'https'\n\t\t\t\t\t: wireProtocol === 'h2c'\n\t\t\t\t\t\t? 'h2c'\n\t\t\t\t\t\t: 'http';\n\t\tconst upstreamUrl = `${upstreamScheme}://${upstream.host}:${upstream.port}`;\n\t\tif (!/^(?:http|h2c|https|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(s)://<host>:<port>, h2c://<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|h2c|https|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\tif (route.wireProtocol === 'h2c') return renderH2cRouteYaml(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: route.wireProtocol === 'https'\n\t\t\t\t? ` # HTTPS upstream — local self-signed backend TLS.\\n`\n\t\t\t\t: '';\n\tconst serversTransport = route.wireProtocol === 'https' ? 'devstack-insecure-local-tls' : null;\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\tserversTransport === null ? null : ` serversTransport: \"${serversTransport}\"`,\n\t\tschemeHint.length > 0 ? schemeHint.trimEnd() : null,\n\t\t` servers:`,\n\t\t` - url: \"${route.upstreamUrl}\"`,\n\t\tserversTransport === null\n\t\t\t? null\n\t\t\t: ` serversTransports:\\n ${serversTransport}:\\n insecureSkipVerify: true`,\n\t\t``,\n\t]\n\t\t.filter((line): line is string => line !== null)\n\t\t.join('\\n');\n};\n\nconst renderH2cRouteYaml = (route: ResolvedRoute, lease: RouteLeaseMetadata): string => {\n\tconst middlewares = [\n\t\trouteReadinessMiddlewareName(route),\n\t\t...(route.cors ? [CORS_MIDDLEWARE_NAME] : []),\n\t];\n\tconst httpFallbackUrl = route.upstreamUrl.replace(/^h2c:\\/\\//, 'http://');\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: h2c`,\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}-grpc-router:`,\n\t\t` rule: \"Host(\\`${route.hostname}\\`) && Header(\\`Content-Type\\`, \\`application/grpc\\`)\"`,\n\t\t` entryPoints: [\"${route.entrypointName}\"]`,\n\t\t` service: \"${route.dispatchFileId}-grpc-svc\"`,\n\t\t` middlewares: [${middlewares.map((name) => `\"${name}\"`).join(', ')}]`,\n\t\t` priority: 20`,\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` priority: 10`,\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}-grpc-svc:`,\n\t\t` loadBalancer:`,\n\t\t` passHostHeader: false`,\n\t\t` # h2c upstream — native gRPC cleartext HTTP/2.`,\n\t\t` servers:`,\n\t\t` - url: \"${route.upstreamUrl}\"`,\n\t\t` ${route.dispatchFileId}-svc:`,\n\t\t` loadBalancer:`,\n\t\t` passHostHeader: false`,\n\t\t` # HTTP fallback — JSON-RPC and grpc-web.`,\n\t\t` servers:`,\n\t\t` - url: \"${httpFallbackUrl}\"`,\n\t\t``,\n\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 (\n\t\tfromComment === 'http' ||\n\t\tfromComment === 'h2c' ||\n\t\tfromComment === 'https' ||\n\t\tfromComment === 'tcp'\n\t) {\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|h2c|https|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":"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"}
@@ -20,7 +20,7 @@ import { Effect, Scope } from "effect";
20
20
  declare const sui: <const O extends SuiOptions = {
21
21
  mode: "local";
22
22
  }>(opts?: O) => Plugin<"sui", {
23
- mode: "local" | "local-rpc" | "live" | "fork";
23
+ mode: "local" | "live" | "fork" | "local-rpc";
24
24
  fundingFaucetStrategy: FaucetStrategy | null;
25
25
  sdk: SuiSdkShim;
26
26
  rpcUrl: string;
@@ -51,7 +51,7 @@ declare const sui: <const O extends SuiOptions = {
51
51
  declare const suiFor: ModeNamespace<{
52
52
  local: {
53
53
  local: <const O extends Omit<SuiLocalOptions, "mode">>(opts?: O) => Plugin<"sui", {
54
- mode: "local" | "local-rpc" | "live" | "fork";
54
+ mode: "local" | "live" | "fork" | "local-rpc";
55
55
  fundingFaucetStrategy: FaucetStrategy | null;
56
56
  sdk: SuiSdkShim;
57
57
  rpcUrl: string;
@@ -70,7 +70,7 @@ declare const suiFor: ModeNamespace<{
70
70
  buildImage: ImageRef | null;
71
71
  }, readonly []>;
72
72
  localRpc: (opts: Omit<SuiLocalRpcOptions, "mode">) => Plugin<"sui", {
73
- mode: "local" | "local-rpc" | "live" | "fork";
73
+ mode: "local" | "live" | "fork" | "local-rpc";
74
74
  fundingFaucetStrategy: FaucetStrategy | null;
75
75
  sdk: SuiSdkShim;
76
76
  rpcUrl: string;
@@ -91,7 +91,7 @@ declare const suiFor: ModeNamespace<{
91
91
  };
92
92
  live: {
93
93
  testnet: (opts?: Omit<SuiLiveOptions, "mode" | "network">) => Plugin<"sui", {
94
- mode: "local" | "local-rpc" | "live" | "fork";
94
+ mode: "local" | "live" | "fork" | "local-rpc";
95
95
  fundingFaucetStrategy: FaucetStrategy | null;
96
96
  sdk: SuiSdkShim;
97
97
  rpcUrl: string;
@@ -110,7 +110,7 @@ declare const suiFor: ModeNamespace<{
110
110
  buildImage: ImageRef | null;
111
111
  }, readonly []>;
112
112
  mainnet: (opts?: Omit<SuiLiveOptions, "mode" | "network">) => Plugin<"sui", {
113
- mode: "local" | "local-rpc" | "live" | "fork";
113
+ mode: "local" | "live" | "fork" | "local-rpc";
114
114
  fundingFaucetStrategy: FaucetStrategy | null;
115
115
  sdk: SuiSdkShim;
116
116
  rpcUrl: string;
@@ -129,7 +129,7 @@ declare const suiFor: ModeNamespace<{
129
129
  buildImage: ImageRef | null;
130
130
  }, readonly []>;
131
131
  devnet: (opts?: Omit<SuiLiveOptions, "mode" | "network">) => Plugin<"sui", {
132
- mode: "local" | "local-rpc" | "live" | "fork";
132
+ mode: "local" | "live" | "fork" | "local-rpc";
133
133
  fundingFaucetStrategy: FaucetStrategy | null;
134
134
  sdk: SuiSdkShim;
135
135
  rpcUrl: string;
@@ -148,7 +148,7 @@ declare const suiFor: ModeNamespace<{
148
148
  buildImage: ImageRef | null;
149
149
  }, readonly []>;
150
150
  custom: (opts: Omit<SuiLiveOptions, "mode" | "network">) => Plugin<"sui", {
151
- mode: "local" | "local-rpc" | "live" | "fork";
151
+ mode: "local" | "live" | "fork" | "local-rpc";
152
152
  fundingFaucetStrategy: FaucetStrategy | null;
153
153
  sdk: SuiSdkShim;
154
154
  rpcUrl: string;
@@ -169,7 +169,7 @@ declare const suiFor: ModeNamespace<{
169
169
  };
170
170
  fork: {
171
171
  mainnet: (opts?: Omit<SuiForkOptions, "mode" | "upstream">) => Plugin<"sui", {
172
- mode: "local" | "local-rpc" | "live" | "fork";
172
+ mode: "local" | "live" | "fork" | "local-rpc";
173
173
  fundingFaucetStrategy: FaucetStrategy | null;
174
174
  sdk: SuiSdkShim;
175
175
  rpcUrl: string;
@@ -188,7 +188,7 @@ declare const suiFor: ModeNamespace<{
188
188
  buildImage: ImageRef | null;
189
189
  }, readonly []>;
190
190
  testnet: (opts?: Omit<SuiForkOptions, "mode" | "upstream">) => Plugin<"sui", {
191
- mode: "local" | "local-rpc" | "live" | "fork";
191
+ mode: "local" | "live" | "fork" | "local-rpc";
192
192
  fundingFaucetStrategy: FaucetStrategy | null;
193
193
  sdk: SuiSdkShim;
194
194
  rpcUrl: string;
@@ -207,7 +207,7 @@ declare const suiFor: ModeNamespace<{
207
207
  buildImage: ImageRef | null;
208
208
  }, readonly []>;
209
209
  devnet: (opts?: Omit<SuiForkOptions, "mode" | "upstream">) => Plugin<"sui", {
210
- mode: "local" | "local-rpc" | "live" | "fork";
210
+ mode: "local" | "live" | "fork" | "local-rpc";
211
211
  fundingFaucetStrategy: FaucetStrategy | null;
212
212
  sdk: SuiSdkShim;
213
213
  rpcUrl: string;
@@ -7,7 +7,7 @@ const SUI_ENTRYPOINTS = [
7
7
  {
8
8
  name: "rpc",
9
9
  port: SUI_RPC_ENTRYPOINT_PORT,
10
- protocol: "http"
10
+ protocol: "h2c"
11
11
  },
12
12
  {
13
13
  name: SUI_FAUCET_ENDPOINT_NAME,
@@ -34,7 +34,7 @@ const makeSuiLocalRoutables = (parts) => [
34
34
  containerPort: SUI_RPC_ENTRYPOINT_PORT
35
35
  },
36
36
  cors: true,
37
- wireProtocol: "http"
37
+ wireProtocol: "h2c"
38
38
  },
39
39
  {
40
40
  kind: "routable",
@@ -80,7 +80,7 @@ const makeSuiForkRoutables = (parts) => [{
80
80
  containerPort: SUI_RPC_ENTRYPOINT_PORT
81
81
  },
82
82
  cors: true,
83
- wireProtocol: "http"
83
+ wireProtocol: "h2c"
84
84
  }];
85
85
  //#endregion
86
86
  export { SUI_ENTRYPOINTS, SUI_FAUCET_ENDPOINT_NAME, SUI_FAUCET_ENTRYPOINT_PORT, SUI_GRAPHQL_ENDPOINT_NAME, SUI_GRAPHQL_ENTRYPOINT_PORT, SUI_RPC_ENTRYPOINT_PORT, makeSuiForkRoutables, makeSuiLocalRoutables };
@@ -1 +1 @@
1
- {"version":3,"file":"routable.mjs","names":[],"sources":["../../../src/plugins/sui/routable.ts"],"sourcesContent":["import type { EntrypointDecl, RoutableDecl } from '../../contracts/routable.ts';\n\nexport const SUI_RPC_ENDPOINT_NAME = 'rpc' as const;\nexport const SUI_FAUCET_ENDPOINT_NAME = 'faucet' as const;\nexport const SUI_GRAPHQL_ENDPOINT_NAME = 'graphql' as const;\n\nexport const SUI_RPC_ENTRYPOINT_PORT = 9000;\nexport const SUI_FAUCET_ENTRYPOINT_PORT = 9123;\nexport const SUI_GRAPHQL_ENTRYPOINT_PORT = 9125;\n\nexport const SUI_ENTRYPOINTS: ReadonlyArray<EntrypointDecl> = [\n\t{ name: SUI_RPC_ENDPOINT_NAME, port: SUI_RPC_ENTRYPOINT_PORT, protocol: 'http' },\n\t{ name: SUI_FAUCET_ENDPOINT_NAME, port: SUI_FAUCET_ENTRYPOINT_PORT, protocol: 'http' },\n\t{ name: SUI_GRAPHQL_ENDPOINT_NAME, port: SUI_GRAPHQL_ENTRYPOINT_PORT, protocol: 'http' },\n];\n\nexport const makeSuiLocalRoutables = (parts: {\n\treadonly containerName: string;\n\treadonly includeGraphql: boolean;\n}): ReadonlyArray<RoutableDecl> => [\n\t{\n\t\tkind: 'routable',\n\t\tendpointName: SUI_RPC_ENDPOINT_NAME,\n\t\tdispatchId: {\n\t\t\tserviceKey: 'sui.local',\n\t\t\trole: SUI_RPC_ENDPOINT_NAME,\n\t\t},\n\t\tupstream: {\n\t\t\ttype: 'container',\n\t\t\tcontainerName: parts.containerName,\n\t\t\tcontainerPort: SUI_RPC_ENTRYPOINT_PORT,\n\t\t},\n\t\tcors: true,\n\t\twireProtocol: 'http',\n\t},\n\t{\n\t\tkind: 'routable',\n\t\tendpointName: SUI_FAUCET_ENDPOINT_NAME,\n\t\tdispatchId: {\n\t\t\tserviceKey: 'sui.local',\n\t\t\trole: SUI_FAUCET_ENDPOINT_NAME,\n\t\t},\n\t\tupstream: {\n\t\t\ttype: 'container',\n\t\t\tcontainerName: parts.containerName,\n\t\t\tcontainerPort: SUI_FAUCET_ENTRYPOINT_PORT,\n\t\t},\n\t\tcors: true,\n\t\twireProtocol: 'http',\n\t},\n\t...(parts.includeGraphql\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tkind: 'routable',\n\t\t\t\t\tendpointName: SUI_GRAPHQL_ENDPOINT_NAME,\n\t\t\t\t\tdispatchId: {\n\t\t\t\t\t\tserviceKey: 'sui.local',\n\t\t\t\t\t\trole: SUI_GRAPHQL_ENDPOINT_NAME,\n\t\t\t\t\t},\n\t\t\t\t\tupstream: {\n\t\t\t\t\t\ttype: 'container',\n\t\t\t\t\t\tcontainerName: parts.containerName,\n\t\t\t\t\t\tcontainerPort: SUI_GRAPHQL_ENTRYPOINT_PORT,\n\t\t\t\t\t},\n\t\t\t\t\tcors: true,\n\t\t\t\t\twireProtocol: 'http',\n\t\t\t\t} satisfies RoutableDecl,\n\t\t\t]\n\t\t: []),\n];\n\nexport const makeSuiForkRoutables = (parts: {\n\treadonly containerName: string;\n}): ReadonlyArray<RoutableDecl> => [\n\t{\n\t\tkind: 'routable',\n\t\tendpointName: SUI_RPC_ENDPOINT_NAME,\n\t\tdispatchId: {\n\t\t\tserviceKey: 'sui.fork',\n\t\t\trole: SUI_RPC_ENDPOINT_NAME,\n\t\t},\n\t\tupstream: {\n\t\t\ttype: 'container',\n\t\t\tcontainerName: parts.containerName,\n\t\t\tcontainerPort: SUI_RPC_ENTRYPOINT_PORT,\n\t\t},\n\t\tcors: true,\n\t\twireProtocol: 'http',\n\t},\n];\n"],"mappings":"AAGA,MAAa,2BAA2B;AACxC,MAAa,4BAA4B;AAEzC,MAAa,0BAA0B;AACvC,MAAa,6BAA6B;AAC1C,MAAa,8BAA8B;AAE3C,MAAa,kBAAiD;CAC7D;EAAE,MAAA;EAA6B,MAAM;EAAyB,UAAU;CAAO;CAC/E;EAAE,MAAM;EAA0B,MAAM;EAA4B,UAAU;CAAO;CACrF;EAAE,MAAM;EAA2B,MAAM;EAA6B,UAAU;CAAO;AACxF;AAEA,MAAa,yBAAyB,UAGH;CAClC;EACC,MAAM;EACN,cAAA;EACA,YAAY;GACX,YAAY;GACZ,MAAA;EACD;EACA,UAAU;GACT,MAAM;GACN,eAAe,MAAM;GACrB,eAAe;EAChB;EACA,MAAM;EACN,cAAc;CACf;CACA;EACC,MAAM;EACN,cAAc;EACd,YAAY;GACX,YAAY;GACZ,MAAM;EACP;EACA,UAAU;GACT,MAAM;GACN,eAAe,MAAM;GACrB,eAAe;EAChB;EACA,MAAM;EACN,cAAc;CACf;CACA,GAAI,MAAM,iBACP,CACA;EACC,MAAM;EACN,cAAc;EACd,YAAY;GACX,YAAY;GACZ,MAAM;EACP;EACA,UAAU;GACT,MAAM;GACN,eAAe,MAAM;GACrB,eAAe;EAChB;EACA,MAAM;EACN,cAAc;CACf,CACD,IACC,CAAC;AACL;AAEA,MAAa,wBAAwB,UAEF,CAClC;CACC,MAAM;CACN,cAAA;CACA,YAAY;EACX,YAAY;EACZ,MAAA;CACD;CACA,UAAU;EACT,MAAM;EACN,eAAe,MAAM;EACrB,eAAe;CAChB;CACA,MAAM;CACN,cAAc;AACf,CACD"}
1
+ {"version":3,"file":"routable.mjs","names":[],"sources":["../../../src/plugins/sui/routable.ts"],"sourcesContent":["import type { EntrypointDecl, RoutableDecl } from '../../contracts/routable.ts';\n\nexport const SUI_RPC_ENDPOINT_NAME = 'rpc' as const;\nexport const SUI_FAUCET_ENDPOINT_NAME = 'faucet' as const;\nexport const SUI_GRAPHQL_ENDPOINT_NAME = 'graphql' as const;\n\nexport const SUI_RPC_ENTRYPOINT_PORT = 9000;\nexport const SUI_FAUCET_ENTRYPOINT_PORT = 9123;\nexport const SUI_GRAPHQL_ENTRYPOINT_PORT = 9125;\n\nexport const SUI_ENTRYPOINTS: ReadonlyArray<EntrypointDecl> = [\n\t{ name: SUI_RPC_ENDPOINT_NAME, port: SUI_RPC_ENTRYPOINT_PORT, protocol: 'h2c' },\n\t{ name: SUI_FAUCET_ENDPOINT_NAME, port: SUI_FAUCET_ENTRYPOINT_PORT, protocol: 'http' },\n\t{ name: SUI_GRAPHQL_ENDPOINT_NAME, port: SUI_GRAPHQL_ENTRYPOINT_PORT, protocol: 'http' },\n];\n\nexport const makeSuiLocalRoutables = (parts: {\n\treadonly containerName: string;\n\treadonly includeGraphql: boolean;\n}): ReadonlyArray<RoutableDecl> => [\n\t{\n\t\tkind: 'routable',\n\t\tendpointName: SUI_RPC_ENDPOINT_NAME,\n\t\tdispatchId: {\n\t\t\tserviceKey: 'sui.local',\n\t\t\trole: SUI_RPC_ENDPOINT_NAME,\n\t\t},\n\t\tupstream: {\n\t\t\ttype: 'container',\n\t\t\tcontainerName: parts.containerName,\n\t\t\tcontainerPort: SUI_RPC_ENTRYPOINT_PORT,\n\t\t},\n\t\tcors: true,\n\t\twireProtocol: 'h2c',\n\t},\n\t{\n\t\tkind: 'routable',\n\t\tendpointName: SUI_FAUCET_ENDPOINT_NAME,\n\t\tdispatchId: {\n\t\t\tserviceKey: 'sui.local',\n\t\t\trole: SUI_FAUCET_ENDPOINT_NAME,\n\t\t},\n\t\tupstream: {\n\t\t\ttype: 'container',\n\t\t\tcontainerName: parts.containerName,\n\t\t\tcontainerPort: SUI_FAUCET_ENTRYPOINT_PORT,\n\t\t},\n\t\tcors: true,\n\t\twireProtocol: 'http',\n\t},\n\t...(parts.includeGraphql\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tkind: 'routable',\n\t\t\t\t\tendpointName: SUI_GRAPHQL_ENDPOINT_NAME,\n\t\t\t\t\tdispatchId: {\n\t\t\t\t\t\tserviceKey: 'sui.local',\n\t\t\t\t\t\trole: SUI_GRAPHQL_ENDPOINT_NAME,\n\t\t\t\t\t},\n\t\t\t\t\tupstream: {\n\t\t\t\t\t\ttype: 'container',\n\t\t\t\t\t\tcontainerName: parts.containerName,\n\t\t\t\t\t\tcontainerPort: SUI_GRAPHQL_ENTRYPOINT_PORT,\n\t\t\t\t\t},\n\t\t\t\t\tcors: true,\n\t\t\t\t\twireProtocol: 'http',\n\t\t\t\t} satisfies RoutableDecl,\n\t\t\t]\n\t\t: []),\n];\n\nexport const makeSuiForkRoutables = (parts: {\n\treadonly containerName: string;\n}): ReadonlyArray<RoutableDecl> => [\n\t{\n\t\tkind: 'routable',\n\t\tendpointName: SUI_RPC_ENDPOINT_NAME,\n\t\tdispatchId: {\n\t\t\tserviceKey: 'sui.fork',\n\t\t\trole: SUI_RPC_ENDPOINT_NAME,\n\t\t},\n\t\tupstream: {\n\t\t\ttype: 'container',\n\t\t\tcontainerName: parts.containerName,\n\t\t\tcontainerPort: SUI_RPC_ENTRYPOINT_PORT,\n\t\t},\n\t\tcors: true,\n\t\twireProtocol: 'h2c',\n\t},\n];\n"],"mappings":"AAGA,MAAa,2BAA2B;AACxC,MAAa,4BAA4B;AAEzC,MAAa,0BAA0B;AACvC,MAAa,6BAA6B;AAC1C,MAAa,8BAA8B;AAE3C,MAAa,kBAAiD;CAC7D;EAAE,MAAA;EAA6B,MAAM;EAAyB,UAAU;CAAM;CAC9E;EAAE,MAAM;EAA0B,MAAM;EAA4B,UAAU;CAAO;CACrF;EAAE,MAAM;EAA2B,MAAM;EAA6B,UAAU;CAAO;AACxF;AAEA,MAAa,yBAAyB,UAGH;CAClC;EACC,MAAM;EACN,cAAA;EACA,YAAY;GACX,YAAY;GACZ,MAAA;EACD;EACA,UAAU;GACT,MAAM;GACN,eAAe,MAAM;GACrB,eAAe;EAChB;EACA,MAAM;EACN,cAAc;CACf;CACA;EACC,MAAM;EACN,cAAc;EACd,YAAY;GACX,YAAY;GACZ,MAAM;EACP;EACA,UAAU;GACT,MAAM;GACN,eAAe,MAAM;GACrB,eAAe;EAChB;EACA,MAAM;EACN,cAAc;CACf;CACA,GAAI,MAAM,iBACP,CACA;EACC,MAAM;EACN,cAAc;EACd,YAAY;GACX,YAAY;GACZ,MAAM;EACP;EACA,UAAU;GACT,MAAM;GACN,eAAe,MAAM;GACrB,eAAe;EAChB;EACA,MAAM;EACN,cAAc;CACf,CACD,IACC,CAAC;AACL;AAEA,MAAa,wBAAwB,UAEF,CAClC;CACC,MAAM;CACN,cAAA;CACA,YAAY;EACX,YAAY;EACZ,MAAA;CACD;CACA,UAAU;EACT,MAAM;EACN,eAAe,MAAM;EACrB,eAAe;CAChB;CACA,MAAM;CACN,cAAc;AACf,CACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mysten-incubation/devstack",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Next-generation Sui devstack package.",
5
5
  "keywords": [
6
6
  "sui",