@mysten-incubation/devstack 0.7.0 → 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.
Files changed (47) hide show
  1. package/dist/build-integrations/vite/index.mjs +1 -0
  2. package/dist/build-integrations/vite/index.mjs.map +1 -1
  3. package/dist/cli/wirings/apply.mjs +24 -6
  4. package/dist/cli/wirings/apply.mjs.map +1 -1
  5. package/dist/cli/wirings/dump-deployment.mjs +2 -1
  6. package/dist/cli/wirings/dump-deployment.mjs.map +1 -1
  7. package/dist/cli/wirings/identity.mjs +2 -1
  8. package/dist/cli/wirings/identity.mjs.map +1 -1
  9. package/dist/cli/wirings/snapshot.mjs +2 -1
  10. package/dist/cli/wirings/snapshot.mjs.map +1 -1
  11. package/dist/cli/wirings/up.mjs +25 -29
  12. package/dist/cli/wirings/up.mjs.map +1 -1
  13. package/dist/orchestrators/router/file-provider.mjs +52 -10
  14. package/dist/orchestrators/router/file-provider.mjs.map +1 -1
  15. package/dist/plugins/package/codegen.mjs +4 -4
  16. package/dist/plugins/package/codegen.mjs.map +1 -1
  17. package/dist/plugins/package/dep-resolution.mjs +19 -9
  18. package/dist/plugins/package/dep-resolution.mjs.map +1 -1
  19. package/dist/plugins/package/index.d.mts +3 -3
  20. package/dist/plugins/package/index.mjs +9 -5
  21. package/dist/plugins/package/index.mjs.map +1 -1
  22. package/dist/plugins/package/mode-known.mjs +2 -2
  23. package/dist/plugins/package/mode-known.mjs.map +1 -1
  24. package/dist/plugins/package/mode-local.mjs +4 -4
  25. package/dist/plugins/package/mode-local.mjs.map +1 -1
  26. package/dist/plugins/sui/chain-build-container.mjs.map +1 -1
  27. package/dist/plugins/sui/index.mjs.map +1 -1
  28. package/dist/plugins/sui/move/index.mjs +10 -6
  29. package/dist/plugins/sui/move/index.mjs.map +1 -1
  30. package/dist/plugins/sui/routable.mjs +3 -3
  31. package/dist/plugins/sui/routable.mjs.map +1 -1
  32. package/dist/plugins/wallet/codegen.d.mts +14 -10
  33. package/dist/plugins/wallet/codegen.mjs.map +1 -1
  34. package/dist/plugins/wallet/origin-policy.mjs +43 -3
  35. package/dist/plugins/wallet/origin-policy.mjs.map +1 -1
  36. package/dist/plugins/wallet/service.mjs +9 -7
  37. package/dist/plugins/wallet/service.mjs.map +1 -1
  38. package/dist/substrate/cross-process.mjs +2 -1
  39. package/dist/substrate/cross-process.mjs.map +1 -1
  40. package/dist/substrate/runtime/cross-process/roster.mjs +5 -2
  41. package/dist/substrate/runtime/cross-process/roster.mjs.map +1 -1
  42. package/dist/surfaces/cli/commands/supervisor-presence.mjs +4 -2
  43. package/dist/surfaces/cli/commands/supervisor-presence.mjs.map +1 -1
  44. package/dist/surfaces/cli/errors.mjs +12 -2
  45. package/dist/surfaces/cli/errors.mjs.map +1 -1
  46. package/dist/surfaces/cli/index.mjs.map +1 -1
  47. package/package.json +1 -1
