@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 =
|
|
45
|
-
|
|
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
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: '
|
|
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"}
|