@@ -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":";;;;;AAmEA,MAAa,yBAAyB;;;;;;AAOtC,MAAM,2BAA2B;;;;;AAoFjC,MAAa,oBAAoB,WAChC,GAAG,2BAA2B,OAAO;;;;;;;;;AAoCtC,MAAa,gBACZ,UACA,MACA,UACA,cAEA,OAAO,IAAI,aAAa;CACvB,MAAM,aAAa,OAAO,SAAS,OAAO,KAAK,YAAY;CAC3D,MAAM,WAAW,OAAO,eAAe,UAAU,KAAK,WAAW,IAAI;CACrE,MAAM,SAAS,OAAO,eAAe;EAAE;EAAU,UAAU,KAAK;CAAW,CAAC;CAC5E,MAAM,WACL,KAAK,SAAS,SAAS,cACpB,OAAO,UAAU,iBAAiB;EAClC,eAAe,KAAK,SAAS;EAC7B,eAAe,KAAK,SAAS;CAC9B,CAAC,IACA,OAAO,UAAU,oBAAoB,EAAE,MAAM,KAAK,SAAS,KAAK,CAAC;CACrE,MAAM,eACL,KAAK,iBAAiB,QACnB,QACA,KAAK,iBAAiB,QACrB,QACA,KAAK,iBAAiB,UACrB,UACA;CAQN,KAFqC,iBAAiB,QAAQ,QAAQ,aAC7B,WAAW,aAAa,QAAQ,QAAQ,SAEhF,OAAO,OAAO,OAAO,KACpB,IAAI,sBAAsB;EACzB,OAAO;EACP,OAAO,WAAW;EAClB,QACC,0CAA0C,aAAa,oBACxC,WAAW,KAAK,QAAQ,WAAW,SAAS;CAC7D,CAAC,CACF;CAeD,MAAM,cAAc,GAPnB,iBAAiB,QACd,QACA,iBAAiB,UAChB,UACA,iBAAiB,QAChB,QACA,OACgC,KAAK,SAAS,KAAK,GAAG,SAAS;CACrE,IAAI,CAAC,oDAAoD,KAAK,WAAW,GACxE,OAAO,OAAO,OAAO,KACpB,IAAI,sBAAsB;EACzB,OAAO;EACP,OAAO;EACP,QAAQ;CACT,CAAC,CACF;CAID,MAAM,OAAO,KAAK,iBAAiB,QAAQ,QAAQ,KAAK;CACxD,OAAO;EACN,gBAAgB;EAChB;EACA,gBAAgB,WAAW;EAC3B,gBAAgB,WAAW;EAC3B;EACA;EACA;CACD;AACD,CAAC;;;;AASF,MAAM,iBAAiB,QAAgD;CACtE,MAAM,WAAW,IAAI,QAAQ,gCAAgC,EAAE;CAC/D,MAAM,YAAY,SAAS,YAAY,GAAG;CAC1C,OAAO;EACN,MAAM,SAAS,MAAM,GAAG,SAAS;EACjC,MAAM,SAAS,MAAM,YAAY,CAAC;CACnC;AACD;AAEA,MAAM,qBAAqB,UAAqD;CAC/E,wBAAwB,MAAM;CAC9B,sBAAsB,MAAM;CAC5B,eAAe,MAAM;CACrB,iBAAiB,MAAM;CACvB,eAAe,MAAM,MAAM;CAC3B,qBAAqB,MAAM,MAAM;CACjC,oBAAoB,MAAM,MAAM;CAChC,qBAAqB,MAAM,MAAM;CACjC,uBAAuB,MAAM,MAAM;CACnC,kBAAkB,MAAM,MAAM;AAC/B;;;;AAKA,MAAa,mBAAmB,OAAsB,UAAsC;CAC3F,IAAI,MAAM,iBAAiB,OAAO,OAAO,mBAAmB,OAAO,KAAK;CACxE,IAAI,MAAM,iBAAiB,OAAO,OAAO,mBAAmB,OAAO,KAAK;CACxE,OAAO,oBAAoB,OAAO,KAAK;AACxC;AAEA,MAAM,gCAAgC,UACrC,GAAG,MAAM,eAAe;AAEzB,MAAM,uBAAuB,OAAsB,UAAsC;CACxF,MAAM,cAAc,CACnB,6BAA6B,KAAK,GAClC,GAAI,MAAM,OAAO,CAAC,oBAAoB,IAAI,CAAC,CAC5C;CACA,MAAM,aACL,MAAM,iBAAiB,QACpB,+DACA,MAAM,iBAAiB,UACtB,gEACA;CACL,MAAM,mBAAmB,MAAM,iBAAiB,UAAU,gCAAgC;CAC1F,OAAO;EACN;EACA,qBAAqB,MAAM;EAC3B,mBAAmB,MAAM;EACzB,qBAAqB,MAAM;EAC3B,qBAAqB,MAAM;EAC3B,eAAe,MAAM;EACrB,GAAG,kBAAkB,KAAK;EAC1B;EACA;EACA,OAAO,MAAM,eAAe;EAC5B,uBAAuB,MAAM,SAAS;EACtC,wBAAwB,MAAM,eAAe;EAC7C,mBAAmB,MAAM,eAAe;EACxC,uBAAuB,YAAY,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;EACzE;EACA,OAAO,6BAA6B,KAAK,EAAE;EAC3C;EACA;EACA,aAAa,uBAAuB,KAAK,MAAM,eAAe;EAC9D;EACA,OAAO,MAAM,eAAe;EAC5B;EACA,qBAAqB,OAAO,OAAO,8BAA8B,iBAAiB;EAClF,WAAW,SAAS,IAAI,WAAW,QAAQ,IAAI;EAC/C;EACA,qBAAqB,MAAM,YAAY;EACvC,qBAAqB,OAClB,OACA,6BAA6B,iBAAiB;EACjD;CACD,CAAC,CACC,QAAQ,SAAyB,SAAS,IAAI,CAAC,CAC/C,KAAK,IAAI;AACZ;AAEA,MAAM,sBAAsB,OAAsB,UAAsC;CACvF,MAAM,cAAc,CACnB,6BAA6B,KAAK,GAClC,GAAI,MAAM,OAAO,CAAC,oBAAoB,IAAI,CAAC,CAC5C;CACA,MAAM,kBAAkB,MAAM,YAAY,QAAQ,aAAa,SAAS;CACxE,OAAO;EACN;EACA,qBAAqB,MAAM;EAC3B;EACA,qBAAqB,MAAM;EAC3B,qBAAqB,MAAM;EAC3B,eAAe,MAAM;EACrB,GAAG,kBAAkB,KAAK;EAC1B;EACA;EACA,OAAO,MAAM,eAAe;EAC5B,uBAAuB,MAAM,SAAS;EACtC,wBAAwB,MAAM,eAAe;EAC7C,mBAAmB,MAAM,eAAe;EACxC,uBAAuB,YAAY,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;EACzE;EACA,OAAO,MAAM,eAAe;EAC5B,uBAAuB,MAAM,SAAS;EACtC,wBAAwB,MAAM,eAAe;EAC7C,mBAAmB,MAAM,eAAe;EACxC,uBAAuB,YAAY,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;EACzE;EACA;EACA,OAAO,6BAA6B,KAAK,EAAE;EAC3C;EACA;EACA,aAAa,uBAAuB,KAAK,MAAM,eAAe;EAC9D;EACA,OAAO,MAAM,eAAe;EAC5B;EACA;EACA;EACA;EACA,qBAAqB,MAAM,YAAY;EACvC,OAAO,MAAM,eAAe;EAC5B;EACA;EACA;EACA;EACA,qBAAqB,gBAAgB;EACrC;CACD,CAAC,CAAC,KAAK,IAAI;AACZ;AAEA,MAAM,sBAAsB,OAAsB,UAAsC;CACvF,MAAM,EAAE,MAAM,SAAS,cAAc,MAAM,WAAW;CACtD,OAAO;EACN;EACA,qBAAqB,MAAM;EAC3B;EACA,qBAAqB,MAAM;EAC3B,qBAAqB,MAAM;EAC3B,eAAe,MAAM;EACrB;EACA,GAAG,kBAAkB,KAAK;EAC1B;EACA;EACA,OAAO,MAAM,eAAe;EAC5B;EACA,wBAAwB,MAAM,eAAe;EAC7C,mBAAmB,MAAM,eAAe;EACxC;EACA,OAAO,MAAM,eAAe;EAC5B;EACA;EACA,yBAAyB,KAAK,GAAG,KAAK;EACtC;CACD,CAAC,CAAC,KAAK,IAAI;AACZ;AAEA,MAAa,8BAA8B,aAAoC;CAI9E,MAAM,gBAAgB,yBAAyB,QAAQ,uBAAuB,MAAM;CAEpF,OADc,IAAI,OAAO,IAAI,cAAc,YAAY,CAAC,CAAC,KAAK,QACnD,CAAC,GAAG,MAAM;AACtB;AAEA,MAAM,gBAAgB,MAAc,QAA+B;CAClE,MAAM,UAAU,IAAI,QAAQ,uBAAuB,MAAM;CAEzD,OADc,IAAI,OAAO,MAAM,QAAQ,UAAU,GAAG,CAAC,CAAC,KAAK,IAChD,CAAC,GAAG,EAAE,EAAE,KAAK,KAAK;AAC9B;AAEA,MAAM,cAAc,MAAc,YACjC,QAAQ,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,KAAK;AAEpC,MAAM,iBAAiB,MAAc,QAA+B;CACnE,MAAM,MAAM,aAAa,MAAM,GAAG;CAClC,IAAI,QAAQ,QAAQ,CAAC,QAAQ,KAAK,GAAG,GAAG,OAAO;CAC/C,OAAO,OAAO,SAAS,KAAK,EAAE;AAC/B;AAEA,MAAM,qBAAqB,SAA8C;CACxE,MAAM,cAAc,aAAa,MAAM,cAAc;CACrD,IACC,gBAAgB,UAChB,gBAAgB,SAChB,gBAAgB,WAChB,gBAAgB,OAEhB,OAAO;CAER,IAAI,SAAS,KAAK,IAAI,GAAG,OAAO;CAChC,IAAI,UAAU,KAAK,IAAI,GAAG,OAAO;CACjC,OAAO;AACR;AAEA,MAAM,yBAAyB,SAC9B;CACC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD,CAAC,CAAC,MAAM,QAAQ,aAAa,MAAM,GAAG,MAAM,IAAI;AAEjD,MAAM,2BACL,MACA,mBAII;CACJ,IAAI,CAAC,sBAAsB,IAAI,GAAG,OAAO;EAAE,OAAO;EAAM,YAAY;CAAK;CACzE,MAAM,aAAa,aAAa,MAAM,mBAAmB;CACzD,MAAM,UAAU,cAAc,MAAM,mBAAmB;CACvD,MAAM,kBAAkB,aAAa,MAAM,iBAAiB;CAC5D,MAAM,MAAM,aAAa,MAAM,UAAU;CACzC,MAAM,QAAQ,aAAa,MAAM,YAAY;CAC7C,MAAM,MAAM,cAAc,MAAM,UAAU;CAC1C,MAAM,YAAY,cAAc,MAAM,gBAAgB;CACtD,MAAM,WAAW,aAAa,MAAM,eAAe;CACnD,MAAM,YAAY,cAAc,MAAM,gBAAgB;CACtD,MAAM,cAAc,cAAc,MAAM,kBAAkB;CAC1D,MAAM,SAAS,aAAa,MAAM,aAAa;CAC/C,IAAI,YAAA,GACH,OAAO;EACN,OAAO;EACP,YAAY;GACX,MAAM;GACN;GACA,QAAQ;GACR,QAAQ,qCAAiE,cAAc;EACxF;CACD;CAED,IACC,oBAAoB,QACpB,QAAQ,QACR,UAAU,QACV,QAAQ,QACR,cAAc,QACd,aAAa,QACb,cAAc,QACd,gBAAgB,QACf,WAAW,YAAY,WAAW,YAEnC,OAAO;EACN,OAAO;EACP,YAAY;GACX,MAAM;GACN;GACA,QAAQ;GACR,QAAQ;EACT;CACD;CAED,OAAO;EACN,OAAO;GACN,SAAA;GACA;GACA;GACA;GACA,OAAO;IACN;IACA;IACA;IACA;IACA;IACA;GACD;EACD;EACA,YAAY;CACb;AACD;AAEA,MAAa,0BACZ,MACA,yBAAwC,SACV;CAC9B,MAAM,iBAAiB,aAAa,MAAM,gBAAgB,KAAK;CAC/D,MAAM,eAAe,kBAAkB,IAAI;CAC3C,MAAM,iBACL,aAAa,MAAM,gBAAgB,KAAK,WAAW,MAAM,4BAA4B;CACtF,MAAM,WACL,aAAa,MAAM,UAAU,KAC7B,WAAW,MAAM,mBAAmB,MACnC,iBAAiB,QAAQ,KAAK;CAChC,MAAM,UAAU,aAAa,MAAM,gBAAgB;CACnD,MAAM,iBACL,YAAY,QAAQ,CAAC,QAAQ,KAAK,OAAO,IAAI,OAAO,OAAO,SAAS,SAAS,EAAE;CAEhF,IACC,mBAAmB,QACnB,iBAAiB,QACjB,mBAAmB,QACnB,aAAa,MACZ;EACD,MAAM,0BAA0B,kBAAkB;EAClD,OAAO;GACN,MAAM;GACN,gBAAgB;GAChB,aAAa,CACZ;IACC,MAAM;IACN,gBAAgB;IAChB,QAAQ;IACR,QACC;GACF,CACD;EACD;CACD;CACA,MAAM,cAAc,wBAAwB,MAAM,cAAc;CAEhE,OAAO;EACN,MAAM;EACN,OAAO;GACN;GACA;GACA;GACA;GACA;GACA,OAAO,YAAY;EACpB;EACA,aAAa,YAAY,eAAe,OAAO,CAAC,IAAI,CAAC,YAAY,UAAU;CAC5E;AACD;;;;;;;;;;;;;AA0BA,MAAa,oBACZ,WAC2B;CAC3B,MAAM,uBAAO,IAAI,IAA2C;CAC5D,KAAK,MAAM,KAAK,QAAQ;EACvB,MAAM,UACL,EAAE,mBAAmB,OAAO,cAAc,EAAE,mBAAmB,QAAQ,EAAE;EAC1E,MAAM,MAAM,EAAE,iBAAiB,QAAQ,OAAO,YAAY,QAAQ,QAAQ,GAAG,EAAE;EAC/E,MAAM,MAAM,KAAK,IAAI,GAAG;EACxB,IAAI,KAAK,IAAI,KAAK,CAAC;OACd,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC;CACvB;CACA,KAAK,MAAM,CAAC,KAAK,cAAc,MAC9B,IAAI,UAAU,SAAS,GAAG;EACzB,MAAM,MAAM,UAAU,KAAK,UAAU,MAAM,cAAc;EACzD,MAAM,QAAQ,UAAU;EACxB,IAAI,IAAI,WAAW,MAAM,GACxB,OAAO,IAAI,eAAe;GACzB,SAAS,sBAAsB;IAC9B,UAAU;IACV,YAAY,OAAO,kBAAkB;IACrC,aAAa;IACb,cAAc;GACf,CAAC;GACD,UAAU;GACV,YAAY,OAAO,kBAAkB;GACrC,aAAa;EACd,CAAC;EAEF,OAAO,IAAI,eAAe;GACzB,SAAS,sBAAsB;IAC9B,UAAU,OAAO,YAAY;IAC7B,YAAY,OAAO,kBAAkB;IACrC,aAAa;IACb,cAAc;GACf,CAAC;GACD,UAAU,OAAO,YAAY;GAC7B,YAAY,OAAO,kBAAkB;GACrC,aAAa;EACd,CAAC;CACF;CAED,OAAO;AACR;AAEA,MAAM,yBAAyB,cAKjB;CACb,MAAM,MAAM,UAAU,YAAY,KAAK,IAAI;CAC3C,IAAI,UAAU,iBAAiB,OAC9B,OAAO,6CAA6C,UAAU,WAAW,sBAAsB;CAEhG,OACC,yCAAyC,UAAU,WAAW,kBAC7C,UAAU,SAAS,sBAAsB;AAE5D"}
@@ -1,11 +1,11 @@
1
1
  import { configCodegenable } from "../../contracts/config-bindings.mjs";
2
- import { mvrNamedForm, mvrNamedFormFrom } from "./dep-resolution.mjs";
2
+ import { mvrNamedFormFrom, normalizeMvrPlaceholder } from "./dep-resolution.mjs";
3
3
  //#region src/plugins/package/codegen.ts
4
4
  /** Validate + normalize a declared MVR-type suffix. Entries are
5
5
  * `'<module>::<Name>'` relative to the package; we reject anything that does
6
6
  * not parse as two `::`-joined Move identifiers so a typo fails at config time
7
7
  * rather than emitting an `isValidNamedType`-rejected key into the override
8
- * map. The `@local/<slug>::` prefix is implied (and stripped if the developer
8
+ * map. The package MVR prefix is implied (and stripped if the developer
9
9
  * redundantly included it). */
10
10
  const normalizeMvrTypeSuffix = (packageName, mvr, entry) => {
11
11
  const suffix = entry.startsWith(`${mvr}::`) ? entry.slice(mvr.length + 2) : entry;
@@ -169,7 +169,7 @@ const makeKnownCodegenable = (resolved, options = {}) => {
169
169
  * compiles (a local `sourcePath`; a git source whose tree has not been
170
170
  * materialized contributes `null`, so the bindings step skips it). */
171
171
  const makeLocalStaticCodegen = (config) => {
172
- const mvrPlaceholder = mvrNamedForm(config.mvrPlaceholder ?? config.name);
172
+ const mvrPlaceholder = normalizeMvrPlaceholder(config.name, config.mvrPlaceholder);
173
173
  return () => {
174
174
  return [packageDecl(packageConfigBindings({
175
175
  name: config.name,
@@ -190,7 +190,7 @@ const makeLocalStaticCodegen = (config) => {
190
190
  * packages already carry a literal `packageId` in config; the committed stub
191
191
  * emits it verbatim. */
192
192
  const makeKnownStaticCodegen = (config) => {
193
- const mvrPlaceholder = mvrNamedForm(config.mvrPlaceholder ?? config.name);
193
+ const mvrPlaceholder = normalizeMvrPlaceholder(config.name, config.mvrPlaceholder);
194
194
  return () => {
195
195
  return [packageDecl(packageConfigBindings({
196
196
  name: config.name,
@@ -1 +1 @@
1
- {"version":3,"file":"codegen.mjs","names":[],"sources":["../../../src/plugins/package/codegen.ts"],"sourcesContent":["// Package plugin — Codegenable contribution, via the UNIFIED config-binding\n// declaration.\n//\n// Distilled doc §Outputs: \"Bindings input — the source path of every local\n// Package is read by the bindings emitter (KnownPackages filtered out).\"\n//\n// The contribution emitted here is the LIGHTWEIGHT one: package id + MVR\n// placeholder + (for local packages) the source path the bindings emitter\n// consumes. The HEAVY codegen — `@mysten/codegen` emitting typed function\n// shims — happens in the codegen ORCHESTRATOR (NOT this plugin). This file\n// declares the SEAM, not the binding bytes.\n//\n// ONE declaration, TWO derivations. A package declares its `config.ts`\n// contributions ONCE as a `ConfigBindingSet`; the framework's\n// `projectLiveConfig` / `projectStaticConfig` derive both behaviors:\n// - LIVE (boot): bakes the resolved package id literal — boot's\n// `assembleDeployment` reads it into the loadable deployment.\n// - STATIC (committed-tree): emits `requireId(dep, '<mvr>')` so the committed\n// `config.ts` carries NO on-chain id (resolved at app build/dev time).\n//\n// The package contribution is `aggregateOnly`: it projects into the combined\n// `generated/config.ts` (`config.packages.<name>` + top-level\n// `config.objects.<name>`) and emits NO standalone `package/<name>.ts`. The\n// `packageBindings` value is still exported on the emit context so the\n// orchestrator's `isPackageBindings` seam forwards it to the Move-bindings\n// emitter (bindings stay in `generated/bindings/`).\n\nimport type { CodegenableDecl, StaticCodegenSource } from '../../contracts/codegenable.ts';\nimport {\n\tconfigCodegenable,\n\ttype ConfigBinding,\n\ttype ConfigBindingSet,\n} from '../../contracts/config-bindings.ts';\nimport type { PackageBindings } from '../../orchestrators/codegen/bindings.ts';\nimport type { JsonValue } from '../../orchestrators/codegen/deployment.ts';\nimport { mvrNamedForm, mvrNamedFormFrom } from './dep-resolution.ts';\nimport type { ResolvedLocalPackage, ResolvedKnownPackage } from './registry.ts';\n\n/** Codegenable shape — what each Package contributes to the codegen\n * orchestrator. Defined once on the orchestrator's `emitBindings` consumer\n * contract (`orchestrators/codegen/bindings.ts`); re-exported here as the\n * package plugin's public surface. */\nexport type { PackageBindings };\n\n/** The typed shape one `config.packages.<name>` entry exports. Per-network\n * package ids now live in the injected deployment envelope (live local +\n * committed `deployments/<net>.ts`), resolved via\n * `config.forNetwork(net).packages.<name>.id` — so this entry carries only\n * the MVR placeholder + the default-network convenience id. */\nexport interface PackageConfigEntry {\n\treadonly mvr: string;\n\t/** The default (local) network's resolved id. */\n\treadonly packageId: string;\n\t/** Resolved (local) object ids for the default network. Present only\n\t * when at least one object is known. */\n\treadonly objects?: Readonly<Record<string, string>>;\n}\n\n/** The state the LIVE binding derivation reads — the resolved package's\n * active-network id + captured object ids. */\ninterface PackageLiveState {\n\treadonly packageId: string;\n\treadonly captured: Readonly<Record<string, string>>;\n}\n\ninterface PackageBindingInput {\n\treadonly name: string;\n\treadonly mvrPlaceholder: string;\n\t/** Pinned literal id (KNOWN package with a declared id). When set, the\n\t * active-network id is a LITERAL binding (identical in both paths). When\n\t * absent (a LOCAL package, or a KNOWN-shaped local stub), the id is a\n\t * RESOLVED binding: static emits `requireId(dep, '<mvr>')`, live computes the\n\t * real resolved id from acquired state. */\n\treadonly pinnedId?: string | undefined;\n\t/** Resolved local object captures (keyed by user `capture` name).\n\t * Surfaced into `config.objects.<name>` + `packages.<name>.objects`\n\t * for the active (local) network. Empty on the committed-stub path —\n\t * the captured IDS are loaded config data (resolved at app build/dev\n\t * time), so the static path emits `requireValue` from `objectKeys`. */\n\treadonly captured: Readonly<Record<string, string>>;\n\t/** The capture KEYS this package declares (the user `capture` option's\n\t * key set, known at config time). Drives the object-id bindings on BOTH\n\t * paths so the committed stub carries `requireValue(dep, 'package:<name>:\n\t * objects', '<key>')` references rather than a live-only `objects` field.\n\t * When omitted, falls back to the keys present in `captured` (live path).\n\t */\n\treadonly objectKeys?: ReadonlyArray<string> | undefined;\n\t/** OPT-IN Move datatypes to expose as MVR `types` overrides. Each entry is a\n\t * `'<module>::<Name>'` suffix relative to this package (the `@local/<slug>`\n\t * prefix is implied). Each emits one\n\t * `mvrOverrides.types['@local/<slug>::<module>::<Name>']` whose static value\n\t * resolves per-network as `` `${requireId(dep, \"<mvr>\")}::<module>::<Name>` ``.\n\t * Absent / empty ⇒ no `types` entries (the orchestrator emits `types: {}`).\n\t * Identical on the live and static paths (config-known, resolution-\n\t * independent); the live deployment slice ignores the value (keeps\n\t * `types: {}`). */\n\treadonly mvrTypes?: ReadonlyArray<string> | undefined;\n}\n\n/** Validate + normalize a declared MVR-type suffix. Entries are\n * `'<module>::<Name>'` relative to the package; we reject anything that does\n * not parse as two `::`-joined Move identifiers so a typo fails at config time\n * rather than emitting an `isValidNamedType`-rejected key into the override\n * map. The `@local/<slug>::` prefix is implied (and stripped if the developer\n * redundantly included it). */\nconst normalizeMvrTypeSuffix = (packageName: string, mvr: string, entry: string): string => {\n\t// Tolerate a redundant `@local/<slug>::` prefix the developer may have\n\t// pasted from a fully-qualified tag.\n\tconst suffix = entry.startsWith(`${mvr}::`) ? entry.slice(mvr.length + 2) : entry;\n\tif (!/^[A-Za-z_][\\w]*::[A-Za-z_][\\w]*$/.test(suffix)) {\n\t\tthrow new Error(\n\t\t\t`localPackage('${packageName}') mvrTypes entry '${entry}' must be '<module>::<Name>' ` +\n\t\t\t\t`(two Move identifiers joined by '::'); the '${mvr}' package prefix is implied.`,\n\t\t);\n\t}\n\treturn suffix;\n};\n\n/**\n * Build the package's config-binding set, declared ONCE. The `name` keys the\n * `config.packages.<name>` entry; `mvrPlaceholder` is a literal in both paths;\n * the active-network id is a RESOLVED binding (sugar `requireId(dep, '<mvr>')` when\n * `resolveViaRuntime`, otherwise a literal already-known id). Declared\n * per-network literals (testnet/mainnet) and captured objects are literals.\n *\n * The same set drives `projectStaticConfig` (committed tree) and\n * `projectLiveConfig` (boot deployment) — no parallel projectors.\n */\nconst packageConfigBindings = (input: PackageBindingInput): ConfigBindingSet<PackageLiveState> => {\n\tconst { name, mvrPlaceholder } = input;\n\n\tconst bindings: Array<ConfigBinding<PackageLiveState>> = [];\n\n\t// `packages.<name>.mvr` — pure literal in both paths.\n\tbindings.push({\n\t\tvariant: 'literal',\n\t\tconfigPath: ['packages', name, 'mvr'],\n\t\tvalue: mvrPlaceholder,\n\t});\n\n\t// The active-network id binding. Two cases:\n\t// - no pinned id (LOCAL, or KNOWN-shaped local stub) → RESOLVED (sugar\n\t// `requireId(dep, '<mvr>')` static; live = the concrete resolved id from\n\t// `state.packageId`).\n\t// - pinned literal (KNOWN with a declared id) → LITERAL (the id stands\n\t// identically in both paths).\n\t// Reused for `packages.<name>.packageId` and the `mvrOverrides` entry so\n\t// both agree.\n\tconst pinned = input.pinnedId;\n\tconst idBinding = (configPath: ReadonlyArray<string>): ConfigBinding<PackageLiveState> =>\n\t\tpinned === undefined\n\t\t\t? {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath,\n\t\t\t\t\tnamespace: 'package',\n\t\t\t\t\tkey: `${name}:packageId`,\n\t\t\t\t\tsugar: { kind: 'id', mvrPlaceholder },\n\t\t\t\t\tlive: (state) => state.packageId,\n\t\t\t\t}\n\t\t\t: { variant: 'literal', configPath, value: pinned };\n\n\tbindings.push(idBinding(['packages', name, 'packageId']));\n\n\t// Active-network objects — captured ids (local). The captured ids are\n\t// LOADED CONFIG DATA, so each is a RESOLVED binding on the generic\n\t// `requireValue(dep, 'package:<name>:objects', '<key>')` channel: the static\n\t// committed stub emits the resolver expr, the live path bakes the real\n\t// captured id AND feeds the deployment `values` channel. The key set comes\n\t// from `objectKeys` (config-known) so BOTH paths emit identical paths —\n\t// no live-only `objects` field. Falls back to the live capture keys when\n\t// `objectKeys` is absent (the live emit path).\n\tconst objectKeys = input.objectKeys ?? Object.keys(input.captured);\n\tif (objectKeys.length > 0) {\n\t\tconst objectsNamespace = `package:${name}:objects`;\n\t\tfor (const objectKey of objectKeys) {\n\t\t\tconst objectBinding = (\n\t\t\t\tconfigPath: ReadonlyArray<string>,\n\t\t\t): ConfigBinding<PackageLiveState> => ({\n\t\t\t\tvariant: 'resolved',\n\t\t\t\tconfigPath,\n\t\t\t\tnamespace: objectsNamespace,\n\t\t\t\tkey: objectKey,\n\t\t\t\tlive: (state) => (state.captured[objectKey] ?? null) as JsonValue,\n\t\t\t});\n\t\t\tbindings.push(objectBinding(['packages', name, 'objects', objectKey]));\n\t\t\t// Top-level `objects.<name>.<key>` mirror.\n\t\t\tbindings.push(objectBinding(['objects', name, objectKey]));\n\t\t}\n\t}\n\n\t// Active-network MVR override entry — `mvrOverrides.packages.<mvr> = <active\n\t// id>` (the @mysten MVR override shape's `packages` map). The sibling `types`\n\t// map is OPT-IN — populated below ONLY from developer-declared `mvrTypes`\n\t// (the change from auto-enumerating every package type). A resolved binding\n\t// always emits (the resolver expr / live id); a pinned literal emits only\n\t// when non-empty.\n\tif (pinned === undefined || pinned.length > 0) {\n\t\tbindings.push(idBinding(['mvrOverrides', 'packages', mvrPlaceholder]));\n\t}\n\n\t// OPT-IN MVR `types` overrides — `mvrOverrides.types['<mvr>::<module>::<Name>']`\n\t// for each developer-declared `<module>::<Name>`. The static path emits the\n\t// per-network type tag `` `${requireId(dep, \"<mvr>\")}::<module>::<Name>` ``\n\t// (the `mvrType` sugar); the live path's value is ignored by the live\n\t// deployment slice (`deploymentFromBucket` keeps `types: {}`). Sugar bindings\n\t// never touch the generic `values` channel. No declared types ⇒ no entries\n\t// here (the orchestrator emits `types: {}`).\n\tfor (const entry of input.mvrTypes ?? []) {\n\t\tconst typeSuffix = normalizeMvrTypeSuffix(name, mvrPlaceholder, entry);\n\t\tconst tag = `${mvrPlaceholder}::${typeSuffix}`;\n\t\tbindings.push({\n\t\t\tvariant: 'resolved',\n\t\t\tconfigPath: ['mvrOverrides', 'types', tag],\n\t\t\tnamespace: 'package',\n\t\t\tkey: `${name}:mvrType:${tag}`,\n\t\t\tsugar: { kind: 'mvrType', mvrPlaceholder, typeSuffix },\n\t\t\t// Ignored by the live deployment slice; a value is required by the\n\t\t\t// binding shape. The resolved per-network tag is what the static\n\t\t\t// committed `config.ts` emits via the sugar.\n\t\t\tlive: () => tag,\n\t\t});\n\t}\n\n\treturn {\n\t\tbucket: 'config.ts',\n\t\tkind: 'package',\n\t\temitterName: 'package',\n\t\t// One Package contribution per published package — shared `'package'`\n\t\t// emitter name is by-design (the orchestrator skips its uniqueness\n\t\t// check for this flag).\n\t\tallowEmitterNameRepetition: true,\n\t\tbindings,\n\t};\n};\n\n/**\n * Build the package's `CodegenableDecl` from its binding set via the unified\n * `configCodegenable` derivation. Mode `'live'` bakes concrete values + feeds\n * the deployment; `'static'` emits resolver expressions. The decl ALSO exports\n * `packageBindings` (the `extraExports` hook) so the orchestrator's\n * `isPackageBindings` seam forwards it to the Move-bindings emitter (bindings\n * stay in `generated/bindings/`).\n */\nconst packageDecl = (\n\tset: ConfigBindingSet<PackageLiveState>,\n\tbindings: PackageBindings,\n\thow: 'static' | { readonly mode: 'live'; readonly state: PackageLiveState },\n): CodegenableDecl<'package'> =>\n\tconfigCodegenable<PackageLiveState, 'package'>(set, how, {\n\t\textraExports: { packageBindings: bindings },\n\t});\n\n/** Build the Codegenable contribution for a local package (LIVE path).\n * Bakes the resolved active-network id + captured objects. */\nexport const makeLocalCodegenable = (\n\tresolved: ResolvedLocalPackage,\n\toptions: {\n\t\treadonly excluded: boolean;\n\t\t/** OPT-IN MVR `types` to expose — `'<module>::<Name>'` suffixes. */\n\t\treadonly mvrTypes?: ReadonlyArray<string> | undefined;\n\t},\n): CodegenableDecl<'package'> => {\n\t// Coerce the resolved placeholder into the current `@local/<slug>` named\n\t// form at the emit seam rather than trusting it verbatim — a cache-served\n\t// `mvrPlaceholder` can be a STALE BARE slug. `mvrNamedFormFrom` is pure +\n\t// deterministic; computed ONCE so the binding default and `config.mvr`\n\t// stay equal. A LOCAL package has NO pinned id — the active-network id is\n\t// always a RESOLVED binding (live bakes the real id; static emits\n\t// `requireId`).\n\tconst mvrPlaceholder = mvrNamedFormFrom(resolved.mvrPlaceholder);\n\tconst set = packageConfigBindings({\n\t\tname: resolved.name,\n\t\tmvrPlaceholder,\n\t\tcaptured: resolved.captured,\n\t\t...(options.mvrTypes !== undefined ? { mvrTypes: options.mvrTypes } : {}),\n\t});\n\tconst bindings: PackageBindings = {\n\t\tname: resolved.name,\n\t\tpackageId: resolved.packageId,\n\t\tmvrPlaceholder,\n\t\tsourcePath: resolved.sourcePath,\n\t\texcluded: options.excluded,\n\t};\n\treturn packageDecl(set, bindings, {\n\t\tmode: 'live',\n\t\tstate: { packageId: resolved.packageId, captured: resolved.captured },\n\t});\n};\n\n/** Build the Codegenable contribution for a known package — `sourcePath:\n * null`, no captured object ids. A KNOWN package's declared id is a PINNED\n * LITERAL (not loaded runtime data), so it renders identically in both the\n * live and static paths. */\nexport const makeKnownCodegenable = (\n\tresolved: ResolvedKnownPackage,\n\toptions: { readonly mvrTypes?: ReadonlyArray<string> | undefined } = {},\n): CodegenableDecl<'package'> => {\n\t// Defensive parity with the local emit seam: coerce to the current\n\t// `@local/<slug>` named form (preserving an already-named override).\n\tconst mvrPlaceholder = mvrNamedFormFrom(resolved.mvrPlaceholder);\n\tconst set = packageConfigBindings({\n\t\tname: resolved.name,\n\t\tmvrPlaceholder,\n\t\tcaptured: {},\n\t\tpinnedId: resolved.packageId,\n\t\t...(options.mvrTypes !== undefined ? { mvrTypes: options.mvrTypes } : {}),\n\t});\n\tconst bindings: PackageBindings = {\n\t\tname: resolved.name,\n\t\tpackageId: resolved.packageId,\n\t\tmvrPlaceholder,\n\t\tsourcePath: null,\n\t\texcluded: true, // implicit — KnownPackages never emit bindings.\n\t};\n\t// A pinned literal renders identically in both paths; derive as `static`\n\t// (the literal bindings ignore live state).\n\treturn packageDecl(set, bindings, 'static');\n};\n\n// ---------------------------------------------------------------------------\n// Stack-free codegen derivation (the `codegen` verb)\n//\n// The live `start` body resolves a package (publish/verify) and feeds the\n// resolved value into `makeLocalCodegenable` / `makeKnownCodegenable`. The\n// `staticCodegen` sources below reconstruct the SAME decls from CONFIG ALONE\n// — no chain, no publish — drawing the `packageId` from the projection\n// id-resolver (sentinel for `'placeholder'`, declared id for `'known'`). The\n// decl SHAPE is identical to the live path.\n// ---------------------------------------------------------------------------\n\n/** Build the static (stack-free) codegen source for a LOCAL package.\n * `sourcePath` is the resolved on-disk Move tree the bindings emitter\n * compiles (a local `sourcePath`; a git source whose tree has not been\n * materialized contributes `null`, so the bindings step skips it). */\nexport const makeLocalStaticCodegen = (config: {\n\treadonly name: string;\n\treadonly sourcePath: string | null;\n\treadonly mvrPlaceholder?: string | undefined;\n\treadonly excluded: boolean;\n\t/** Capture KEYS declared by the user `capture` option (config-known).\n\t * The static stub emits `requireValue(dep, 'package:<name>:objects', '<key>')`\n\t * for each so the committed tree carries object-id references with NO\n\t * baked id and NO live-only `objects` field. */\n\treadonly objectKeys?: ReadonlyArray<string> | undefined;\n\t/** OPT-IN MVR `types` to expose — `'<module>::<Name>'` suffixes. */\n\treadonly mvrTypes?: ReadonlyArray<string> | undefined;\n}): StaticCodegenSource => {\n\tconst mvrPlaceholder = mvrNamedForm(config.mvrPlaceholder ?? config.name);\n\treturn () => {\n\t\t// A LOCAL package has no pinned id — resolve the active id at app\n\t\t// build/dev time, never embed it in the committed tree. A git source\n\t\t// whose tree has NOT been materialized has no local path, so the\n\t\t// bindings emitter can't compile it; the `sourcePath` carries through\n\t\t// (null → bindings step skips it) but the `config.ts` entry is the same.\n\t\tconst set = packageConfigBindings({\n\t\t\tname: config.name,\n\t\t\tmvrPlaceholder,\n\t\t\tcaptured: {},\n\t\t\t...(config.objectKeys !== undefined ? { objectKeys: config.objectKeys } : {}),\n\t\t\t...(config.mvrTypes !== undefined ? { mvrTypes: config.mvrTypes } : {}),\n\t\t\t// No pinned id — the active id resolves at app build/dev time.\n\t\t});\n\t\tconst bindings: PackageBindings = {\n\t\t\tname: config.name,\n\t\t\t// No live publish → the committed stub carries no id (the resolver\n\t\t\t// fills it at app build/dev time). Carry the sentinel-free name.\n\t\t\tpackageId: '',\n\t\t\tmvrPlaceholder,\n\t\t\tsourcePath: config.sourcePath,\n\t\t\texcluded: config.sourcePath === null ? true : config.excluded,\n\t\t};\n\t\treturn [packageDecl(set, bindings, 'static')];\n\t};\n};\n\n/** Build the static (stack-free) codegen source for a KNOWN package. Known\n * packages already carry a literal `packageId` in config; the committed stub\n * emits it verbatim. */\nexport const makeKnownStaticCodegen = (config: {\n\treadonly name: string;\n\treadonly packageId: string;\n\treadonly upgradeCapId?: string | undefined;\n\treadonly mvrPlaceholder?: string | undefined;\n\t/** OPT-IN MVR `types` to expose — `'<module>::<Name>'` suffixes. */\n\treadonly mvrTypes?: ReadonlyArray<string> | undefined;\n}): StaticCodegenSource => {\n\tconst mvrPlaceholder = mvrNamedForm(config.mvrPlaceholder ?? config.name);\n\treturn () => {\n\t\tconst set = packageConfigBindings({\n\t\t\tname: config.name,\n\t\t\tmvrPlaceholder,\n\t\t\tcaptured: {},\n\t\t\tpinnedId: config.packageId,\n\t\t\t...(config.mvrTypes !== undefined ? { mvrTypes: config.mvrTypes } : {}),\n\t\t});\n\t\tconst bindings: PackageBindings = {\n\t\t\tname: config.name,\n\t\t\tpackageId: config.packageId,\n\t\t\tmvrPlaceholder,\n\t\t\tsourcePath: null,\n\t\t\texcluded: true,\n\t\t};\n\t\t// A known package's literal id renders the same in both paths — derive\n\t\t// the decl as `static` (the literal bindings ignore live state).\n\t\treturn [packageDecl(set, bindings, 'static')];\n\t};\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"codegen.mjs","names":[],"sources":["../../../src/plugins/package/codegen.ts"],"sourcesContent":["// Package plugin — Codegenable contribution, via the UNIFIED config-binding\n// declaration.\n//\n// Distilled doc §Outputs: \"Bindings input — the source path of every local\n// Package is read by the bindings emitter (KnownPackages filtered out).\"\n//\n// The contribution emitted here is the LIGHTWEIGHT one: package id + MVR\n// placeholder + (for local packages) the source path the bindings emitter\n// consumes. The HEAVY codegen — `@mysten/codegen` emitting typed function\n// shims — happens in the codegen ORCHESTRATOR (NOT this plugin). This file\n// declares the SEAM, not the binding bytes.\n//\n// ONE declaration, TWO derivations. A package declares its `config.ts`\n// contributions ONCE as a `ConfigBindingSet`; the framework's\n// `projectLiveConfig` / `projectStaticConfig` derive both behaviors:\n// - LIVE (boot): bakes the resolved package id literal — boot's\n// `assembleDeployment` reads it into the loadable deployment.\n// - STATIC (committed-tree): emits `requireId(dep, '<mvr>')` so the committed\n// `config.ts` carries NO on-chain id (resolved at app build/dev time).\n//\n// The package contribution is `aggregateOnly`: it projects into the combined\n// `generated/config.ts` (`config.packages.<name>` + top-level\n// `config.objects.<name>`) and emits NO standalone `package/<name>.ts`. The\n// `packageBindings` value is still exported on the emit context so the\n// orchestrator's `isPackageBindings` seam forwards it to the Move-bindings\n// emitter (bindings stay in `generated/bindings/`).\n\nimport type { CodegenableDecl, StaticCodegenSource } from '../../contracts/codegenable.ts';\nimport {\n\tconfigCodegenable,\n\ttype ConfigBinding,\n\ttype ConfigBindingSet,\n} from '../../contracts/config-bindings.ts';\nimport type { PackageBindings } from '../../orchestrators/codegen/bindings.ts';\nimport type { JsonValue } from '../../orchestrators/codegen/deployment.ts';\nimport { mvrNamedFormFrom, normalizeMvrPlaceholder } from './dep-resolution.ts';\nimport type { ResolvedLocalPackage, ResolvedKnownPackage } from './registry.ts';\n\n/** Codegenable shape — what each Package contributes to the codegen\n * orchestrator. Defined once on the orchestrator's `emitBindings` consumer\n * contract (`orchestrators/codegen/bindings.ts`); re-exported here as the\n * package plugin's public surface. */\nexport type { PackageBindings };\n\n/** The typed shape one `config.packages.<name>` entry exports. Per-network\n * package ids now live in the injected deployment envelope (live local +\n * committed `deployments/<net>.ts`), resolved via\n * `config.forNetwork(net).packages.<name>.id` — so this entry carries only\n * the MVR placeholder + the default-network convenience id. */\nexport interface PackageConfigEntry {\n\treadonly mvr: string;\n\t/** The default (local) network's resolved id. */\n\treadonly packageId: string;\n\t/** Resolved (local) object ids for the default network. Present only\n\t * when at least one object is known. */\n\treadonly objects?: Readonly<Record<string, string>>;\n}\n\n/** The state the LIVE binding derivation reads — the resolved package's\n * active-network id + captured object ids. */\ninterface PackageLiveState {\n\treadonly packageId: string;\n\treadonly captured: Readonly<Record<string, string>>;\n}\n\ninterface PackageBindingInput {\n\treadonly name: string;\n\treadonly mvrPlaceholder: string;\n\t/** Pinned literal id (KNOWN package with a declared id). When set, the\n\t * active-network id is a LITERAL binding (identical in both paths). When\n\t * absent (a LOCAL package, or a KNOWN-shaped local stub), the id is a\n\t * RESOLVED binding: static emits `requireId(dep, '<mvr>')`, live computes the\n\t * real resolved id from acquired state. */\n\treadonly pinnedId?: string | undefined;\n\t/** Resolved local object captures (keyed by user `capture` name).\n\t * Surfaced into `config.objects.<name>` + `packages.<name>.objects`\n\t * for the active (local) network. Empty on the committed-stub path —\n\t * the captured IDS are loaded config data (resolved at app build/dev\n\t * time), so the static path emits `requireValue` from `objectKeys`. */\n\treadonly captured: Readonly<Record<string, string>>;\n\t/** The capture KEYS this package declares (the user `capture` option's\n\t * key set, known at config time). Drives the object-id bindings on BOTH\n\t * paths so the committed stub carries `requireValue(dep, 'package:<name>:\n\t * objects', '<key>')` references rather than a live-only `objects` field.\n\t * When omitted, falls back to the keys present in `captured` (live path).\n\t */\n\treadonly objectKeys?: ReadonlyArray<string> | undefined;\n\t/** OPT-IN Move datatypes to expose as MVR `types` overrides. Each entry is a\n\t * `'<module>::<Name>'` suffix relative to this package (the package MVR\n\t * prefix is implied). Each emits one\n\t * `mvrOverrides.types['<mvr>::<module>::<Name>']` whose static value\n\t * resolves per-network as `` `${requireId(dep, \"<mvr>\")}::<module>::<Name>` ``.\n\t * Absent / empty ⇒ no `types` entries (the orchestrator emits `types: {}`).\n\t * Identical on the live and static paths (config-known, resolution-\n\t * independent); the live deployment slice ignores the value (keeps\n\t * `types: {}`). */\n\treadonly mvrTypes?: ReadonlyArray<string> | undefined;\n}\n\n/** Validate + normalize a declared MVR-type suffix. Entries are\n * `'<module>::<Name>'` relative to the package; we reject anything that does\n * not parse as two `::`-joined Move identifiers so a typo fails at config time\n * rather than emitting an `isValidNamedType`-rejected key into the override\n * map. The package MVR prefix is implied (and stripped if the developer\n * redundantly included it). */\nconst normalizeMvrTypeSuffix = (packageName: string, mvr: string, entry: string): string => {\n\t// Tolerate a redundant package prefix the developer may have pasted from a\n\t// fully-qualified tag.\n\tconst suffix = entry.startsWith(`${mvr}::`) ? entry.slice(mvr.length + 2) : entry;\n\tif (!/^[A-Za-z_][\\w]*::[A-Za-z_][\\w]*$/.test(suffix)) {\n\t\tthrow new Error(\n\t\t\t`localPackage('${packageName}') mvrTypes entry '${entry}' must be '<module>::<Name>' ` +\n\t\t\t\t`(two Move identifiers joined by '::'); the '${mvr}' package prefix is implied.`,\n\t\t);\n\t}\n\treturn suffix;\n};\n\n/**\n * Build the package's config-binding set, declared ONCE. The `name` keys the\n * `config.packages.<name>` entry; `mvrPlaceholder` is a literal in both paths;\n * the active-network id is a RESOLVED binding (sugar `requireId(dep, '<mvr>')` when\n * `resolveViaRuntime`, otherwise a literal already-known id). Declared\n * per-network literals (testnet/mainnet) and captured objects are literals.\n *\n * The same set drives `projectStaticConfig` (committed tree) and\n * `projectLiveConfig` (boot deployment) — no parallel projectors.\n */\nconst packageConfigBindings = (input: PackageBindingInput): ConfigBindingSet<PackageLiveState> => {\n\tconst { name, mvrPlaceholder } = input;\n\n\tconst bindings: Array<ConfigBinding<PackageLiveState>> = [];\n\n\t// `packages.<name>.mvr` — pure literal in both paths.\n\tbindings.push({\n\t\tvariant: 'literal',\n\t\tconfigPath: ['packages', name, 'mvr'],\n\t\tvalue: mvrPlaceholder,\n\t});\n\n\t// The active-network id binding. Two cases:\n\t// - no pinned id (LOCAL, or KNOWN-shaped local stub) → RESOLVED (sugar\n\t// `requireId(dep, '<mvr>')` static; live = the concrete resolved id from\n\t// `state.packageId`).\n\t// - pinned literal (KNOWN with a declared id) → LITERAL (the id stands\n\t// identically in both paths).\n\t// Reused for `packages.<name>.packageId` and the `mvrOverrides` entry so\n\t// both agree.\n\tconst pinned = input.pinnedId;\n\tconst idBinding = (configPath: ReadonlyArray<string>): ConfigBinding<PackageLiveState> =>\n\t\tpinned === undefined\n\t\t\t? {\n\t\t\t\t\tvariant: 'resolved',\n\t\t\t\t\tconfigPath,\n\t\t\t\t\tnamespace: 'package',\n\t\t\t\t\tkey: `${name}:packageId`,\n\t\t\t\t\tsugar: { kind: 'id', mvrPlaceholder },\n\t\t\t\t\tlive: (state) => state.packageId,\n\t\t\t\t}\n\t\t\t: { variant: 'literal', configPath, value: pinned };\n\n\tbindings.push(idBinding(['packages', name, 'packageId']));\n\n\t// Active-network objects — captured ids (local). The captured ids are\n\t// LOADED CONFIG DATA, so each is a RESOLVED binding on the generic\n\t// `requireValue(dep, 'package:<name>:objects', '<key>')` channel: the static\n\t// committed stub emits the resolver expr, the live path bakes the real\n\t// captured id AND feeds the deployment `values` channel. The key set comes\n\t// from `objectKeys` (config-known) so BOTH paths emit identical paths —\n\t// no live-only `objects` field. Falls back to the live capture keys when\n\t// `objectKeys` is absent (the live emit path).\n\tconst objectKeys = input.objectKeys ?? Object.keys(input.captured);\n\tif (objectKeys.length > 0) {\n\t\tconst objectsNamespace = `package:${name}:objects`;\n\t\tfor (const objectKey of objectKeys) {\n\t\t\tconst objectBinding = (\n\t\t\t\tconfigPath: ReadonlyArray<string>,\n\t\t\t): ConfigBinding<PackageLiveState> => ({\n\t\t\t\tvariant: 'resolved',\n\t\t\t\tconfigPath,\n\t\t\t\tnamespace: objectsNamespace,\n\t\t\t\tkey: objectKey,\n\t\t\t\tlive: (state) => (state.captured[objectKey] ?? null) as JsonValue,\n\t\t\t});\n\t\t\tbindings.push(objectBinding(['packages', name, 'objects', objectKey]));\n\t\t\t// Top-level `objects.<name>.<key>` mirror.\n\t\t\tbindings.push(objectBinding(['objects', name, objectKey]));\n\t\t}\n\t}\n\n\t// Active-network MVR override entry — `mvrOverrides.packages.<mvr> = <active\n\t// id>` (the @mysten MVR override shape's `packages` map). The sibling `types`\n\t// map is OPT-IN — populated below ONLY from developer-declared `mvrTypes`\n\t// (the change from auto-enumerating every package type). A resolved binding\n\t// always emits (the resolver expr / live id); a pinned literal emits only\n\t// when non-empty.\n\tif (pinned === undefined || pinned.length > 0) {\n\t\tbindings.push(idBinding(['mvrOverrides', 'packages', mvrPlaceholder]));\n\t}\n\n\t// OPT-IN MVR `types` overrides — `mvrOverrides.types['<mvr>::<module>::<Name>']`\n\t// for each developer-declared `<module>::<Name>`. The static path emits the\n\t// per-network type tag `` `${requireId(dep, \"<mvr>\")}::<module>::<Name>` ``\n\t// (the `mvrType` sugar); the live path's value is ignored by the live\n\t// deployment slice (`deploymentFromBucket` keeps `types: {}`). Sugar bindings\n\t// never touch the generic `values` channel. No declared types ⇒ no entries\n\t// here (the orchestrator emits `types: {}`).\n\tfor (const entry of input.mvrTypes ?? []) {\n\t\tconst typeSuffix = normalizeMvrTypeSuffix(name, mvrPlaceholder, entry);\n\t\tconst tag = `${mvrPlaceholder}::${typeSuffix}`;\n\t\tbindings.push({\n\t\t\tvariant: 'resolved',\n\t\t\tconfigPath: ['mvrOverrides', 'types', tag],\n\t\t\tnamespace: 'package',\n\t\t\tkey: `${name}:mvrType:${tag}`,\n\t\t\tsugar: { kind: 'mvrType', mvrPlaceholder, typeSuffix },\n\t\t\t// Ignored by the live deployment slice; a value is required by the\n\t\t\t// binding shape. The resolved per-network tag is what the static\n\t\t\t// committed `config.ts` emits via the sugar.\n\t\t\tlive: () => tag,\n\t\t});\n\t}\n\n\treturn {\n\t\tbucket: 'config.ts',\n\t\tkind: 'package',\n\t\temitterName: 'package',\n\t\t// One Package contribution per published package — shared `'package'`\n\t\t// emitter name is by-design (the orchestrator skips its uniqueness\n\t\t// check for this flag).\n\t\tallowEmitterNameRepetition: true,\n\t\tbindings,\n\t};\n};\n\n/**\n * Build the package's `CodegenableDecl` from its binding set via the unified\n * `configCodegenable` derivation. Mode `'live'` bakes concrete values + feeds\n * the deployment; `'static'` emits resolver expressions. The decl ALSO exports\n * `packageBindings` (the `extraExports` hook) so the orchestrator's\n * `isPackageBindings` seam forwards it to the Move-bindings emitter (bindings\n * stay in `generated/bindings/`).\n */\nconst packageDecl = (\n\tset: ConfigBindingSet<PackageLiveState>,\n\tbindings: PackageBindings,\n\thow: 'static' | { readonly mode: 'live'; readonly state: PackageLiveState },\n): CodegenableDecl<'package'> =>\n\tconfigCodegenable<PackageLiveState, 'package'>(set, how, {\n\t\textraExports: { packageBindings: bindings },\n\t});\n\n/** Build the Codegenable contribution for a local package (LIVE path).\n * Bakes the resolved active-network id + captured objects. */\nexport const makeLocalCodegenable = (\n\tresolved: ResolvedLocalPackage,\n\toptions: {\n\t\treadonly excluded: boolean;\n\t\t/** OPT-IN MVR `types` to expose — `'<module>::<Name>'` suffixes. */\n\t\treadonly mvrTypes?: ReadonlyArray<string> | undefined;\n\t},\n): CodegenableDecl<'package'> => {\n\t// Coerce the resolved placeholder into the current `@local/<slug>` named\n\t// form at the emit seam rather than trusting it verbatim — a cache-served\n\t// `mvrPlaceholder` can be a STALE BARE slug. `mvrNamedFormFrom` is pure +\n\t// deterministic; computed ONCE so the binding default and `config.mvr`\n\t// stay equal. A LOCAL package has NO pinned id — the active-network id is\n\t// always a RESOLVED binding (live bakes the real id; static emits\n\t// `requireId`).\n\tconst mvrPlaceholder = mvrNamedFormFrom(resolved.mvrPlaceholder);\n\tconst set = packageConfigBindings({\n\t\tname: resolved.name,\n\t\tmvrPlaceholder,\n\t\tcaptured: resolved.captured,\n\t\t...(options.mvrTypes !== undefined ? { mvrTypes: options.mvrTypes } : {}),\n\t});\n\tconst bindings: PackageBindings = {\n\t\tname: resolved.name,\n\t\tpackageId: resolved.packageId,\n\t\tmvrPlaceholder,\n\t\tsourcePath: resolved.sourcePath,\n\t\texcluded: options.excluded,\n\t};\n\treturn packageDecl(set, bindings, {\n\t\tmode: 'live',\n\t\tstate: { packageId: resolved.packageId, captured: resolved.captured },\n\t});\n};\n\n/** Build the Codegenable contribution for a known package — `sourcePath:\n * null`, no captured object ids. A KNOWN package's declared id is a PINNED\n * LITERAL (not loaded runtime data), so it renders identically in both the\n * live and static paths. */\nexport const makeKnownCodegenable = (\n\tresolved: ResolvedKnownPackage,\n\toptions: { readonly mvrTypes?: ReadonlyArray<string> | undefined } = {},\n): CodegenableDecl<'package'> => {\n\t// Defensive parity with the local emit seam: coerce to the current\n\t// `@local/<slug>` named form (preserving an already-named override).\n\tconst mvrPlaceholder = mvrNamedFormFrom(resolved.mvrPlaceholder);\n\tconst set = packageConfigBindings({\n\t\tname: resolved.name,\n\t\tmvrPlaceholder,\n\t\tcaptured: {},\n\t\tpinnedId: resolved.packageId,\n\t\t...(options.mvrTypes !== undefined ? { mvrTypes: options.mvrTypes } : {}),\n\t});\n\tconst bindings: PackageBindings = {\n\t\tname: resolved.name,\n\t\tpackageId: resolved.packageId,\n\t\tmvrPlaceholder,\n\t\tsourcePath: null,\n\t\texcluded: true, // implicit — KnownPackages never emit bindings.\n\t};\n\t// A pinned literal renders identically in both paths; derive as `static`\n\t// (the literal bindings ignore live state).\n\treturn packageDecl(set, bindings, 'static');\n};\n\n// ---------------------------------------------------------------------------\n// Stack-free codegen derivation (the `codegen` verb)\n//\n// The live `start` body resolves a package (publish/verify) and feeds the\n// resolved value into `makeLocalCodegenable` / `makeKnownCodegenable`. The\n// `staticCodegen` sources below reconstruct the SAME decls from CONFIG ALONE\n// — no chain, no publish — drawing the `packageId` from the projection\n// id-resolver (sentinel for `'placeholder'`, declared id for `'known'`). The\n// decl SHAPE is identical to the live path.\n// ---------------------------------------------------------------------------\n\n/** Build the static (stack-free) codegen source for a LOCAL package.\n * `sourcePath` is the resolved on-disk Move tree the bindings emitter\n * compiles (a local `sourcePath`; a git source whose tree has not been\n * materialized contributes `null`, so the bindings step skips it). */\nexport const makeLocalStaticCodegen = (config: {\n\treadonly name: string;\n\treadonly sourcePath: string | null;\n\treadonly mvrPlaceholder?: string | undefined;\n\treadonly excluded: boolean;\n\t/** Capture KEYS declared by the user `capture` option (config-known).\n\t * The static stub emits `requireValue(dep, 'package:<name>:objects', '<key>')`\n\t * for each so the committed tree carries object-id references with NO\n\t * baked id and NO live-only `objects` field. */\n\treadonly objectKeys?: ReadonlyArray<string> | undefined;\n\t/** OPT-IN MVR `types` to expose — `'<module>::<Name>'` suffixes. */\n\treadonly mvrTypes?: ReadonlyArray<string> | undefined;\n}): StaticCodegenSource => {\n\tconst mvrPlaceholder = normalizeMvrPlaceholder(config.name, config.mvrPlaceholder);\n\treturn () => {\n\t\t// A LOCAL package has no pinned id — resolve the active id at app\n\t\t// build/dev time, never embed it in the committed tree. A git source\n\t\t// whose tree has NOT been materialized has no local path, so the\n\t\t// bindings emitter can't compile it; the `sourcePath` carries through\n\t\t// (null → bindings step skips it) but the `config.ts` entry is the same.\n\t\tconst set = packageConfigBindings({\n\t\t\tname: config.name,\n\t\t\tmvrPlaceholder,\n\t\t\tcaptured: {},\n\t\t\t...(config.objectKeys !== undefined ? { objectKeys: config.objectKeys } : {}),\n\t\t\t...(config.mvrTypes !== undefined ? { mvrTypes: config.mvrTypes } : {}),\n\t\t\t// No pinned id — the active id resolves at app build/dev time.\n\t\t});\n\t\tconst bindings: PackageBindings = {\n\t\t\tname: config.name,\n\t\t\t// No live publish → the committed stub carries no id (the resolver\n\t\t\t// fills it at app build/dev time). Carry the sentinel-free name.\n\t\t\tpackageId: '',\n\t\t\tmvrPlaceholder,\n\t\t\tsourcePath: config.sourcePath,\n\t\t\texcluded: config.sourcePath === null ? true : config.excluded,\n\t\t};\n\t\treturn [packageDecl(set, bindings, 'static')];\n\t};\n};\n\n/** Build the static (stack-free) codegen source for a KNOWN package. Known\n * packages already carry a literal `packageId` in config; the committed stub\n * emits it verbatim. */\nexport const makeKnownStaticCodegen = (config: {\n\treadonly name: string;\n\treadonly packageId: string;\n\treadonly upgradeCapId?: string | undefined;\n\treadonly mvrPlaceholder?: string | undefined;\n\t/** OPT-IN MVR `types` to expose — `'<module>::<Name>'` suffixes. */\n\treadonly mvrTypes?: ReadonlyArray<string> | undefined;\n}): StaticCodegenSource => {\n\tconst mvrPlaceholder = normalizeMvrPlaceholder(config.name, config.mvrPlaceholder);\n\treturn () => {\n\t\tconst set = packageConfigBindings({\n\t\t\tname: config.name,\n\t\t\tmvrPlaceholder,\n\t\t\tcaptured: {},\n\t\t\tpinnedId: config.packageId,\n\t\t\t...(config.mvrTypes !== undefined ? { mvrTypes: config.mvrTypes } : {}),\n\t\t});\n\t\tconst bindings: PackageBindings = {\n\t\t\tname: config.name,\n\t\t\tpackageId: config.packageId,\n\t\t\tmvrPlaceholder,\n\t\t\tsourcePath: null,\n\t\t\texcluded: true,\n\t\t};\n\t\t// A known package's literal id renders the same in both paths — derive\n\t\t// the decl as `static` (the literal bindings ignore live state).\n\t\treturn [packageDecl(set, bindings, 'static')];\n\t};\n};\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { isValidNamedPackage } from "@mysten/sui/utils";
1
2
  //#region src/plugins/package/dep-resolution.ts
2
3
  /**
3
4
  * Slugify a package name into the bare slug shape (`[a-z0-9-]+`).
@@ -28,9 +29,21 @@ const MVR_LOCAL_ORG = "@local";
28
29
  * `hasMvrName` (see `test/plugins/package/mvr-named-form.test.ts`).
29
30
  */
30
31
  const mvrNamedForm = (name) => `${MVR_LOCAL_ORG}/${mvrSlugify(name)}`;
32
+ /** Normalize the user-facing `mvrPlaceholder` option.
33
+ *
34
+ * Devstack's default remains `@local/<slug>`, derived from the package's
35
+ * symbolic name. An explicit override is different: it is already a full
36
+ * MVR named package (`@org/package`) and must be preserved verbatim so it
37
+ * matches `@mysten/codegen`'s documented `@local-pkg/counter` style.
38
+ */
39
+ const normalizeMvrPlaceholder = (packageName, placeholder) => {
40
+ if (placeholder === void 0) return mvrNamedForm(packageName);
41
+ if (isValidNamedPackage(placeholder)) return placeholder;
42
+ throw new Error(`package '${packageName}' mvrPlaceholder '${placeholder}' is not a valid MVR named package; use a full name like '@local/${mvrSlugify(packageName)}' or '@local-pkg/${mvrSlugify(packageName)}'.`);
43
+ };
31
44
  /**
32
- * Coerce a (possibly STALE) persisted MVR placeholder into the current
33
- * `@local/<slug>` named form.
45
+ * Coerce a (possibly STALE) persisted MVR placeholder into a valid named
46
+ * package form.
34
47
  *
35
48
  * The package-publish cache (`projection.v4.json`'s `mvrPlaceholder`)
36
49
  * persists whatever placeholder shape was current at publish time. A
@@ -42,17 +55,14 @@ const mvrNamedForm = (name) => `${MVR_LOCAL_ORG}/${mvrSlugify(name)}`;
42
55
  *
43
56
  * `mvrNamedForm` is not idempotent over an already-named string
44
57
  * (`mvrSlugify('@local/vault')` → `'local-vault'`), so we cannot blindly
45
- * re-wrap. Instead:
46
- * - if the placeholder is ALREADY in `@<org>/...` named form, keep it
47
- * verbatim (this preserves a user-supplied `mvrPlaceholder` override);
48
- * - otherwise treat it as a bare slug/name and wrap it via
49
- * `mvrNamedForm`.
58
+ * re-wrap. Instead, preserve anything the SDK accepts as a full MVR named
59
+ * package and wrap only legacy bare names.
50
60
  *
51
61
  * Pure + deterministic over its input, so recomputing at the emit seam is
52
62
  * always safe and always current regardless of cached projection state.
53
63
  */
54
- const mvrNamedFormFrom = (placeholder) => placeholder.startsWith(`${MVR_LOCAL_ORG}/`) ? placeholder : mvrNamedForm(placeholder);
64
+ const mvrNamedFormFrom = (placeholder) => isValidNamedPackage(placeholder) ? placeholder : mvrNamedForm(placeholder);
55
65
  //#endregion
56
- export { mvrNamedForm, mvrNamedFormFrom };
66
+ export { mvrNamedFormFrom, normalizeMvrPlaceholder };
57
67
 
58
68
  //# sourceMappingURL=dep-resolution.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"dep-resolution.mjs","names":[],"sources":["../../../src/plugins/package/dep-resolution.ts"],"sourcesContent":["// Move-dep helpers.\n//\n// `mvrSlugify` normalises a package name into the slug shape that\n// codegen emitters write into Move source as MVR placeholders.\n// Distilled doc Invariant 13: the result MUST satisfy `[a-z0-9-]+`\n// (downstream validators reject underscores).\n\n/**\n * Slugify a package name into the bare slug shape (`[a-z0-9-]+`).\n *\n * This bare slug is what codegen writes into Move-source file stems /\n * the dead `package/<slug>.ts` output path, and is the `{app}`\n * component of the MVR named form (`mvrNamedForm` below).\n */\nexport const mvrSlugify = (name: string): string =>\n\tname\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9-]+/g, '-')\n\t\t.replace(/^-+|-+$/g, '')\n\t\t.replace(/-{2,}/g, '-');\n\n/**\n * Local-org used for the MVR named form devstack emits as the binding\n * package default + `config.packages.<name>.mvr`. It is syntactically\n * valid per `@mysten/sui`'s `isValidSuiNSName` (it contains `@`), so\n * the resulting `@local/<slug>` passes BOTH `hasMvrName` and\n * `isValidNamedPackage`. Resolution never hits a real registry — the\n * dapp-kit/`MvrClient` override map keys on this exact string and\n * short-circuits to the active network's resolved package id.\n */\nconst MVR_LOCAL_ORG = '@local';\n\n/**\n * Build the MVR NAMED form (`@local/<slug>`) from a package name.\n *\n * This is the value emitted as BOTH the generated-binding package\n * default (`options.package ?? '@local/<slug>'`) AND\n * `config.packages.<name>.mvr` — the two MUST match so a dapp can key\n * an `MvrClient` override on `config.mvr` and have generated functions\n * resolve by name alone. Verified to satisfy `isValidNamedPackage` and\n * `hasMvrName` (see `test/plugins/package/mvr-named-form.test.ts`).\n */\nexport const mvrNamedForm = (name: string): string => `${MVR_LOCAL_ORG}/${mvrSlugify(name)}`;\n\n/**\n * Coerce a (possibly STALE) persisted MVR placeholder into the current\n * `@local/<slug>` named form.\n *\n * The package-publish cache (`projection.v4.json`'s `mvrPlaceholder`)\n * persists whatever placeholder shape was current at publish time. A\n * stack created BEFORE the `mvrSlugify`→`mvrNamedForm` change carries a\n * BARE slug (e.g. `'vault'`), which an INCREMENTAL re-apply would\n * otherwise emit verbatim — and a bare slug fails `hasMvrName`, so the\n * generated binding's `package:` default never resolves through the\n * `MvrClient` override map. We MUST always emit the named form.\n *\n * `mvrNamedForm` is not idempotent over an already-named string\n * (`mvrSlugify('@local/vault')` → `'local-vault'`), so we cannot blindly\n * re-wrap. Instead:\n * - if the placeholder is ALREADY in `@<org>/...` named form, keep it\n * verbatim (this preserves a user-supplied `mvrPlaceholder` override);\n * - otherwise treat it as a bare slug/name and wrap it via\n * `mvrNamedForm`.\n *\n * Pure + deterministic over its input, so recomputing at the emit seam is\n * always safe and always current regardless of cached projection state.\n */\nexport const mvrNamedFormFrom = (placeholder: string): string =>\n\tplaceholder.startsWith(`${MVR_LOCAL_ORG}/`) ? placeholder : mvrNamedForm(placeholder);\n"],"mappings":";;;;;;;;AAcA,MAAa,cAAc,SAC1B,KACE,YAAY,CAAC,CACb,QAAQ,gBAAgB,GAAG,CAAC,CAC5B,QAAQ,YAAY,EAAE,CAAC,CACvB,QAAQ,UAAU,GAAG;;;;;;;;;;AAWxB,MAAM,gBAAgB;;;;;;;;;;;AAYtB,MAAa,gBAAgB,SAAyB,GAAG,cAAc,GAAG,WAAW,IAAI;;;;;;;;;;;;;;;;;;;;;;;;AAyBzF,MAAa,oBAAoB,gBAChC,YAAY,WAAW,GAAG,cAAc,EAAE,IAAI,cAAc,aAAa,WAAW"}
1
+ {"version":3,"file":"dep-resolution.mjs","names":[],"sources":["../../../src/plugins/package/dep-resolution.ts"],"sourcesContent":["// Move-dep helpers.\n//\n// `mvrSlugify` normalises a package name into the slug shape that\n// devstack uses for its default local MVR placeholders.\n// Distilled doc Invariant 13: the result MUST satisfy `[a-z0-9-]+`\n// (downstream validators reject underscores).\n\nimport { isValidNamedPackage } from '@mysten/sui/utils';\n\n/**\n * Slugify a package name into the bare slug shape (`[a-z0-9-]+`).\n *\n * This bare slug is what codegen writes into Move-source file stems /\n * the dead `package/<slug>.ts` output path, and is the `{app}`\n * component of the MVR named form (`mvrNamedForm` below).\n */\nexport const mvrSlugify = (name: string): string =>\n\tname\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9-]+/g, '-')\n\t\t.replace(/^-+|-+$/g, '')\n\t\t.replace(/-{2,}/g, '-');\n\n/**\n * Local-org used for the MVR named form devstack emits as the binding\n * package default + `config.packages.<name>.mvr`. It is syntactically\n * valid per `@mysten/sui`'s `isValidSuiNSName` (it contains `@`), so\n * the resulting `@local/<slug>` passes BOTH `hasMvrName` and\n * `isValidNamedPackage`. Resolution never hits a real registry — the\n * dapp-kit/`MvrClient` override map keys on this exact string and\n * short-circuits to the active network's resolved package id.\n */\nconst MVR_LOCAL_ORG = '@local';\n\n/**\n * Build the MVR NAMED form (`@local/<slug>`) from a package name.\n *\n * This is the value emitted as BOTH the generated-binding package\n * default (`options.package ?? '@local/<slug>'`) AND\n * `config.packages.<name>.mvr` — the two MUST match so a dapp can key\n * an `MvrClient` override on `config.mvr` and have generated functions\n * resolve by name alone. Verified to satisfy `isValidNamedPackage` and\n * `hasMvrName` (see `test/plugins/package/mvr-named-form.test.ts`).\n */\nexport const mvrNamedForm = (name: string): string => `${MVR_LOCAL_ORG}/${mvrSlugify(name)}`;\n\n/** Normalize the user-facing `mvrPlaceholder` option.\n *\n * Devstack's default remains `@local/<slug>`, derived from the package's\n * symbolic name. An explicit override is different: it is already a full\n * MVR named package (`@org/package`) and must be preserved verbatim so it\n * matches `@mysten/codegen`'s documented `@local-pkg/counter` style.\n */\nexport const normalizeMvrPlaceholder = (\n\tpackageName: string,\n\tplaceholder: string | undefined,\n): string => {\n\tif (placeholder === undefined) return mvrNamedForm(packageName);\n\tif (isValidNamedPackage(placeholder)) return placeholder;\n\tthrow new Error(\n\t\t`package '${packageName}' mvrPlaceholder '${placeholder}' is not a valid MVR named package; ` +\n\t\t\t`use a full name like '@local/${mvrSlugify(packageName)}' or '@local-pkg/${mvrSlugify(packageName)}'.`,\n\t);\n};\n\n/**\n * Coerce a (possibly STALE) persisted MVR placeholder into a valid named\n * package form.\n *\n * The package-publish cache (`projection.v4.json`'s `mvrPlaceholder`)\n * persists whatever placeholder shape was current at publish time. A\n * stack created BEFORE the `mvrSlugify`→`mvrNamedForm` change carries a\n * BARE slug (e.g. `'vault'`), which an INCREMENTAL re-apply would\n * otherwise emit verbatim — and a bare slug fails `hasMvrName`, so the\n * generated binding's `package:` default never resolves through the\n * `MvrClient` override map. We MUST always emit the named form.\n *\n * `mvrNamedForm` is not idempotent over an already-named string\n * (`mvrSlugify('@local/vault')` → `'local-vault'`), so we cannot blindly\n * re-wrap. Instead, preserve anything the SDK accepts as a full MVR named\n * package and wrap only legacy bare names.\n *\n * Pure + deterministic over its input, so recomputing at the emit seam is\n * always safe and always current regardless of cached projection state.\n */\nexport const mvrNamedFormFrom = (placeholder: string): string =>\n\tisValidNamedPackage(placeholder) ? placeholder : mvrNamedForm(placeholder);\n"],"mappings":";;;;;;;;;AAgBA,MAAa,cAAc,SAC1B,KACE,YAAY,CAAC,CACb,QAAQ,gBAAgB,GAAG,CAAC,CAC5B,QAAQ,YAAY,EAAE,CAAC,CACvB,QAAQ,UAAU,GAAG;;;;;;;;;;AAWxB,MAAM,gBAAgB;;;;;;;;;;;AAYtB,MAAa,gBAAgB,SAAyB,GAAG,cAAc,GAAG,WAAW,IAAI;;;;;;;;AASzF,MAAa,2BACZ,aACA,gBACY;CACZ,IAAI,gBAAgB,KAAA,GAAW,OAAO,aAAa,WAAW;CAC9D,IAAI,oBAAoB,WAAW,GAAG,OAAO;CAC7C,MAAM,IAAI,MACT,YAAY,YAAY,oBAAoB,YAAY,mEACvB,WAAW,WAAW,EAAE,mBAAmB,WAAW,WAAW,EAAE,GACrG;AACD;;;;;;;;;;;;;;;;;;;;;AAsBA,MAAa,oBAAoB,gBAChC,oBAAoB,WAAW,IAAI,cAAc,aAAa,WAAW"}
@@ -54,10 +54,10 @@ interface LocalPackageOptions<Publisher extends PublisherAccountMember = Publish
54
54
  readonly excludeFromCodegen?: boolean;
55
55
  /** Move datatypes (struct / enum) to expose as MVR `types` overrides.
56
56
  * OPT-IN: each entry is a `'<module>::<Name>'` relative to THIS package
57
- * (the `@local/<slug>` prefix is implied), e.g. `['game::Lobby',
57
+ * (the package MVR prefix is implied), e.g. `['game::Lobby',
58
58
  * 'game::Game']`. Each declared type emits one
59
- * `mvrOverrides.types['@local/<slug>::<module>::<Name>']` entry whose value
60
- * resolves per-network to `` `${requireId(dep, "@local/<slug>")}::<module>::<Name>` ``
59
+ * `mvrOverrides.types['<mvr>::<module>::<Name>']` entry whose value
60
+ * resolves per-network to `` `${requireId(dep, "<mvr>")}::<module>::<Name>` ``
61
61
  * (the resolved package id, never baked). Absent / empty ⇒ `types: {}` — we
62
62
  * do NOT auto-enumerate every package type (that balloons the config + every
63
63
  * committed `deployments/<net>.ts`). */
@@ -13,6 +13,7 @@ import { suiResource } from "../sui/index.mjs";
13
13
  import { pickCreatedByType } from "./publish-output.mjs";
14
14
  import { discoverCoinsFromPublish } from "../coin/discovery.mjs";
15
15
  import "../coin/index.mjs";
16
+ import { normalizeMvrPlaceholder } from "./dep-resolution.mjs";
16
17
  import { makeKnownCodegenable, makeKnownStaticCodegen, makeLocalCodegenable, makeLocalStaticCodegen } from "./codegen.mjs";
17
18
  import { hashMoveSources } from "./build.mjs";
18
19
  import { materializeGitSource } from "./git-source.mjs";
@@ -63,6 +64,7 @@ const buildLocalPlugin = (name, opts) => {
63
64
  const capture = normalizeCapture(name, opts.capture);
64
65
  if (opts.sourcePath === void 0 && opts.git === void 0) throw new Error(`localPackage('${name}'): provide either 'sourcePath' or 'git'.`);
65
66
  if (opts.sourcePath !== void 0 && opts.git !== void 0) throw new Error(`localPackage('${name}'): 'sourcePath' and 'git' are mutually exclusive.`);
67
+ const mvrPlaceholder = normalizeMvrPlaceholder(name, opts.mvrPlaceholder);
66
68
  const resolveSourcePath = () => opts.git !== void 0 ? materializeGitSource(opts.git, name) : Effect.succeed(opts.sourcePath);
67
69
  return definePlugin({
68
70
  id: packageRef.id,
@@ -96,7 +98,7 @@ const buildLocalPlugin = (name, opts) => {
96
98
  staticCodegen: makeLocalStaticCodegen({
97
99
  name,
98
100
  sourcePath: opts.sourcePath ?? null,
99
- mvrPlaceholder: opts.mvrPlaceholder,
101
+ mvrPlaceholder,
100
102
  excluded: opts.excludeFromCodegen ?? false,
101
103
  ...opts.mvrTypes !== void 0 ? { mvrTypes: opts.mvrTypes } : {},
102
104
  ...opts.capture !== void 0 ? { objectKeys: Object.keys(opts.capture) } : {}
@@ -122,7 +124,7 @@ const buildLocalPlugin = (name, opts) => {
122
124
  sourcePath,
123
125
  chainId: sui.chainId,
124
126
  publisherAddress: publisherAccount.address,
125
- mvrOverride: opts.mvrPlaceholder,
127
+ mvrOverride: mvrPlaceholder,
126
128
  ...capture !== void 0 ? { capture } : {},
127
129
  executor
128
130
  });
@@ -139,8 +141,10 @@ const buildLocalPlugin = (name, opts) => {
139
141
  });
140
142
  };
141
143
  const buildKnownPlugin = (name, opts) => {
144
+ const packageRef = resource(packageResourceId(name));
145
+ const mvrPlaceholder = normalizeMvrPlaceholder(name, opts.mvrPlaceholder);
142
146
  return definePlugin({
143
- id: resource(packageResourceId(name)).id,
147
+ id: packageRef.id,
144
148
  dependsOn: { sui: suiResource },
145
149
  role: "task",
146
150
  section: "package",
@@ -155,7 +159,7 @@ const buildKnownPlugin = (name, opts) => {
155
159
  staticCodegen: makeKnownStaticCodegen({
156
160
  name,
157
161
  packageId: opts.packageId,
158
- mvrPlaceholder: opts.mvrPlaceholder,
162
+ mvrPlaceholder,
159
163
  ...opts.upgradeCapId !== void 0 ? { upgradeCapId: opts.upgradeCapId } : {},
160
164
  ...opts.mvrTypes !== void 0 ? { mvrTypes: opts.mvrTypes } : {}
161
165
  }),
@@ -166,7 +170,7 @@ const buildKnownPlugin = (name, opts) => {
166
170
  packageName: name,
167
171
  packageId: opts.packageId,
168
172
  upgradeCapId: opts.upgradeCapId,
169
- mvrOverride: opts.mvrPlaceholder
173
+ mvrOverride: mvrPlaceholder
170
174
  });
171
175
  emitContributions(ctx, makeKnownCapabilities(name, opts, resolved));
172
176
  return resolved;