@geekmidas/cli 10.0.0-alpha.4 → 10.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{config-C21ocYNd.cjs → config-CNz5KaSR.cjs} +16 -45
- package/dist/config-CNz5KaSR.cjs.map +1 -0
- package/dist/{config-WRdsQYZK.mjs → config-CxP7P-qG.mjs} +17 -46
- package/dist/config-CxP7P-qG.mjs.map +1 -0
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/{credentials-BBIRJVp4.mjs → credentials-BK3Z93MX.mjs} +5 -5
- package/dist/credentials-BK3Z93MX.mjs.map +1 -0
- package/dist/{credentials-G1JwBoXH.cjs → credentials-DzsXZzkx.cjs} +5 -5
- package/dist/credentials-DzsXZzkx.cjs.map +1 -0
- package/dist/discover-CAlFgYec.mjs.map +1 -1
- package/dist/discover-DTmFeloL.cjs.map +1 -1
- package/dist/{index-BThUzEIp.d.mts → index-BGGdjueF.d.cts} +2 -2
- package/dist/index-BGGdjueF.d.cts.map +1 -0
- package/dist/{index-DCtipn2h.d.cts → index-DEbaleny.d.mts} +2 -2
- package/dist/index-DEbaleny.d.mts.map +1 -0
- package/dist/index.cjs +332 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +332 -179
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-kkQbsRVj.mjs → openapi-Bw8DWFWz.mjs} +104 -50
- package/dist/openapi-Bw8DWFWz.mjs.map +1 -0
- package/dist/{openapi-CA0yJUTN.cjs → openapi-D-dx9Akj.cjs} +122 -50
- package/dist/openapi-D-dx9Akj.cjs.map +1 -0
- package/dist/openapi.cjs +5 -3
- package/dist/openapi.d.cts +46 -10
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +46 -10
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +4 -4
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/setup-8IWPsRIN.cjs +11 -0
- package/dist/{setup-DXvRLTZN.mjs → setup-BWMhbW-p.mjs} +3 -3
- package/dist/{setup-DXvRLTZN.mjs.map → setup-BWMhbW-p.mjs.map} +1 -1
- package/dist/{setup-Cm2UnAdh.cjs → setup-D7L4BgBT.cjs} +3 -3
- package/dist/{setup-Cm2UnAdh.cjs.map → setup-D7L4BgBT.cjs.map} +1 -1
- package/dist/{setup-ZijS4qeC.mjs → setup-DGIPd2fh.mjs} +4 -4
- package/dist/{types-BhIl-COG.d.cts → types-BLSNdrk0.d.cts} +2 -10
- package/dist/{types-C-arnQ1r.d.mts.map → types-BLSNdrk0.d.cts.map} +1 -1
- package/dist/{types-C-arnQ1r.d.mts → types-C3POXhw3.d.mts} +2 -10
- package/dist/{types-BhIl-COG.d.cts.map → types-C3POXhw3.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-DdJss8I6.cjs → workspace-CGaUAw5A.cjs} +3 -3
- package/dist/workspace-CnTqIlB3.mjs +8 -0
- package/dist/{workspace-BFMrAKqI.mjs → workspace-DIuZx-cc.mjs} +1 -17
- package/dist/{workspace-BFMrAKqI.mjs.map → workspace-DIuZx-cc.mjs.map} +1 -1
- package/dist/{workspace-BcucjwvA.cjs → workspace-LKiNyaDy.cjs} +1 -17
- package/dist/{workspace-BcucjwvA.cjs.map → workspace-LKiNyaDy.cjs.map} +1 -1
- package/package.json +11 -11
- package/dist/config-C21ocYNd.cjs.map +0 -1
- package/dist/config-WRdsQYZK.mjs.map +0 -1
- package/dist/credentials-BBIRJVp4.mjs.map +0 -1
- package/dist/credentials-G1JwBoXH.cjs.map +0 -1
- package/dist/index-BThUzEIp.d.mts.map +0 -1
- package/dist/index-DCtipn2h.d.cts.map +0 -1
- package/dist/openapi-CA0yJUTN.cjs.map +0 -1
- package/dist/openapi-kkQbsRVj.mjs.map +0 -1
- package/dist/setup-B5_oHHUj.cjs +0 -11
- package/dist/workspace-k4l3rsFh.mjs +0 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials-BK3Z93MX.mjs","names":["CACHE_BACKENDS: readonly CacheBackend[]","EMAIL_BACKENDS: readonly EmailBackend[]","STORAGE_BACKENDS: readonly StorageBackend[]","value: unknown","on: MainProvider","workspace: {\n\tdeploy?: { default?: string } | undefined;\n}","backends: readonly T[]","EDGE_KINDS: Readonly<Record<string, true>>","plan: Plan","project: string","addresses: Readonly<Record<string, string>>","sites: CaddySite[]","address: string","resource: PlannedResource","sites: readonly CaddySite[]","LOCAL_USER","port: number","version: PostgresVersion","DEFAULT_IMAGES: Readonly<Record<string, string>>","PORTS: Readonly<Record<string, readonly ContainerPort[]>>","container: string","containers: readonly string[]","volumes: Readonly<Record<string, string>>","LOCAL_USER","LOCAL_TOKEN","plan: Plan","options: ComposeOptions","services: Record<string, ComposeService>","volumes: Record<string, Record<string, never>>","file: ComposeFile","container: string","image: string","published: string[]","ports: PortAssignments","key: string","dockerCli: Docker","CONTAINERS: Partial<Record<DeclarationKind, string>>","CACHE_CONTAINERS: Partial<Record<CacheBackend, string>>","CONTAINERLESS: Partial<Record<DeclarationKind, true>>","REQUIRES: Readonly<Record<string, readonly string[]>>","ROLES: Partial<Record<DeclarationKind, string>>","EVENT_KINDS: Partial<Record<DeclarationKind, true>>","PROVISIONS: Partial<Record<DeclarationKind, true>>","EVENT_CONTAINERS: Partial<Record<EventsBackend, string>>","DEFAULT_EVENTS: EventsBackend","SEPARATORS: Partial<Record<DeclarationKind, string>>","id: string","kind: DeclarationKind","stage: string","events: EventsBackend","cache: CacheBackend","manifest: ConstructManifest","order: readonly string[]","options: PlanOptions","resources: PlannedResource[]","declaration: Declaration | undefined","ids: readonly string[]","databases: readonly string[]","STORAGE_CREDENTIALS: Readonly<Record<string, string>>","plan: Plan","options: EnvOptions","env: Record<string, string>","resolved: Record<string, string>","resource: PlannedResource","url: string | undefined","resolved: Readonly<Record<string, string>>","address: string","kind: PlannedResource['kind']","ports: PortAssignments","project: string","addresses: Readonly<Record<string, string>>","port: number","database: string","credential: { user: string; password: string }","role: string","url: string","table: string","current: PlannedResource | undefined","backend: string","project: string","containers: readonly string[]","existing: PortAssignments","probe: PortProbe","assignments: Record<string, number>","from: number","taken: ReadonlySet<number>","port: number","resolve","plan: Plan","statements: Statement[]","cache: PlannedResource","home: PlannedResource","resource: PlannedResource","schema: string","project: string","name: string","client: SqlClient","statements: readonly Statement[]","applied: Applied[]","bucket: string","open: readonly string[]","client: BucketClient","policies: readonly { bucket: string; open: string[] }[]","current: string | undefined","wanted: string","buckets: readonly string[]","plan: Plan","compose: ComposeFile","generated: Readonly<Record<string, string>>","root: string","stage: string","state: ReconcileState","stage: string","options: ReconcileOptions","result: ReconcileResult","plan: Plan","ports: PortAssignments","sql: (port: number) => SqlClient","buckets: (port: number) => BucketClient","project: string","applied: Applied[]","docker: Docker","composePath: string","containers: readonly string[]","observed: Record<string, number>","addresses: Record<string, string>","path: string","content: string","workspace: NormalizedWorkspace","stage: string","manifest?: ConstructManifest","options: WorkspaceReconcileOptions","manifest: ConstructManifest","addresses: Record<string, string>","id: string","port: number","envConfig: string | string[] | undefined","cwd: string","loaded: string[]","missing: string[]","port: number","resolve","err: NodeJS.ErrnoException","preferredPort: number","composePath: string","results: ComposePortMapping[]","workspaceRoot: string","ports: PortState","service: string","containerPort: number","dockerEnv: Record<string, string>","url: string","oldPort: number","newPort: number","secrets: Record<string, string>","resolvedPorts: ResolvedServicePorts","portReplacements: { defaultPort: number; resolvedPort: number }[]","secretsEnv?: Record<string, string>","portEnv?: Record<string, string>","workspace: { root: string; apps: Record<string, unknown> }","secretsRoot: string","appName?: string","stages: string[]","startDir: string","secretsJsonPath: string","preloadPath: string","wrapperPath: string","entryPath: string","secretsJsonPath?: string","options: {\n\texplicitPort?: number;\n\tcwd?: string;\n\tresolveDockerPorts?: 'full' | 'readonly';\n\t/** Secret stages to try, in order. Default: ['dev', 'development'] */\n\tstages?: string[];\n\t/** Start Docker Compose services after port resolution. Default: false */\n\tstartDocker?: boolean;\n\t/** Custom secrets JSON filename. Default: 'dev-secrets-{appName}.json' or 'dev-secrets.json' */\n\tsecretsFileName?: string;\n\t/**\n\t * The stage to reconcile the local target for.\n\t *\n\t * `gkm test` passes `test` and gets the same containers with suffixed\n\t * resources; `gkm dev` passes `development` and gets unsuffixed ones. Only\n\t * read by workspaces that have adopted the constructs glob.\n\t */\n\treconcileStage?: string;\n}","workspaceAppPort: number | undefined","appName: string | undefined","appInfo: WorkspaceAppInfo | undefined","declaredKeys: string[]"],"sources":["../src/workspace/backends.ts","../src/reconcile/caddyfile.ts","../src/reconcile/clients.ts","../src/reconcile/containers.ts","../src/reconcile/compose.ts","../src/reconcile/docker.ts","../src/reconcile/plan.ts","../src/reconcile/env.ts","../src/reconcile/ports.ts","../src/reconcile/provision.ts","../src/reconcile/state.ts","../src/reconcile/index.ts","../src/reconcile/workspace.ts","../src/credentials/index.ts"],"sourcesContent":["/**\n * Reading a backend, or an image pin, out of one `services:` value.\n *\n * `services.cache` and `services.mail` each answer two questions that used to\n * be one: *what runs locally* and *what backs this when deployed*. A boolean or\n * an object is the first; a string is the second. Keeping them in one key rather\n * than adding `cacheBackend` beside `cache` follows `services.events`, which has\n * always been a bare string — and means a project that says `cache: 'db'` has\n * said everything, because a cache in the database needs no container to pin.\n *\n * These readers exist so nothing downstream has to know a value can be either.\n */\n\nimport {\n\ttype CacheBackend,\n\tDEFAULT_CACHE,\n\tDEFAULT_EMAIL,\n\tDEFAULT_STORAGE,\n\ttype EmailBackend,\n\ttype MainProvider,\n\ttype StorageBackend,\n} from '../types.js';\n\nconst CACHE_BACKENDS: readonly CacheBackend[] = [\n\t'upstash',\n\t'elasticache',\n\t'db',\n];\nconst EMAIL_BACKENDS: readonly EmailBackend[] = ['resend', 'ses', 'smtp'];\nconst STORAGE_BACKENDS: readonly StorageBackend[] = ['minio', 's3', 'r2'];\n\n/**\n * The image pin in a `services:` value, if it carries one.\n *\n * A backend name is not one, so it reads as \"left at the default\" — which it is:\n * naming a backend says nothing about which image the local container should\n * run, and the default is what anyone naming a backend expects.\n */\nexport function imagePinOf<T>(value: T): Exclude<T, string> | undefined {\n\treturn typeof value === 'string' ? undefined : (value as Exclude<T, string>);\n}\n\n/**\n * The cache backend a workspace selected, or the one its target implies.\n *\n * Defaulted here rather than by every caller, so \"what happens when nobody\n * said\" is answered once and the answer is greppable — and now answered by\n * *where it deploys*, because there is no single right answer for both a\n * Lambda and a box running its own Postgres.\n *\n * `on` is the deploy target, not where this code happens to be running. A\n * local `gkm dev` for an AWS app still defaults to Upstash, because a backend\n * that differed between local and deployed would be worse than a slower one —\n * which is the property the type's own documentation asks for.\n */\nexport function cacheBackendOf(\n\tvalue: unknown,\n\ton: MainProvider = 'aws',\n): CacheBackend {\n\treturn isBackend(value, CACHE_BACKENDS) ? value : DEFAULT_CACHE[on];\n}\n\n/** The email backend a workspace selected, defaulting to SES on every target. */\nexport function emailBackendOf(value: unknown): EmailBackend {\n\treturn isBackend(value, EMAIL_BACKENDS) ? value : DEFAULT_EMAIL;\n}\n\n/** The storage backend a workspace selected, or the one its target implies. */\nexport function storageBackendOf(\n\tvalue: unknown,\n\ton: MainProvider = 'aws',\n): StorageBackend {\n\treturn isBackend(value, STORAGE_BACKENDS) ? value : DEFAULT_STORAGE[on];\n}\n\n/**\n * Which family of defaults a workspace's deploy target belongs to.\n *\n * One question, asked once: **does this target run containers the project\n * controls?** Dokploy and a bare server do, so a bucket or a cache can live\n * beside the app; AWS, Vercel and Cloudflare do not, so the default has to be\n * something managed. That is the whole of the distinction, and it is why\n * `MainProvider` is the right axis rather than the deploy target's own name —\n * three targets share one answer.\n *\n * An AWS deploy names no `deploy.default` at all, because it goes through SST,\n * which is why the absence is what selects `aws`.\n */\nexport function providerOf(workspace: {\n\tdeploy?: { default?: string } | undefined;\n}): MainProvider {\n\tconst target = workspace.deploy?.default;\n\n\treturn target === 'dokploy' || target === 'server' ? 'server' : 'aws';\n}\n\n/**\n * Whether a string names a backend, as opposed to being one of the other things\n * a `services:` value can be.\n *\n * A misspelling falls through to the default rather than throwing, which is the\n * wrong trade for something with a cost attached — so callers that can report it\n * should use {@link unknownBackend} to say so.\n */\nfunction isBackend<T extends string>(\n\tvalue: unknown,\n\tbackends: readonly T[],\n): value is T {\n\treturn typeof value === 'string' && backends.includes(value as T);\n}\n\n/**\n * The backend name in a value that names one wrongly.\n *\n * Returns nothing when the value is not a string at all — an image pin is not a\n * misspelled backend — so a caller can report `cache: 'upstsh'` without\n * complaining about `cache: true`.\n */\nexport function unknownBackend(\n\tvalue: unknown,\n\tkind: BackendKind,\n): string | undefined {\n\tif (typeof value !== 'string') return undefined;\n\n\treturn backendsFor(kind).includes(value) ? undefined : value;\n}\n\n/** A `services:` key that selects a backend. */\nexport type BackendKind = 'cache' | 'mail' | 'storage';\n\n/** What a `services:` key accepts, for an error that can list it. */\nexport function backendsFor(kind: BackendKind): readonly string[] {\n\tif (kind === 'cache') return CACHE_BACKENDS;\n\tif (kind === 'storage') return STORAGE_BACKENDS;\n\n\treturn EMAIL_BACKENDS;\n}\n","/**\n * The Caddy configuration a plan implies.\n *\n * A file server is a *domain that serves a bucket*, and locally it had neither:\n * `UPLOADS_SERVER_URL` resolved to MinIO's path-style address, which works and\n * is not the deployed shape. MinIO's own virtual-host mode cannot fix that —\n * it reads the leading label *as the bucket name*, so it produces the right\n * shape only when the server's id and the bucket's happen to agree, and never\n * for a server fronting two buckets.\n *\n * So the mapping lives in front of MinIO instead: one host per file server,\n * rewriting the bucket in as a path prefix. That is the whole of it — no\n * caching, because caching is not what is missing locally, and a caching proxy\n * would add an invalidation story a local stack has no use for.\n *\n * What it buys beyond the shape is a certificate. Caddy issues one per host\n * from its own CA, so a local address is `https://` like the deployed one, and\n * everything downstream of that — `Secure` cookies, `SameSite`, a cookie domain\n * with more than one host under it — starts behaving the way it will in\n * production rather than the way `http://localhost` allows.\n *\n * Pure, like `composeFor`: what should exist is data, and rendering it is one\n * line at the edge.\n */\n\nimport type { Plan, PlannedResource } from './plan';\n\n/** One host on the edge, and what answers behind it. */\nexport interface CaddySite {\n\t/** e.g. `api.kitchen-sink.localhost` */\n\thost: string;\n\t/** Where the request is sent, e.g. `http://minio:9000`. */\n\tupstream: string;\n\t/**\n\t * A path to rewrite to, where the upstream expects a different one.\n\t *\n\t * The bucket prefix on an object store is the case. A surface and a site\n\t * want none: they are already serving the paths the client asked for.\n\t */\n\trewrite?: string;\n}\n\n/**\n * The kinds the edge serves.\n *\n * Everything that owns a *public address*. Deployed, each of these has a real\n * hostname and a certificate, so this is what makes the local shape the same\n * one rather than a near-enough one — and the application is indifferent\n * either way, because it reads whichever address was injected and never\n * composes one.\n *\n * Adding a kind here is the whole of adding it to the edge.\n */\nexport const EDGE_KINDS: Readonly<Record<string, true>> = {\n\t'file-server': true,\n\t'rest-api': true,\n\tsite: true,\n};\n\n/**\n * How the edge reaches the host, from inside its container.\n *\n * A surface and a site are served by processes `gkm dev` starts on the host,\n * not by containers — so the edge has to leave Docker's network to reach them.\n * Docker Desktop resolves this name already; on Linux the compose definition\n * maps it to the host gateway.\n */\nconst HOST_GATEWAY = 'host.docker.internal';\n\n/**\n * The suffix every local host shares.\n *\n * `.localhost` rather than a made-up TLD because it resolves to loopback\n * without touching `/etc/hosts` — macOS and systemd-resolved both answer for\n * subdomains of it, and Caddy issues internal certificates for it by default.\n */\nexport const LOCAL_TLD = 'localhost';\n\n/**\n * The sites a plan implies, one per file server.\n *\n * The leading label is the *server's* stage-scoped name rather than the\n * bucket's, which is the difference that matters: two servers over one bucket\n * are a legitimate arrangement — two cache behaviours, one origin — and naming\n * the host after the bucket would make them collide.\n */\nexport function sitesFor(\n\tplan: Plan,\n\tproject: string,\n\t/**\n\t * Where surfaces and sites answer on the host, keyed by construct id.\n\t *\n\t * Assigned by whatever starts them rather than published by a container,\n\t * which is why they arrive here instead of being read off a port.\n\t */\n\taddresses: Readonly<Record<string, string>> = {},\n): CaddySite[] {\n\tconst byId = new Map(plan.resources.map((r) => [r.id, r]));\n\tconst sites: CaddySite[] = [];\n\n\tfor (const resource of plan.resources) {\n\t\tif (!EDGE_KINDS[resource.kind]) continue;\n\n\t\tconst host = hostFor(resource, project);\n\n\t\tif (resource.kind === 'file-server') {\n\t\t\t// The bucket is a prefix on the origin, never part of the address: the\n\t\t\t// client asks for the key it stored.\n\t\t\tconst origin = resource.of ? byId.get(resource.of) : undefined;\n\t\t\tif (!origin) continue;\n\n\t\t\tsites.push({\n\t\t\t\thost,\n\t\t\t\tupstream: 'http://minio:9000',\n\t\t\t\trewrite: `/${origin.name}{uri}`,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\t// A surface or a site: a process on the host, at an address something\n\t\t// else assigned. Without one there is nothing to route to — which is the\n\t\t// ordinary state before `gkm dev` has decided where things listen.\n\t\tconst address = addresses[resource.id];\n\t\tif (!address) continue;\n\n\t\tconst port = portOf(address);\n\t\tif (!port) continue;\n\n\t\tsites.push({ host, upstream: `http://${HOST_GATEWAY}:${port}` });\n\t}\n\n\treturn sites;\n}\n\n/** The port a local address answers on, defaulting by scheme. */\nfunction portOf(address: string): string | undefined {\n\ttry {\n\t\tconst { port, protocol } = new URL(address);\n\n\t\treturn port || (protocol === 'https:' ? '443' : '80');\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Where one file server answers, including the stage its name carries.\n *\n * The project is a label rather than a decoration: it is what keeps two\n * checkouts from answering on one address. Omitted when there is no project to\n * name, because an empty label makes `uploads..localhost` — a host nothing\n * resolves and no certificate can be issued for.\n */\nexport function hostFor(resource: PlannedResource, project: string): string {\n\treturn [resource.name, project, LOCAL_TLD].filter(Boolean).join('.');\n}\n\n/**\n * The static root config, which only ever imports.\n *\n * Written once and identical for every stage, because the *sites* are what\n * differ and they live in their own files. That split is not tidiness: one\n * container serves every stage — the same way one Postgres holds `orders` and\n * `orders_test` — so a single Caddyfile would mean `gkm test` deleting the\n * routes `gkm dev` is serving, and each command silently breaking the other.\n */\nexport function caddyfileRoot(): string {\n\treturn `${GENERATED}\n{\n\t# No ACME, no public DNS: the CA is local and so are the names.\n\tlocal_certs\n\tauto_https disable_redirects\n}\n\n# One file per stage, so two stages share this edge the way they already share\n# one Postgres and one MinIO.\nimport /etc/caddy/sites/*.caddy\n`;\n}\n\n/**\n * One stage's sites, as the file imported above.\n *\n * `rewrite` rather than a path prefix on the address: the bucket is the\n * origin's business, and the client asks for the key it stored.\n *\n * `header_up Host {upstream_hostport}` matters more than it looks. MinIO routes\n * and signs on the Host header, so forwarding the requested hostname would make\n * it look for a bucket named after the domain.\n */\nexport function toCaddyfile(sites: readonly CaddySite[]): string {\n\tif (sites.length === 0) {\n\t\treturn `${GENERATED}\n# No file server is declared in this stage, so it routes nothing.\n`;\n\t}\n\n\tconst blocks = sites.map(\n\t\t({ host, upstream, rewrite }) => `\nhttps://${host} {\n\ttls internal\n\n\treverse_proxy ${upstream} {${\n\t\trewrite\n\t\t\t? `\n\t\trewrite ${rewrite}\n\t\t# MinIO routes and signs on the Host header, so forwarding the requested\n\t\t# hostname would make it look for a bucket named after the domain.\n\t\theader_up Host {upstream_hostport}`\n\t\t\t: `\n\t\t# The application reads whatever address was injected, so it has to see\n\t\t# the one a caller used — that is what makes a redirect, a cookie domain\n\t\t# and a generated link point back here rather than at the upstream.\n\t\theader_up Host {host}\n\t\theader_up X-Forwarded-Proto {scheme}`\n\t}\n\t}\n}`,\n\t);\n\n\treturn `${GENERATED}${blocks.join('\\n')}\\n`;\n}\n\nconst GENERATED = `# Generated by gkm from the construct manifest — do not edit.\n#\n# One host per declared file server, in front of the object store that holds its\n# objects. This is the shape a file server has deployed — a domain serving a\n# bucket — which MinIO alone cannot produce: its virtual-host mode reads the\n# leading label as the bucket name.\n#\n# Certificates come from Caddy's own CA. \"gkm dev\" and \"gkm test\" export its\n# root and point Node at it, so nothing has to be installed to use these URLs\n# from code; a browser needs the root trusted once.\n`;\n","/**\n * The real Postgres and MinIO clients the applier runs against.\n *\n * Kept apart from `provision.ts` so the rules there — what gets created, in what\n * order, and never dropping anything — are asserted against fakes, while the\n * drivers live here where there is nothing to assert but wiring.\n *\n * Both are constructed from the same local addresses the env writer composes,\n * so the applier cannot end up talking to a different container than the app.\n */\n\nimport {\n\tCreateBucketCommand,\n\tGetBucketPolicyCommand,\n\tHeadBucketCommand,\n\tPutBucketPolicyCommand,\n\tS3Client,\n} from '@aws-sdk/client-s3';\nimport { Client } from 'pg';\nimport type { BucketClient, SqlClient } from './provision';\n\n/** The credential local containers are brought up with. */\nconst LOCAL_USER = 'geekmidas';\n\n/**\n * A Postgres client that opens one connection per database it is asked about.\n *\n * `CREATE DATABASE` cannot run from a connection to the database being created,\n * and a schema must be created from inside its own database — so the database is\n * part of each query rather than fixed when the client is built.\n */\nexport function pgClient(port: number): SqlClient {\n\treturn {\n\t\tasync query(database, sql, values) {\n\t\t\tconst client = new Client({\n\t\t\t\thost: 'localhost',\n\t\t\t\tport,\n\t\t\t\tuser: LOCAL_USER,\n\t\t\t\tpassword: LOCAL_USER,\n\t\t\t\t// The cluster's own database, which the image creates. Connecting to\n\t\t\t\t// it is what makes `CREATE DATABASE` possible at all.\n\t\t\t\tdatabase: database ?? LOCAL_USER,\n\t\t\t});\n\n\t\t\tawait client.connect();\n\t\t\ttry {\n\t\t\t\tconst result = await client.query(sql, values as never[]);\n\t\t\t\treturn result.rows;\n\t\t\t} finally {\n\t\t\t\tawait client.end();\n\t\t\t}\n\t\t},\n\t};\n}\n\n/** A bucket client pointed at the local MinIO. */\nexport function bucketClient(port: number): BucketClient {\n\tconst s3 = new S3Client({\n\t\tregion: 'us-east-1',\n\t\tendpoint: `http://localhost:${port}`,\n\t\t// MinIO serves buckets as paths, not as subdomains of the endpoint.\n\t\tforcePathStyle: true,\n\t\tcredentials: { accessKeyId: LOCAL_USER, secretAccessKey: LOCAL_USER },\n\t});\n\n\treturn {\n\t\tasync exists(bucket) {\n\t\t\ttry {\n\t\t\t\tawait s3.send(new HeadBucketCommand({ Bucket: bucket }));\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\t// Missing, or not reachable. Creating is idempotent enough that\n\t\t\t\t// treating both as \"missing\" is safe.\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\n\t\tasync create(bucket) {\n\t\t\tawait s3.send(new CreateBucketCommand({ Bucket: bucket }));\n\t\t},\n\n\t\tasync policy(bucket) {\n\t\t\ttry {\n\t\t\t\tconst result = await s3.send(\n\t\t\t\t\tnew GetBucketPolicyCommand({ Bucket: bucket }),\n\t\t\t\t);\n\t\t\t\treturn result.Policy;\n\t\t\t} catch {\n\t\t\t\t// No policy, or the bucket is not reachable. Both mean there is\n\t\t\t\t// nothing to compare against, and writing one is idempotent.\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t},\n\n\t\tasync setPolicy(bucket, policy) {\n\t\t\tawait s3.send(\n\t\t\t\tnew PutBucketPolicyCommand({ Bucket: bucket, Policy: policy }),\n\t\t\t);\n\t\t},\n\t};\n}\n","import {\n\tDEFAULT_POSTGRES_VERSION,\n\ttype PostgresVersion,\n} from '@geekmidas/manifest';\n/**\n * What gkm knows about each container it can derive.\n *\n * Everything provider-specific about the local target lives here: the image, the\n * ports the image listens on, and whether its data is worth keeping. The plan\n * says *which* containers exist and never says any of this, which is the split\n * the design asks for — neutral facts from the declaration, provider detail\n * from here and from config.\n *\n * Shared by allocation and by the compose writer so the two cannot disagree\n * about how many ports a container needs.\n */\n\n/**\n * The Postgres image for a major version.\n *\n * One place that knows the tag shape, so a declared version reaches the\n * container without anyone restating `-alpine` beside it.\n */\nexport function postgresImage(version: PostgresVersion): string {\n\treturn `postgres:${version}-alpine`;\n}\n\n/**\n * The default image for each container.\n *\n * Pinned to a major rather than `latest` where the image publishes one, so a\n * rebuild does not silently move a developer's Postgres a version. Overridable\n * per project: a project needing `postgis/postgis` says so in config.\n */\nexport const DEFAULT_IMAGES: Readonly<Record<string, string>> = {\n\tpostgres: postgresImage(DEFAULT_POSTGRES_VERSION),\n\tminio: 'pgsty/minio:RELEASE.2026-08-04T00-00-00Z',\n\tmailpit: 'axllent/mailpit:latest',\n\tredis: 'redis:8-alpine',\n\t'redis-http': 'hiett/serverless-redis-http:latest',\n\trabbitmq: 'rabbitmq:4-management-alpine',\n\tlocalstack: 'floci/floci:latest',\n\t// The local edge: TLS with its own CA, and host-based routing onto the\n\t// bucket behind it. Not a CDN — what is missing locally is the *mapping*\n\t// and the certificate, not caching, and a caching proxy would add an\n\t// invalidation story no local stack needs.\n\tcaddy: 'caddy:2-alpine',\n};\n\n/** One port an image listens on. */\nexport interface ContainerPort {\n\t/**\n\t * The name this port is allocated and persisted under.\n\t *\n\t * Per port rather than per container so that adding a console port later\n\t * assigns a new one instead of shifting the port the app already connects to.\n\t */\n\tkey: string;\n\t/** The port inside the container, fixed by the image. */\n\tinside: number;\n\t/** What it is for, in the one line `gkm status` prints. */\n\tlabel: string;\n}\n\n/**\n * The ports each container listens on.\n *\n * The first is always the one an app connects to; the rest are consoles and\n * management UIs, published because a bucket or an inbox you can look at is most\n * of why these are real containers rather than stubs.\n */\nconst PORTS: Readonly<Record<string, readonly ContainerPort[]>> = {\n\tpostgres: [{ key: 'postgres', inside: 5432, label: 'postgres' }],\n\tminio: [\n\t\t{ key: 'minio', inside: 9000, label: 'minio api' },\n\t\t{ key: 'minio-console', inside: 9001, label: 'minio console' },\n\t],\n\tmailpit: [\n\t\t{ key: 'mailpit', inside: 1025, label: 'smtp' },\n\t\t{ key: 'mailpit-web', inside: 8025, label: 'mailpit inbox' },\n\t],\n\tredis: [{ key: 'redis', inside: 6379, label: 'redis' }],\n\t'redis-http': [{ key: 'redis-http', inside: 80, label: 'cache' }],\n\trabbitmq: [\n\t\t{ key: 'rabbitmq', inside: 5672, label: 'amqp' },\n\t\t{ key: 'rabbitmq-management', inside: 15672, label: 'rabbitmq console' },\n\t],\n\tlocalstack: [{ key: 'localstack', inside: 4566, label: 'localstack' }],\n\t// One port, and an assigned one rather than 443. The whole point of\n\t// allocation is that two projects run at once, and an edge that insists on\n\t// the privileged port puts that back — at the cost of a port in the URL,\n\t// which no cookie or CORS rule looks at.\n\tcaddy: [{ key: 'caddy', inside: 443, label: 'https edge' }],\n};\n\n/** The ports a container needs published. Empty for one gkm does not know. */\nexport function portsOf(container: string): readonly ContainerPort[] {\n\treturn PORTS[container] ?? [];\n}\n\n/** The key a container's primary port is allocated under. */\nexport function primaryPortKey(container: string): string {\n\treturn portsOf(container)[0]?.key ?? container;\n}\n\n/**\n * Every port key a set of containers needs, in a stable order.\n *\n * What allocation is handed: it assigns ports to names and knows nothing about\n * what listens on them.\n */\nexport function portKeys(containers: readonly string[]): string[] {\n\treturn [...containers]\n\t\t.sort()\n\t\t.flatMap((container) => portsOf(container).map((port) => port.key));\n}\n\n/**\n * The named volume a container's data lives in.\n *\n * Mailpit deliberately has none: local mail is disposable, and an inbox that\n * survives days of dev is noise rather than state.\n */\nexport function volumeOf(container: string): string | undefined {\n\tconst volumes: Readonly<Record<string, string>> = {\n\t\tpostgres: 'postgres-data',\n\t\tminio: 'minio-data',\n\t\tredis: 'redis-data',\n\t\trabbitmq: 'rabbitmq-data',\n\t\tlocalstack: 'localstack-data',\n\t\t// Holds the CA it generated. Losing it means a new root on every start,\n\t\t// and a trust store full of dead authorities.\n\t\tcaddy: 'caddy-data',\n\t};\n\n\treturn volumes[container];\n}\n","/**\n * The compose document a plan implies.\n *\n * The third declaration of the same containers disappears here: today a\n * `services: ['postgres','minio']` block is kept in step with the code by hand,\n * which is the triplication this design exists to remove, in the one place you\n * touch every day. A `database` implies Postgres; the list is derived.\n *\n * Returns an object rather than a string for the same reason `plan` is pure —\n * what should exist is asserted as data, and turning it into YAML is one line at\n * the edge.\n *\n * What stays config is unchanged: images and versions are pinned explicitly, and\n * a container no construct implies is still declared by hand. Neutral facts come\n * from the declaration, provider detail from config.\n */\n\nimport { stringify } from 'yaml';\nimport { DEFAULT_IMAGES, portsOf, postgresImage, volumeOf } from './containers';\nimport type { Plan } from './plan';\nimport type { PortAssignments } from './ports';\n\n/** A compose service definition — as much of the schema as is emitted here. */\nexport interface ComposeService {\n\timage: string;\n\trestart?: string;\n\tentrypoint?: string;\n\tcommand?: string;\n\tports?: string[];\n\tenvironment?: Record<string, string>;\n\tvolumes?: string[];\n\tdepends_on?: string[];\n\thealthcheck?: {\n\t\ttest: string[];\n\t\tinterval: string;\n\t\ttimeout: string;\n\t\tretries: number;\n\t\tstart_period?: string;\n\t};\n\textra_hosts?: string[];\n}\n\nexport interface ComposeFile {\n\tname: string;\n\tservices: Record<string, ComposeService>;\n\tvolumes?: Record<string, Record<string, never>>;\n}\n\n/** The credential every derived container is brought up with locally. */\nconst LOCAL_USER = 'geekmidas';\n\n/**\n * The token the local cache accepts.\n *\n * Fixed rather than generated: it is only ever reachable on a port this project\n * published, and the URL that carries it is derived, so nothing is made safer\n * by making it unguessable and a stable one keeps `redis-cli` sessions working.\n */\nconst LOCAL_TOKEN = 'geekmidas';\n\nexport interface ComposeOptions {\n\t/** Names the compose project, which is what isolates it from other stacks. */\n\tproject: string;\n\t/** Ports assigned by `allocate`, keyed by port key — see `portKeys`. */\n\tports: PortAssignments;\n\t/** Per-container image overrides — the config half of the split. */\n\timages?: Readonly<Record<string, string>>;\n}\n\n/**\n * Build the compose document for a plan.\n *\n * Every container is published on its assigned port, never on the image's\n * default: the point of allocation is that two projects can run at once, and\n * one container publishing 5432 puts that back.\n */\nexport function composeFor(plan: Plan, options: ComposeOptions): ComposeFile {\n\tconst services: Record<string, ComposeService> = {};\n\tconst volumes: Record<string, Record<string, never>> = {};\n\n\tfor (const container of [...plan.containers].sort()) {\n\t\t// A declared version beats the default image, so the container a\n\t\t// developer runs is the major the declaration names — and the same one a\n\t\t// deploy provisions. An explicit `images` override still wins over both.\n\t\tconst declared =\n\t\t\tcontainer === 'postgres'\n\t\t\t\t? plan.resources.find((r) => r.version)?.version\n\t\t\t\t: undefined;\n\n\t\tconst image =\n\t\t\toptions.images?.[container] ??\n\t\t\t(declared ? postgresImage(declared) : DEFAULT_IMAGES[container]);\n\t\tif (!image) throw new UnknownContainer(container);\n\n\t\tservices[container] = define(\n\t\t\tcontainer,\n\t\t\timage,\n\t\t\tpublish(container, options.ports),\n\t\t);\n\n\t\tconst volume = volumeOf(container);\n\t\tif (volume) volumes[volume] = {};\n\t}\n\n\treturn {\n\t\t// Compose derives a project name from the directory otherwise, so two\n\t\t// checkouts of the same repo would fight over one set of containers.\n\t\tname: options.project,\n\t\tservices,\n\t\t...(Object.keys(volumes).length ? { volumes } : {}),\n\t};\n}\n\n/** Render a compose document as the file it becomes. */\nexport function toYaml(file: ComposeFile): string {\n\treturn `# Generated by gkm from the construct manifest — do not edit.\n# Containers are derived from what the app declares; images and anything no\n# construct implies are yours, in gkm.config.ts.\n#\n# Use \"gkm dev\" or \"gkm test\" to start these. Running \"docker compose up\"\n# directly will not inject secrets.\n${stringify(file, { lineWidth: 0 })}`;\n}\n\n/** A container with no image, from config or from the defaults. */\nexport class UnknownContainer extends Error {\n\t/** The container that could not be resolved. */\n\treadonly container: string;\n\t/** The containers that have a default, so the caller can see the set. */\n\treadonly known: readonly string[] = Object.keys(DEFAULT_IMAGES).sort();\n\n\tconstructor(container: string) {\n\t\tsuper('No image is known for this container; pin one in config');\n\t\tthis.name = 'UnknownContainer';\n\t\tthis.container = container;\n\t}\n}\n\n/** One container's definition, published on the port it was assigned. */\nfunction define(\n\tcontainer: string,\n\timage: string,\n\tpublished: string[],\n): ComposeService {\n\tswitch (container) {\n\t\tcase 'postgres':\n\t\t\treturn {\n\t\t\t\timage,\n\t\t\t\trestart: 'unless-stopped',\n\t\t\t\tports: published,\n\t\t\t\tenvironment: {\n\t\t\t\t\tPOSTGRES_USER: LOCAL_USER,\n\t\t\t\t\tPOSTGRES_PASSWORD: LOCAL_USER,\n\t\t\t\t\t// The cluster's own database. Every declared database is created\n\t\t\t\t\t// inside this cluster by the applier, not by the image.\n\t\t\t\t\tPOSTGRES_DB: LOCAL_USER,\n\t\t\t\t},\n\t\t\t\t// One mount at the parent, not at `data`: the 18 image keeps its\n\t\t\t\t// cluster in a version-named subdirectory and refuses to start when\n\t\t\t\t// something is mounted over the legacy path. Mounting the parent is\n\t\t\t\t// also what makes the next major bump a `pg_upgrade --link` rather\n\t\t\t\t// than a new volume.\n\t\t\t\tvolumes: ['postgres-data:/var/lib/postgresql'],\n\t\t\t\thealthcheck: {\n\t\t\t\t\ttest: ['CMD-SHELL', `pg_isready -U ${LOCAL_USER}`],\n\t\t\t\t\tinterval: '5s',\n\t\t\t\t\ttimeout: '3s',\n\t\t\t\t\tretries: 10,\n\t\t\t\t\tstart_period: '10s',\n\t\t\t\t},\n\t\t\t};\n\n\t\tcase 'minio':\n\t\t\treturn {\n\t\t\t\timage,\n\t\t\t\trestart: 'unless-stopped',\n\t\t\t\tports: published,\n\t\t\t\tenvironment: {\n\t\t\t\t\tMINIO_ROOT_USER: LOCAL_USER,\n\t\t\t\t\tMINIO_ROOT_PASSWORD: LOCAL_USER,\n\t\t\t\t},\n\t\t\t\t// No bucket is created here: buckets are planned resources, and\n\t\t\t\t// creating them in the entrypoint would put their names back in the\n\t\t\t\t// compose file — the declaration this change removes.\n\t\t\t\tcommand: 'server --console-address \":9001\" /data',\n\t\t\t\tvolumes: ['minio-data:/data'],\n\t\t\t\thealthcheck: {\n\t\t\t\t\ttest: ['CMD', 'mc', 'ready', 'local'],\n\t\t\t\t\tinterval: '10s',\n\t\t\t\t\ttimeout: '5s',\n\t\t\t\t\tretries: 5,\n\t\t\t\t\tstart_period: '10s',\n\t\t\t\t},\n\t\t\t};\n\n\t\tcase 'caddy':\n\t\t\treturn {\n\t\t\t\timage,\n\t\t\t\trestart: 'unless-stopped',\n\t\t\t\tports: published,\n\t\t\t\t// The generated Caddyfile sits beside this file, so a relative\n\t\t\t\t// mount resolves against `.gkm/` wherever the project lives.\n\t\t\t\tvolumes: [\n\t\t\t\t\t'./Caddyfile:/etc/caddy/Caddyfile:ro',\n\t\t\t\t\t// A directory, not a file: every stage writes its own routes into\n\t\t\t\t\t// it, so `gkm test` cannot delete what `gkm dev` is serving.\n\t\t\t\t\t'./caddy-sites:/etc/caddy/sites:ro',\n\t\t\t\t\t'caddy-data:/data',\n\t\t\t\t],\n\t\t\t\t// A surface and a site are processes on the *host*, not containers,\n\t\t\t\t// so the edge has to leave Docker's network to reach them. Docker\n\t\t\t\t// Desktop resolves this name already; on Linux it needs saying.\n\t\t\t\textra_hosts: ['host.docker.internal:host-gateway'],\n\t\t\t\thealthcheck: {\n\t\t\t\t\ttest: ['CMD', 'caddy', 'version'],\n\t\t\t\t\tinterval: '10s',\n\t\t\t\t\ttimeout: '5s',\n\t\t\t\t\tretries: 5,\n\t\t\t\t},\n\t\t\t};\n\n\t\tcase 'mailpit':\n\t\t\treturn {\n\t\t\t\timage,\n\t\t\t\trestart: 'unless-stopped',\n\t\t\t\tports: published,\n\t\t\t\tenvironment: {\n\t\t\t\t\t// Accept mail from anything on this network. The URL carries no\n\t\t\t\t\t// credentials locally, which is what the parser's optional `auth`\n\t\t\t\t\t// exists for.\n\t\t\t\t\tMP_SMTP_AUTH_ACCEPT_ANY: '1',\n\t\t\t\t\tMP_SMTP_AUTH_ALLOW_INSECURE: '1',\n\t\t\t\t},\n\t\t\t\thealthcheck: {\n\t\t\t\t\ttest: ['CMD', 'wget', '-q', '--spider', 'http://localhost:8025'],\n\t\t\t\t\tinterval: '10s',\n\t\t\t\t\ttimeout: '5s',\n\t\t\t\t\tretries: 5,\n\t\t\t\t},\n\t\t\t};\n\n\t\tcase 'redis':\n\t\t\treturn {\n\t\t\t\timage,\n\t\t\t\trestart: 'unless-stopped',\n\t\t\t\tports: published,\n\t\t\t\tvolumes: ['redis-data:/data'],\n\t\t\t\thealthcheck: {\n\t\t\t\t\ttest: ['CMD', 'redis-cli', 'ping'],\n\t\t\t\t\tinterval: '5s',\n\t\t\t\t\ttimeout: '3s',\n\t\t\t\t\tretries: 5,\n\t\t\t\t},\n\t\t\t};\n\n\t\tcase 'redis-http':\n\t\t\treturn {\n\t\t\t\timage,\n\t\t\t\trestart: 'unless-stopped',\n\t\t\t\tports: published,\n\t\t\t\tenvironment: {\n\t\t\t\t\tSRH_MODE: 'env',\n\t\t\t\t\tSRH_TOKEN: LOCAL_TOKEN,\n\t\t\t\t\t// The Redis beside it, by compose service name — the one address\n\t\t\t\t\t// in this file that is not published, because nothing outside the\n\t\t\t\t\t// network speaks to it.\n\t\t\t\t\tSRH_CONNECTION_STRING: 'redis://redis:6379',\n\t\t\t\t},\n\t\t\t\tdepends_on: ['redis'],\n\t\t\t\thealthcheck: {\n\t\t\t\t\t// 127.0.0.1, not localhost: the proxy binds IPv4 only, and\n\t\t\t\t\t// `localhost` resolves to ::1 first inside the container — which\n\t\t\t\t\t// is a health check that never passes and a container that never\n\t\t\t\t\t// comes up.\n\t\t\t\t\ttest: [\n\t\t\t\t\t\t'CMD',\n\t\t\t\t\t\t'wget',\n\t\t\t\t\t\t'-q',\n\t\t\t\t\t\t'-O',\n\t\t\t\t\t\t'/dev/null',\n\t\t\t\t\t\t'http://127.0.0.1:80/',\n\t\t\t\t\t],\n\t\t\t\t\tinterval: '5s',\n\t\t\t\t\ttimeout: '3s',\n\t\t\t\t\tretries: 10,\n\t\t\t\t},\n\t\t\t};\n\n\t\tcase 'rabbitmq':\n\t\t\treturn {\n\t\t\t\timage,\n\t\t\t\trestart: 'unless-stopped',\n\t\t\t\tports: published,\n\t\t\t\tenvironment: {\n\t\t\t\t\tRABBITMQ_DEFAULT_USER: LOCAL_USER,\n\t\t\t\t\tRABBITMQ_DEFAULT_PASS: LOCAL_USER,\n\t\t\t\t},\n\t\t\t\tvolumes: ['rabbitmq-data:/var/lib/rabbitmq'],\n\t\t\t\thealthcheck: {\n\t\t\t\t\ttest: ['CMD', 'rabbitmq-diagnostics', '-q', 'ping'],\n\t\t\t\t\tinterval: '10s',\n\t\t\t\t\ttimeout: '5s',\n\t\t\t\t\tretries: 5,\n\t\t\t\t},\n\t\t\t};\n\n\t\tcase 'localstack':\n\t\t\treturn {\n\t\t\t\timage,\n\t\t\t\trestart: 'unless-stopped',\n\t\t\t\tports: published,\n\t\t\t\tenvironment: {\n\t\t\t\t\tAWS_DEFAULT_REGION: 'us-east-1',\n\t\t\t\t\t// The prefix LocalStack required on issued keys. floci does not\n\t\t\t\t\t// care, and keeping it means a project that pins the old image\n\t\t\t\t\t// still works.\n\t\t\t\t\tAWS_ACCESS_KEY_ID: 'LSIAQAAAAAAVNCBMPNSG',\n\t\t\t\t\tAWS_SECRET_ACCESS_KEY: LOCAL_USER,\n\t\t\t\t},\n\t\t\t\tvolumes: ['localstack-data:/var/lib/localstack'],\n\t\t\t\thealthcheck: {\n\t\t\t\t\ttest: [\n\t\t\t\t\t\t'CMD',\n\t\t\t\t\t\t'curl',\n\t\t\t\t\t\t'-f',\n\t\t\t\t\t\t'http://localhost:4566/_localstack/health',\n\t\t\t\t\t],\n\t\t\t\t\tinterval: '10s',\n\t\t\t\t\ttimeout: '5s',\n\t\t\t\t\tretries: 5,\n\t\t\t\t\tstart_period: '10s',\n\t\t\t\t},\n\t\t\t};\n\n\t\tdefault:\n\t\t\tthrow new UnknownContainer(container);\n\t}\n}\n\n/**\n * The published port mappings for a container.\n *\n * Every port is looked up by its own key rather than being derived as an offset\n * from the container's first, so MinIO's console cannot land on whatever was\n * allocated to the next container.\n */\nfunction publish(container: string, ports: PortAssignments): string[] {\n\treturn portsOf(container).map((port) => {\n\t\tconst assigned = ports[port.key];\n\t\tif (assigned === undefined) throw new UnassignedPort(port.key);\n\n\t\treturn `${assigned}:${port.inside}`;\n\t});\n}\n\n/** A planned port that allocation never assigned. */\nexport class UnassignedPort extends Error {\n\t/** The port key that went unassigned, e.g. `minio-console`. */\n\treadonly key: string;\n\n\tconstructor(key: string) {\n\t\tsuper('No port was assigned for this container port');\n\t\tthis.name = 'UnassignedPort';\n\t\tthis.key = key;\n\t}\n}\n","/**\n * The real Docker, behind the small interface reconcile needs.\n *\n * Shells out to `docker compose` rather than talking to the daemon socket: it is\n * what a developer runs by hand when something is wrong, so the two cannot\n * disagree, and it keeps Docker out of this package's dependencies.\n *\n * Every call is scoped to the generated compose file. Nothing here can touch a\n * container this project did not generate.\n */\n\nimport { execFile } from 'node:child_process';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { promisify } from 'node:util';\nimport type { Docker } from './index';\n\nconst run = promisify(execFile);\n\n/** How long to wait for containers to pass their health checks. */\nconst HEALTH_TIMEOUT_MS = 120_000;\n\nexport const dockerCli: Docker = {\n\tasync publishedPort(composePath, service, inside) {\n\t\ttry {\n\t\t\tconst { stdout } = await run('docker', [\n\t\t\t\t'compose',\n\t\t\t\t'-f',\n\t\t\t\tcomposePath,\n\t\t\t\t'port',\n\t\t\t\tservice,\n\t\t\t\tString(inside),\n\t\t\t]);\n\n\t\t\t// `0.0.0.0:54321` — the part after the last colon is the host port.\n\t\t\tconst port = Number(stdout.trim().split(':').pop());\n\n\t\t\treturn Number.isFinite(port) && port > 0 ? port : undefined;\n\t\t} catch {\n\t\t\t// Not running, or no such service. Both mean \"nothing to reuse\".\n\t\t\treturn undefined;\n\t\t}\n\t},\n\n\tasync up(composePath, services) {\n\t\tawait run('docker', [\n\t\t\t'compose',\n\t\t\t'-f',\n\t\t\tcomposePath,\n\t\t\t'up',\n\t\t\t'-d',\n\t\t\t// Recreate anything whose definition moved — a changed image pin is\n\t\t\t// exactly the case the hash detects, and leaving the old container\n\t\t\t// running would make the reconcile a lie.\n\t\t\t'--remove-orphans',\n\t\t\t'--wait',\n\t\t\t`--wait-timeout=${Math.floor(HEALTH_TIMEOUT_MS / 1000)}`,\n\t\t\t...services,\n\t\t]);\n\t},\n\n\tasync copyOut(composePath, service, from, to) {\n\t\tawait mkdir(dirname(to), { recursive: true });\n\t\tawait run('docker', [\n\t\t\t'compose',\n\t\t\t'-f',\n\t\t\tcomposePath,\n\t\t\t'cp',\n\t\t\t`${service}:${from}`,\n\t\t\tto,\n\t\t]);\n\t},\n\n\tasync reload(composePath, service) {\n\t\t// Caddy's own reload is graceful and validates first, so a config it\n\t\t// refuses leaves the previous one serving rather than dropping the edge.\n\t\tawait run('docker', [\n\t\t\t'compose',\n\t\t\t'-f',\n\t\t\tcomposePath,\n\t\t\t'exec',\n\t\t\t'-T',\n\t\t\tservice,\n\t\t\t'caddy',\n\t\t\t'reload',\n\t\t\t'--config',\n\t\t\t'/etc/caddy/Caddyfile',\n\t\t]);\n\t},\n\n\tasync healthy(composePath, services) {\n\t\tif (services.length === 0) return true;\n\n\t\ttry {\n\t\t\tconst { stdout } = await run('docker', [\n\t\t\t\t'compose',\n\t\t\t\t'-f',\n\t\t\t\tcomposePath,\n\t\t\t\t'ps',\n\t\t\t\t'--format',\n\t\t\t\t'json',\n\t\t\t\t...services,\n\t\t\t]);\n\n\t\t\tconst running = new Set<string>();\n\t\t\tfor (const line of stdout.trim().split('\\n').filter(Boolean)) {\n\t\t\t\tconst entry = JSON.parse(line) as {\n\t\t\t\t\tService?: string;\n\t\t\t\t\tState?: string;\n\t\t\t\t\tHealth?: string;\n\t\t\t\t};\n\n\t\t\t\t// A container with a health check must be passing it; one without\n\t\t\t\t// only has to be running, since there is nothing better to ask.\n\t\t\t\tconst ok =\n\t\t\t\t\tentry.State === 'running' &&\n\t\t\t\t\t(entry.Health === undefined ||\n\t\t\t\t\t\tentry.Health === '' ||\n\t\t\t\t\t\tentry.Health === 'healthy');\n\n\t\t\t\tif (ok && entry.Service) running.add(entry.Service);\n\t\t\t}\n\n\t\t\treturn services.every((service) => running.has(service));\n\t\t} catch {\n\t\t\t// No compose file yet, or no daemon. Either way, not healthy.\n\t\t\treturn false;\n\t\t}\n\t},\n};\n\nexport { HEALTH_TIMEOUT_MS };\n","/**\n * The reconcile plan — what a manifest wants to exist, for one stage.\n *\n * Pure. Turning the plan into running containers and provisioned resources is\n * the applier's job; deciding *what* should exist is this, so the decisions can\n * be asserted as data rather than by starting Docker.\n *\n * One plan serves every stage. `gkm dev` reconciles `development` and `gkm test`\n * reconciles `test`; they differ only in what the resources are called, never in\n * what infrastructure exists. Roles are cluster-scoped in Postgres while grants\n * are per-object, so one container and one role pair serve both.\n */\n\nimport {\n\ttype ConstructManifest,\n\ttype Declaration,\n\ttype DeclarationKind,\n\tdependentsOf,\n\ttype PostgresVersion,\n\tprovidedKeyFor,\n\tpublicEnvFor,\n} from '@geekmidas/manifest';\nimport { type CacheBackend, DEFAULT_CACHE, type EventsBackend } from '../types';\nimport { EDGE_KINDS } from './caddyfile';\n\n/** The stage whose resources carry no suffix. */\nexport const DEFAULT_STAGE = 'development';\n\n/**\n * Which container serves a kind.\n *\n * Derived kinds map to the same container as their parent — a schema tenant\n * lives in its parent's Postgres — so the set is deduplicated rather than\n * counted.\n */\nconst CONTAINERS: Partial<Record<DeclarationKind, string>> = {\n\tdatabase: 'postgres',\n\t'database-reader': 'postgres',\n\t'database-schema': 'postgres',\n\tobjects: 'minio',\n\t// The server is not the bucket. Its own container is the local edge, which\n\t// is what lets it answer on a *host* rather than on a path under MinIO —\n\t// the shape it has deployed, and the one MinIO's virtual-host mode cannot\n\t// produce because that mode reads the leading label as the bucket name.\n\t//\n\t// MinIO still runs: it is implied by the bucket this server is declared\n\t// over, which is a parent it always has.\n\t'file-server': 'caddy',\n\t// Every mail backend speaks SMTP, so locally there is one answer whichever\n\t// one is selected — which is the same reason the declaration has no\n\t// `provider` field.\n\temail: 'mailpit',\n};\n\n/**\n * The container a cache backend needs locally.\n *\n * Each one matches the protocol its deployed form speaks, which is the property\n * worth having: a cache that behaves differently in the two places is worse than\n * a slower one.\n *\n * `db` is deliberately absent, the same way `pgboss` is absent from\n * `EVENT_CONTAINERS`: the cache is a table in a database the manifest already\n * declares, so mapping it here would start a container for a project that needs\n * none.\n */\nconst CACHE_CONTAINERS: Partial<Record<CacheBackend, string>> = {\n\t// The proxy, not the Redis behind it: the client speaks HTTP with a token\n\t// wherever it runs, which is what makes dev and prod the same client.\n\tupstash: 'redis-http',\n\t// The wire protocol, which is what ElastiCache offers — so dev speaks it too.\n\telasticache: 'redis',\n};\n\n/**\n * Kinds that resolve a value without anything running.\n *\n * A secret is the case: it has no address, so there is nothing to start and\n * nothing to connect to — but it still appears in the plan, because the target\n * still has to resolve it.\n */\nconst CONTAINERLESS: Partial<Record<DeclarationKind, true>> = {\n\tsecret: true,\n\t// A credential has no address either — and unlike a secret it has no value\n\t// this target can derive, because it was issued by somebody else.\n\tcredential: true,\n\t// A surface answers on the app's own port. It is the first kind whose\n\t// address belongs to something gkm starts rather than something Docker does.\n\t'rest-api': true,\n\t// A site is the second: it is served by its own framework's dev server, and\n\t// what the target resolves for it is only where that server answers.\n\tsite: true,\n};\n\n/**\n * Containers that cannot run alone.\n *\n * The one case is the cache: `serverless-redis-http` is a proxy and needs the\n * Redis it proxies. Kept here rather than in the compose definition so the plan\n * -- which is what decides what must be running -- still names everything.\n */\nconst REQUIRES: Readonly<Record<string, readonly string[]>> = {\n\t'redis-http': ['redis'],\n};\n\n/**\n * The role each kind's one key plays, where it is not a URL.\n *\n * The twin of `ProvidesByKind` in `@geekmidas/manifest`: that is what a\n * construct declares, this is what the local target resolves, and both derive\n * the key from the same `provideKey` so they cannot disagree.\n */\nconst ROLES: Partial<Record<DeclarationKind, string>> = {\n\tqueue: 'publisherConnectionString',\n\ttopic: 'publisherConnectionString',\n\tcredential: 'credential',\n};\n\n/** The kinds whose container is the events backend's rather than their own. */\nconst EVENT_KINDS: Partial<Record<DeclarationKind, true>> = {\n\tqueue: true,\n\ttopic: true,\n};\n\n/**\n * Which kinds provision something *inside* their container.\n *\n * A database creates a database, a bucket creates a bucket; mail creates\n * nothing — Mailpit accepts whatever is sent to it and has no per-stage object\n * to name. Both still appear in the plan, because both still resolve a URL; the\n * flag is the difference between \"must be running\" and \"must be created\".\n */\nconst PROVISIONS: Partial<Record<DeclarationKind, true>> = {\n\tdatabase: true,\n\t'database-reader': true,\n\t'database-schema': true,\n\tobjects: true,\n\t// Only a cache that lives in a database creates anything — its table. The\n\t// other backends have nothing to create inside the container they use.\n\tcache: true,\n\t// The open patterns become a bucket policy on the origin — real\n\t// enforcement locally, from the same declaration the CDN behaviour comes\n\t// from deployed.\n\t'file-server': true,\n};\n// Queues and topics are absent deliberately: pg-boss creates its own schema and\n// tables on first connect, and a RabbitMQ exchange is declared by the client\n// that binds to it. Both still appear in the plan, because both still resolve a\n// connection string.\n\n/**\n * The container each events backend needs, where it needs one of its own.\n *\n * `pgboss` is deliberately absent rather than mapped to `postgres`: it is a\n * schema tenant in a database the manifest already declares, so mapping it here\n * would start a Postgres for a project that declared no database at all.\n *\n * This one comes from config rather than from a declaration because there are\n * no `topic` or `queue` kinds yet. When they land the mapping moves to\n * {@link CONTAINERS} keyed by kind, and this constant goes with them.\n */\nconst EVENT_CONTAINERS: Partial<Record<EventsBackend, string>> = {\n\tsns: 'localstack',\n\trabbitmq: 'rabbitmq',\n};\n\n/** The backend a project gets when it declares queues or topics and says nothing. */\nexport const DEFAULT_EVENTS: EventsBackend = 'pgboss';\n\n/**\n * How a resource of each kind separates its stage suffix.\n *\n * Postgres identifiers are snake_case and a hyphen would need quoting\n * everywhere; buckets and queues are kebab and disallow underscores in some\n * providers. The suffix is the same word either way.\n */\nconst SEPARATORS: Partial<Record<DeclarationKind, string>> = {\n\tdatabase: '_',\n\t'database-reader': '_',\n\t'database-schema': '_',\n\tobjects: '-',\n};\n\n/** One thing that must exist, and what it is called in this stage. */\nexport interface PlannedResource {\n\t/** The construct's canonical id, e.g. `Orders`. */\n\tid: string;\n\tkind: DeclarationKind;\n\t/** The container that serves it, e.g. `postgres`. Absent for a secret. */\n\tcontainer?: string;\n\t/** The stage-scoped name, e.g. `orders_test`. */\n\tname: string;\n\t/** The env key it resolves onto anything depending on it. */\n\tenvKey: string;\n\t/** The parent it derives from, for kinds that provision nothing themselves. */\n\tof?: string;\n\t/**\n\t * Whether anything has to be created inside the container for this.\n\t *\n\t * False for mail: Mailpit takes whatever is sent to it. The applier skips\n\t * these; the env writer still resolves their URL.\n\t */\n\tprovisions: boolean;\n\t/** The schema a tenant pins on its roles' `search_path`. */\n\tschema?: string;\n\t/** For a cache in a database: the table entries are kept in. */\n\ttable?: string;\n\t/**\n\t * Whether to provision the owner/runtime role split.\n\t *\n\t * `false` is the documented downgrade — both URLs fall back to the cluster's\n\t * master credential — and it is a choice someone made rather than a default\n\t * they got, which is why it is carried through rather than inferred.\n\t */\n\troles?: boolean;\n\t/**\n\t * For a database: the engine major version to run.\n\t *\n\t * Carried like `schema` and `roles` — read off the declaration so the\n\t * container tag and the deployed engine come from one statement. They used\n\t * to come from two, and had drifted a major apart with nothing recording it.\n\t */\n\tversion?: PostgresVersion;\n\t/**\n\t * For a file server: the paths it serves without a signature.\n\t *\n\t * Carried into the plan because the local target enforces them for real — a\n\t * bucket policy with prefix resources, from the same declaration the CDN\n\t * behaviour comes from deployed. An open path that is open locally and shut\n\t * in production is the bug this avoids.\n\t */\n\topen?: readonly string[];\n\t/**\n\t * For a site: the keys its bundle needs, mapped to the key each value comes\n\t * from — `{ VITE_API_URL: 'API_URL' }`.\n\t *\n\t * One neutral name from the construct and one serialisation per framework.\n\t * The prefix is the whole of what varies, which is why the mapping is a\n\t * rename rather than a second derivation: `API_URL` is resolved once, by the\n\t * same code that resolves it for the server, and a site simply reads it\n\t * under the name its bundler will inline.\n\t */\n\tpublicEnv?: Record<string, string>;\n\t/**\n\t * The ids that depend on this one — the graph read backwards.\n\t *\n\t * Carried on surfaces, where it is the whole answer to \"who may call this\":\n\t * CORS origins, trusted origins, and the cookie domain are three readings of\n\t * this one list. Resolved to addresses by the target, because the manifest\n\t * knows which constructs call which and only the target knows where any of\n\t * them answer.\n\t */\n\tcallers?: string[];\n}\n\nexport interface Plan {\n\tstage: string;\n\t/**\n\t * The events backend this plan was built for.\n\t *\n\t * Carried on the plan rather than passed alongside it, because everything\n\t * downstream that composes a connection string needs it and reading it off\n\t * the plan is what keeps the two from disagreeing.\n\t */\n\tevents: EventsBackend;\n\t/** Where the cache lives, for everything downstream that composes its URL. */\n\tcache: CacheBackend;\n\t/** Containers to start, deduplicated. */\n\tcontainers: string[];\n\t/**\n\t * Everything the stage resolves a URL for, parents before children.\n\t *\n\t * Not all of it is provisioned — see `PlannedResource.provisions`.\n\t */\n\tresources: PlannedResource[];\n}\n\n/** What the plan needs that the manifest cannot tell it. */\nexport interface PlanOptions {\n\t/**\n\t * Whether the local edge fronts the addresses this plan resolves.\n\t *\n\t * On by default, because deployed every surface, site and file server has a\n\t * real hostname and a certificate, and the point of the local target is to\n\t * be the same shape rather than a near-enough one. Off falls back to\n\t * `http://localhost:<port>`.\n\t *\n\t * Config rather than declaration for the same reason `cache` is: the\n\t * application reads whichever address was injected and composes none, so it\n\t * cannot tell the difference.\n\t */\n\tedge?: boolean;\n\t/**\n\t * Where a declared cache lives, which decides both the container it needs\n\t * locally and the protocol its URL speaks.\n\t *\n\t * Config rather than declaration, because the same application code caches\n\t * into any of them — the same reason `events` is config.\n\t */\n\tcache?: CacheBackend;\n\t/**\n\t * The events backend, which selects a container of its own for everything\n\t * except pg-boss.\n\t *\n\t * Config rather than declaration only until `topic` and `queue` are kinds —\n\t * see {@link EVENT_CONTAINERS}.\n\t */\n\tevents?: EventsBackend;\n\t/**\n\t * Containers no construct implies — the escape hatch, and the whole of what\n\t * the workspace `services:` block becomes.\n\t *\n\t * A list of exceptions rather than a list you maintain: everything a\n\t * construct implies is already in the plan before this is read.\n\t */\n\textraContainers?: readonly string[];\n}\n\n/**\n * The stage-scoped name for a resource.\n *\n * The default stage carries no suffix, so `gkm dev` keeps using `orders` and a\n * developer's psql history, saved connections, and muscle memory survive this\n * change. Every other stage is suffixed.\n */\nexport function resourceName(\n\tid: string,\n\tkind: DeclarationKind,\n\tstage: string,\n): string {\n\tconst base = id.toLowerCase();\n\tif (stage === DEFAULT_STAGE) return base;\n\n\treturn `${base}${SEPARATORS[kind] ?? '-'}${stage}`;\n}\n\n/**\n * The container a kind needs, or `undefined` if it needs none.\n *\n * Queues and topics have no container of their own — they live in whichever\n * broker the project selected, which is why the backend is an argument here\n * rather than a second pass that adds containers after the fact.\n */\nexport function containerFor(\n\tkind: DeclarationKind,\n\tevents: EventsBackend = DEFAULT_EVENTS,\n\t// The target-aware choice is made upstream, in `reconcile/workspace.ts`,\n\t// where the workspace knows where it deploys. This fires only for a caller\n\t// that passed nothing at all.\n\tcache: CacheBackend = DEFAULT_CACHE.aws,\n\t/** Whether the declaration named a parent — see the `cache` branch. */\n\tderived = false,\n\t/** Whether the local edge exists — see the `file-server` branch. */\n\tedge = true,\n): string | undefined {\n\tif (EVENT_KINDS[kind]) {\n\t\t// pg-boss is deliberately absent from EVENT_CONTAINERS: it is a schema\n\t\t// tenant in a database the manifest already declares, so its container is\n\t\t// that database's.\n\t\treturn EVENT_CONTAINERS[events] ?? CONTAINERS.database;\n\t}\n\n\t// A file server answers on the edge, which is what gives it a host of its\n\t// own. With the edge off it falls back to the object store that holds its\n\t// objects — a path under MinIO, which works and is not the deployed shape.\n\tif (kind === 'file-server') return edge ? 'caddy' : CONTAINERS.objects;\n\n\t// The same shape: a cache in the database lives in the database's container.\n\tif (kind === 'cache') {\n\t\t// A cache that *named* a database is in it whatever the backend config\n\t\t// says — the declaration is the stronger statement, and config choosing\n\t\t// otherwise would move a cache the app said lives here.\n\t\tif (derived) return CONTAINERS.database;\n\n\t\treturn CACHE_CONTAINERS[cache] ?? CONTAINERS.database;\n\t}\n\n\treturn CONTAINERS[kind];\n}\n\n/**\n * Build the plan for a manifest and stage.\n *\n * `order` is the caller's provisioning order — parents before children — which\n * `provisionOrder` produces from the manifest. Passing it in keeps this\n * function free of the traversal and makes the ordering assertable on its own.\n */\nexport function planFor(\n\tmanifest: ConstructManifest,\n\tstage: string,\n\torder: readonly string[],\n\toptions: PlanOptions = {},\n): Plan {\n\tconst containers = new Set<string>();\n\tconst resources: PlannedResource[] = [];\n\n\tconst events = options.events ?? DEFAULT_EVENTS;\n\tconst cache = options.cache ?? DEFAULT_CACHE.aws;\n\n\tfor (const id of order) {\n\t\tconst declaration: Declaration | undefined = manifest[id];\n\t\tif (!declaration) continue;\n\n\t\tconst container = containerFor(\n\t\t\tdeclaration.kind,\n\t\t\tevents,\n\t\t\tcache,\n\t\t\t'of' in declaration && typeof declaration.of === 'string',\n\t\t\toptions.edge !== false,\n\t\t);\n\t\tif (!container && !CONTAINERLESS[declaration.kind]) continue;\n\n\t\tif (container) {\n\t\t\tcontainers.add(container);\n\t\t\tfor (const required of REQUIRES[container] ?? [])\n\t\t\t\tcontainers.add(required);\n\t\t}\n\n\t\tresources.push({\n\t\t\tid,\n\t\t\tkind: declaration.kind,\n\t\t\t...(container ? { container } : {}),\n\t\t\tname: resourceName(id, declaration.kind, stage),\n\t\t\t// Through the shared derivation: a secret's name *is* its key, and\n\t\t\t// this target and the deploy target must not answer that separately.\n\t\t\tenvKey: providedKeyFor(\n\t\t\t\tid,\n\t\t\t\tdeclaration.kind,\n\t\t\t\tROLES[declaration.kind] ?? 'url',\n\t\t\t),\n\t\t\tprovisions: PROVISIONS[declaration.kind] === true,\n\t\t\t// Only for surfaces. Every other kind is reached by a URL that says\n\t\t\t// nothing about who holds it, so a caller list would be a fact with\n\t\t\t// no reader.\n\t\t\t...(declaration.kind === 'rest-api'\n\t\t\t\t? { callers: dependentsOf(manifest, id) }\n\t\t\t\t: {}),\n\t\t\t...(declaration.kind === 'site'\n\t\t\t\t? { publicEnv: publicEnvFor(declaration, manifest) }\n\t\t\t\t: {}),\n\t\t\t...(declaration.kind === 'file-server' && declaration.open?.length\n\t\t\t\t? { open: declaration.open }\n\t\t\t\t: {}),\n\t\t\t...('of' in declaration ? { of: declaration.of } : {}),\n\t\t\t...('schema' in declaration && declaration.schema\n\t\t\t\t? { schema: declaration.schema }\n\t\t\t\t: {}),\n\t\t\t...('version' in declaration && declaration.version\n\t\t\t\t? { version: declaration.version }\n\t\t\t\t: {}),\n\t\t\t...('roles' in declaration && declaration.roles === false\n\t\t\t\t? { roles: false }\n\t\t\t\t: {}),\n\t\t\t...('table' in declaration && declaration.table\n\t\t\t\t? { table: declaration.table }\n\t\t\t\t: {}),\n\t\t});\n\t}\n\n\t// The edge serves anything that owns a public address — a surface and a site\n\t// as much as a file server. Added here rather than in `containerFor` because\n\t// those two are containerless in every other sense: nothing runs them in\n\t// Docker, and what the edge does is put a hostname and a certificate in\n\t// front of a process on the host.\n\t//\n\t// `edge: false` opts out, and then every local address is\n\t// `http://localhost:<port>` as before. The application cannot tell: it reads\n\t// whichever address was injected and composes none.\n\tif (\n\t\toptions.edge !== false &&\n\t\tresources.some((r) => EDGE_KINDS[r.kind] === true)\n\t) {\n\t\tcontainers.add('caddy');\n\t}\n\n\tfor (const extra of options.extraContainers ?? []) containers.add(extra);\n\n\t// pg-boss lives in a declared database. Without one there is nothing for it\n\t// to live in, and starting a Postgres to hold only a queue would be the\n\t// container this design refuses to invent.\n\tif (\n\t\tevents === 'pgboss' &&\n\t\tresources.some((r) => EVENT_KINDS[r.kind]) &&\n\t\t!resources.some((r) => r.kind === 'database')\n\t) {\n\t\tthrow new PgBossNeedsDatabase(\n\t\t\tresources.filter((r) => EVENT_KINDS[r.kind]).map((r) => r.id),\n\t\t);\n\t}\n\n\t// A cache the config placed in a database becomes an *edge*, here, once.\n\t//\n\t// `services.cache: 'db'` names something that exists in this graph, which\n\t// makes it different in kind from `upstash` or `elasticache`: those name a\n\t// backend nothing declares, while this one names a database sitting right\n\t// there. So it is resolved to `of` — the same field `database.cache()` sets\n\t// — and every reader downstream follows an edge instead of re-deriving the\n\t// answer from a string.\n\t//\n\t// It also turns a silent guess into an error. \"The declared database\" is\n\t// unambiguous with one and arbitrary with two, and picking whichever came\n\t// first would put the cache in a database nobody chose.\n\t//\n\t// A cache that named its parent is untouched: the declaration is the\n\t// stronger statement, and its parent is checked by `assertDerivations`,\n\t// which names the database that is missing rather than reporting that some\n\t// database is.\n\tif (cache === 'db') {\n\t\tconst placeless = resources.filter((r) => r.kind === 'cache' && !r.of);\n\n\t\tif (placeless.length > 0) {\n\t\t\tconst databases = resources.filter((r) => r.kind === 'database');\n\t\t\tconst [only] = databases;\n\n\t\t\t// Starting a Postgres to hold only a cache would be the container this\n\t\t\t// design refuses to invent.\n\t\t\tif (!only) {\n\t\t\t\tthrow new CacheNeedsDatabase(\n\t\t\t\t\tresources.filter((r) => r.kind === 'cache').map((r) => r.id),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (databases.length > 1) {\n\t\t\t\tthrow new CacheIsAmbiguous(\n\t\t\t\t\tplaceless.map((r) => r.id),\n\t\t\t\t\tdatabases.map((r) => r.id),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tfor (const resource of placeless) resource.of = only.id;\n\t\t}\n\t}\n\n\treturn { stage, events, cache, containers: [...containers], resources };\n}\n\n/**\n * Queues or topics were declared with pg-boss and no database to hold them.\n *\n * Not a reconcile failure but a manifest one: the fix is a line of application\n * code either way — declare a database, or select a backend that brings its own\n * broker.\n */\nexport class CacheNeedsDatabase extends Error {\n\tconstructor(readonly ids: readonly string[]) {\n\t\tsuper(\n\t\t\t`A cache backed by the database needs a declared database, and none ` +\n\t\t\t\t`was declared. Declare one, or set services.cache to 'upstash' or ` +\n\t\t\t\t`'elasticache'. Caches affected: ${ids.join(', ')}.`,\n\t\t);\n\t\tthis.name = 'CacheNeedsDatabase';\n\t}\n}\n\n/**\n * `services.cache: 'db'` in an app that declares more than one database.\n *\n * The config says *a* database and the graph offers several, so there is no\n * answer to give — and the wrong kind of failure would be to pick one, since a\n * cache silently landing in the wrong database is a bug that surfaces as\n * missing rows much later. The fix is a stronger statement in application code:\n * `orders.cache('Sessions')` names the database, which is a fact about the\n * application rather than a deployment choice.\n */\nexport class CacheIsAmbiguous extends Error {\n\tconstructor(\n\t\treadonly ids: readonly string[],\n\t\treadonly databases: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`A cache backed by the database needs to know which one, and this app ` +\n\t\t\t\t`declares ${databases.length}: ${databases.join(', ')}. Declare the ` +\n\t\t\t\t`cache from its database — e.g. ${databases[0]}.cache('Sessions') — ` +\n\t\t\t\t`rather than with services.cache. Caches affected: ${ids.join(', ')}.`,\n\t\t);\n\t\tthis.name = 'CacheIsAmbiguous';\n\t}\n}\n\nexport class PgBossNeedsDatabase extends Error {\n\tconstructor(readonly ids: readonly string[]) {\n\t\tsuper(\n\t\t\t`pg-boss keeps its queues in a declared database, and none was declared. ` +\n\t\t\t\t`Declare one, or set services.events to 'rabbitmq' or 'sns'. ` +\n\t\t\t\t`Queues and topics affected: ${ids.join(', ')}.`,\n\t\t);\n\t\tthis.name = 'PgBossNeedsDatabase';\n\t}\n}\n","/**\n * The URLs a reconciled stage injects.\n *\n * One `<NAME>_URL` per construct, and the protocol picks the driver. This is the\n * seam between the target that writes a URL and the client that parses it, so\n * every component of it is read off the resource — never inherited from ambient\n * environment. A bucket can live in a different region than the function reading\n * it, and `AWS_REGION` in a Lambda is the *function's* region, so a URL that\n * omits `?region=` breaks cross-region silently at runtime.\n *\n * Locally that self-containment is what lets the port be whatever was free: the\n * app reads `ORDERS_URL` and never sees a host or a port.\n */\n\nimport { createHash } from 'node:crypto';\nimport { ownerRole, readerRole } from '@geekmidas/db/pg/roles';\nimport { cacheTable, cookieDomain, provideKey } from '@geekmidas/manifest';\nimport { hostFor } from './caddyfile';\nimport { primaryPortKey } from './containers';\nimport { PgBossNeedsDatabase, type Plan, type PlannedResource } from './plan';\nimport type { PortAssignments } from './ports';\n\n/**\n * The credential local containers are brought up with.\n *\n * The design's `roles: false` case — both URLs fall back to the cluster's master\n * credential. A deliberate downgrade, not a default: it holds until the role DDL\n * lands in `@geekmidas/db`, at which point the runtime role's URL replaces this\n * and the owner URL stops being reachable from any manifest edge.\n */\nconst LOCAL_USER = 'geekmidas';\n\n/** The host containers are published on. */\nconst LOCAL_HOST = 'localhost';\n\n/** The region local buckets claim, so the client has one to sign with. */\nconst LOCAL_REGION = 'us-east-1';\n\n/**\n * The credentials the S3 client resolves for MinIO.\n *\n * An `s3://` URL deliberately carries none: deployed, the SDK reads them from\n * the execution role, so a URL that embedded a key would be one more thing to\n * rotate and one more thing to leak into a log. Locally there is no role, and\n * the same chain reads these — which is why they are injected beside the URL\n * rather than written into it.\n */\nconst STORAGE_CREDENTIALS: Readonly<Record<string, string>> = {\n\tAWS_ACCESS_KEY_ID: LOCAL_USER,\n\tAWS_SECRET_ACCESS_KEY: LOCAL_USER,\n\tAWS_REGION: LOCAL_REGION,\n};\n\n/** The token the local cache proxy accepts. Matches the compose definition. */\nconst LOCAL_TOKEN = 'geekmidas';\n\n/** The schema pg-boss keeps its tables in, inside the declared database. */\nconst PGBOSS_SCHEMA = 'pgboss';\n\n/** The exchange every local topic and queue shares on RabbitMQ. */\nconst RABBITMQ_EXCHANGE = 'geekmidas.events';\n\nexport interface EnvOptions {\n\t/** Assigned ports, keyed by port key. */\n\tports: PortAssignments;\n\t/**\n\t * The project, which seeds any secret this stage derives.\n\t *\n\t * Derived rather than random so it survives a restart -- a signing key that\n\t * changed on every `gkm dev` would invalidate every session you were in the\n\t * middle of -- and seeded by the project so two checkouts do not share one.\n\t */\n\tproject?: string;\n\t/**\n\t * Where each address-owning construct answers, keyed by id — surfaces and\n\t * sites, e.g. `{ Api: 'http://localhost:3000', Console: 'http://localhost:5173' }`.\n\t *\n\t * Not container addresses: `gkm dev` assigns these, which is why they arrive\n\t * here rather than being read off a published port. Keyed by id rather than\n\t * being one `surface` string because there is more than one address in a\n\t * workspace and the difference between them is exactly what CORS is about.\n\t */\n\taddresses?: Readonly<Record<string, string>>;\n\t/**\n\t * The domain mail is sent from locally.\n\t *\n\t * Stage config, exactly as it is deployed — the difference is only that here\n\t * nobody had to verify it.\n\t */\n\tmailFrom?: string;\n}\n\n/**\n * Every env key a reconciled plan resolves, and its value.\n *\n * Keys come from the declaration rather than being re-derived, so what the build\n * publishes and what the app reads cannot drift.\n */\nexport function envFor(\n\tplan: Plan,\n\toptions: EnvOptions,\n): Record<string, string> {\n\tconst env: Record<string, string> = {};\n\n\t// Resolved once, up front, because a surface's cookie domain and origin list\n\t// are derived from *other constructs'* addresses — and reading them as the\n\t// loop happened to reach them would make the answer depend on the order the\n\t// manifest was keyed in.\n\tconst resolved: Record<string, string> = {};\n\tfor (const resource of plan.resources) {\n\t\tconst url = urlFor(\n\t\t\tresource,\n\t\t\tplan,\n\t\t\toptions.ports,\n\t\t\toptions.project ?? '',\n\t\t\toptions.addresses,\n\t\t);\n\t\tif (url) resolved[resource.id] = url;\n\t}\n\n\tfor (const resource of plan.resources) {\n\t\tconst url = resolved[resource.id];\n\n\t\t// A surface carries the origins its callers may come from, and they are\n\t\t// its inbound edges — nothing more. Better Auth's CSRF check applies to\n\t\t// every caller and not only to browsers, so a sibling service calling it\n\t\t// is rejected unless its origin is listed; that this list is now the\n\t\t// graph rather than every app the workspace happens to run is what makes\n\t\t// it the same list deployed, where no workspace is watching.\n\t\tif (resource.kind === 'rest-api') {\n\t\t\tObject.assign(env, surfaceEnv(resource, url, resolved));\n\t\t}\n\t\tif (url) env[resource.envKey] = url;\n\n\t\t// A database owns a second key, and it is deliberately not in `provides`:\n\t\t// the owner URL is what a migrator connects with, so no edge in any\n\t\t// manifest can name it and nothing can be granted DDL rights by mistake.\n\t\t// `gkm exec` injects it; a handler never sees it.\n\t\tif (isDatabase(resource.kind) && resource.roles !== false) {\n\t\t\tconst owner = ownerUrl(\n\t\t\t\tresource,\n\t\t\t\tplan,\n\t\t\t\toptions.ports,\n\t\t\t\toptions.project ?? '',\n\t\t\t);\n\t\t\tif (owner) env[provideKey(resource.id, 'ownerUrl')] = owner;\n\t\t}\n\n\t\t// Mail owns a second key. It is the sending identity, which is the one\n\t\t// thing about mail that differs per stage — so it travels with the URL\n\t\t// rather than being written into the construct.\n\t\tif (resource.kind === 'email' && url) {\n\t\t\tenv[provideKey(resource.id, 'from')] =\n\t\t\t\toptions.mailFrom ?? `noreply@${LOCAL_HOST}`;\n\t\t}\n\t}\n\n\t// After the loop, and deliberately: a site's keys are renames of values the\n\t// constructs it depends on resolved, so every source has to exist before any\n\t// of them can be read. Doing it inline would make the result depend on the\n\t// order the manifest happened to be keyed in.\n\tObject.assign(env, publicEnv(plan, env));\n\n\tObject.assign(env, brokerEnv(plan, options.ports));\n\n\t// Only once a bucket actually resolved: an unresolvable plan resolves\n\t// nothing, and credentials for a container that is not running are noise.\n\tif (plan.resources.some((r) => r.kind === 'objects' && env[r.envKey])) {\n\t\tObject.assign(env, STORAGE_CREDENTIALS);\n\t}\n\n\treturn env;\n}\n\n/**\n * The values a site's bundler inlines, under the names it inlines them by.\n *\n * A rename and nothing more. `API_URL` was resolved once, by the same code that\n * resolved it for the server; a site reads the same value under `VITE_API_URL`\n * because that prefix is how its bundler is told to ship it. Nothing is derived\n * twice, so a site and its API cannot come to disagree about where the API is.\n *\n * A source that resolved to nothing is skipped rather than written empty: an\n * inlined empty string is a frontend that builds and then fails at runtime\n * against `http:///`, where a missing variable fails at build with the name of\n * the thing that is missing.\n */\nfunction publicEnv(\n\tplan: Plan,\n\tresolved: Record<string, string>,\n): Record<string, string> {\n\tconst env: Record<string, string> = {};\n\n\tfor (const resource of plan.resources) {\n\t\tfor (const [key, source] of Object.entries(resource.publicEnv ?? {})) {\n\t\t\tconst value = resolved[source];\n\t\t\tif (value) env[key] = value;\n\t\t}\n\t}\n\n\treturn env;\n}\n\n/**\n * What a surface publishes beyond its own address: who may call it, and where\n * a cookie set by it is readable.\n *\n * Both are read off the same list — the constructs that declared an edge to\n * this surface — which is why neither appears in application code. A surface\n * that listed its own callers would be edited every time something new called\n * it, and the thing being edited is already recorded in the graph.\n *\n * An empty origin list is written, and a surface with no address writes nothing\n * at all. The two cases look alike and are not: \"nothing depends on this yet\" is\n * a real state a target should publish, while \"this surface has no address here\"\n * means it is not running in this stage, and keys belong with the address they\n * describe.\n */\nfunction surfaceEnv(\n\tresource: PlannedResource,\n\turl: string | undefined,\n\t/**\n\t * Every construct's *resolved* address, keyed by id — not the raw ones the\n\t * workspace assigned.\n\t *\n\t * The difference is the feature. Behind the edge a surface and its callers\n\t * are `api.shop.localhost` and `console.shop.localhost`, which share a\n\t * parent and therefore a cookie; the addresses they were assigned are\n\t * `localhost:3000` and `localhost:5173`, which are one host with two ports\n\t * and share nothing. Deriving from the wrong one produces a local cookie\n\t * model that is *different* from the deployed one rather than matching it.\n\t */\n\tresolved: Readonly<Record<string, string>> = {},\n): Record<string, string> {\n\tif (!url) return {};\n\n\tconst origins = [\n\t\t...new Set(\n\t\t\t(resource.callers ?? [])\n\t\t\t\t.map((caller) => resolved[caller])\n\t\t\t\t.filter((address): address is string => Boolean(address))\n\t\t\t\t.map(originOf)\n\t\t\t\t.filter((origin): origin is string => Boolean(origin)),\n\t\t),\n\t].sort();\n\n\t// Its own address belongs in the cookie derivation but not in the origin\n\t// list: a surface does not need permission to call itself, and adding it\n\t// would make every surface trust every other one that shares a port.\n\tconst domain = cookieDomain([url, ...origins]);\n\n\treturn {\n\t\t[provideKey(resource.id, 'trustedOrigins')]: origins.join(','),\n\t\t...(domain ? { [provideKey(resource.id, 'cookieDomain')]: domain } : {}),\n\t};\n}\n\n/** An address reduced to the origin a browser compares against. */\nfunction originOf(address: string): string | undefined {\n\ttry {\n\t\treturn new URL(address).origin;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * The runtime role of whatever a derived node hangs off.\n *\n * A reader's parent may itself be a schema tenant, so this follows `of` by one\n * hop rather than assuming the cluster — the reader of a tenant reads that\n * tenant's schema, not the database's.\n */\nfunction parentRole(resource: PlannedResource, plan: Plan): string {\n\tconst parent = plan.resources.find((r) => r.id === resource.of);\n\n\treturn parent ? localRole(parent) : resource.name;\n}\n\n/** Whether a kind connects to Postgres and therefore has an owner role. */\nfunction isDatabase(kind: PlannedResource['kind']): boolean {\n\treturn kind === 'database' || kind === 'database-schema';\n}\n\n/**\n * The URL a migrator connects with — the owner role, which can create, alter\n * and drop.\n *\n * Never in `provides`, which is the whole point. A handler that could reach this\n * could `DROP TABLE`, and the security property the role split exists for is\n * that it cannot: the value is injected by `gkm exec` for the migrate step and\n * by nothing else.\n */\nfunction ownerUrl(\n\tresource: PlannedResource,\n\tplan: Plan,\n\tports: PortAssignments,\n\tproject: string,\n): string | undefined {\n\tif (!resource.container) return undefined;\n\n\tconst port = ports[primaryPortKey(resource.container)];\n\tif (port === undefined) return undefined;\n\n\tconst owner = ownerRole(localRole(resource));\n\n\treturn postgres(port, rootDatabase(resource, plan), {\n\t\tuser: owner,\n\t\tpassword: localRolePassword(project, plan, owner),\n\t});\n}\n\n/**\n * The shared broker keys, when anything is published or consumed.\n *\n * A queue's own key is the producer's. These two are the *connection* itself,\n * which locally is one broker for every queue and topic in the project: the\n * generated pollers open a single connection and subscribe each worker by name\n * on it. Deployed there is no such thing — a Lambda is handed its own event\n * source — so this pair exists for the local target and says so.\n */\nfunction brokerEnv(plan: Plan, ports: PortAssignments): Record<string, string> {\n\tconst carrier = plan.resources.find(\n\t\t(r) => r.kind === 'queue' || r.kind === 'topic',\n\t);\n\tif (!carrier) return {};\n\n\tconst publisher = urlFor(carrier, plan, ports, '');\n\tif (!publisher) return {};\n\n\treturn {\n\t\tEVENT_PUBLISHER_CONNECTION_STRING: publisher,\n\t\t// One connection for both directions everywhere except SNS, where the\n\t\t// thing you publish to and the thing you poll are different services.\n\t\tEVENT_SUBSCRIBER_CONNECTION_STRING:\n\t\t\tplan.events === 'sns' ? publisher.replace(/^sns:/, 'sqs:') : publisher,\n\t};\n}\n\n/**\n * The URL for one planned resource.\n *\n * Every part of it comes from the resource and the port it was published on.\n */\nfunction urlFor(\n\tresource: PlannedResource,\n\tplan: Plan,\n\tports: PortAssignments,\n\tproject: string,\n\taddresses: Readonly<Record<string, string>> = {},\n): string | undefined {\n\t// A secret has no address, so there is no port to wait for.\n\tif (resource.kind === 'secret') return localSecret(project, plan, resource);\n\n\t// A credential resolves to nothing here, deliberately. A secret is derived\n\t// because the platform owns it; a credential was issued by a third party, so\n\t// inventing a value would produce a Stripe key that is not a Stripe key and\n\t// fail at the first call rather than at the first read. It comes from\n\t// `gkm secrets` or `.env` like any other supplied value, and the construct's\n\t// own schema is what reports it missing.\n\tif (resource.kind === 'credential') return undefined;\n\n\t// A surface answers on the app's own port, and a site on its dev server's —\n\t// both assigned by the workspace, neither published by a container.\n\t//\n\t// Behind the edge when there is one, which is what gives them a hostname and\n\t// a certificate rather than a port on localhost. The application cannot tell\n\t// the difference: it reads whichever address was injected and composes none,\n\t// so this is the target's decision alone.\n\tif (resource.kind === 'rest-api' || resource.kind === 'site') {\n\t\tconst address = addresses[resource.id];\n\t\tconst edge = plan.containers.includes('caddy')\n\t\t\t? ports[primaryPortKey('caddy')]\n\t\t\t: undefined;\n\n\t\treturn address && edge !== undefined\n\t\t\t? `https://${hostFor(resource, project)}:${edge}`\n\t\t\t: address;\n\t}\n\n\tif (!resource.container) return undefined;\n\n\tconst port = ports[primaryPortKey(resource.container)];\n\tif (port === undefined) return undefined;\n\n\tswitch (resource.kind) {\n\t\tcase 'database':\n\t\tcase 'database-schema':\n\t\tcase 'database-reader': {\n\t\t\t// A tenant and a reader both live in the *parent's* database; what\n\t\t\t// separates them is the role's grants and the `search_path` pinned on\n\t\t\t// that role, never a database of their own.\n\t\t\tconst database = rootDatabase(resource, plan);\n\n\t\t\t// The runtime role, which can read and write rows and create nothing.\n\t\t\t// `search_path` is pinned on the role by the DDL rather than carried\n\t\t\t// here, so a connection string cannot forget it — and forgetting it\n\t\t\t// looks like an empty database rather than an error.\n\t\t\t//\n\t\t\t// `roles: false` is the documented downgrade: both URLs fall back to\n\t\t\t// the cluster's master credential, and the schema goes back into the\n\t\t\t// URL because there is no role to pin it on.\n\t\t\tif (resource.roles === false) {\n\t\t\t\tconst schema = schemaOf(resource, plan);\n\n\t\t\t\treturn schema\n\t\t\t\t\t? `${postgres(port, database)}?search_path=${schema}`\n\t\t\t\t\t: postgres(port, database);\n\t\t\t}\n\n\t\t\t// A reader connects as the read-only role on its parent, not as a role\n\t\t\t// of its own: read-only is enforced by the grants, which is what makes\n\t\t\t// falling back to the writer's endpoint safe where no replica exists.\n\t\t\tconst role =\n\t\t\t\tresource.kind === 'database-reader'\n\t\t\t\t\t? readerRole(parentRole(resource, plan))\n\t\t\t\t\t: localRole(resource);\n\n\t\t\treturn postgres(port, database, {\n\t\t\t\tuser: role,\n\t\t\t\tpassword: localRolePassword(project, plan, role),\n\t\t\t});\n\t\t}\n\n\t\tcase 'email':\n\t\t\t// The same scheme the deployed target writes; only host and credentials\n\t\t\t// differ, which is what lets the client never branch on provider.\n\t\t\treturn `smtp://${LOCAL_HOST}:${port}`;\n\n\t\tcase 'objects':\n\t\t\t// `?endpoint=` is what points the same S3 client at MinIO — the client\n\t\t\t// is identical, only the URL differs. Path-style addressing goes with\n\t\t\t// it: virtual-host style resolves `bucket.localhost`, which is not\n\t\t\t// MinIO and not anything.\n\t\t\treturn `s3://${resource.name}?region=${LOCAL_REGION}&endpoint=http://${LOCAL_HOST}:${port}&forcePathStyle=true`;\n\n\t\tcase 'file-server': {\n\t\t\t// A host of its own, over TLS — the shape it has deployed, and the one\n\t\t\t// MinIO alone cannot produce: its virtual-host mode reads the leading\n\t\t\t// label *as the bucket name*, so it matches only when the server's id\n\t\t\t// and the bucket's agree and never for a server fronting two buckets.\n\t\t\t// The local edge in front of it does the mapping instead, and issues\n\t\t\t// the certificate. An AWS emulator supplies neither: CloudFront\n\t\t\t// emulation is control plane only, and this is the data plane.\n\t\t\t//\n\t\t\t// The port is in the address because ports are assigned rather than\n\t\t\t// fixed, so two projects can run at once. Nothing that reads a\n\t\t\t// hostname — a cookie domain, a CORS origin — looks at it.\n\t\t\tconst origin = plan.resources.find((r) => r.id === resource.of);\n\t\t\tif (!origin) return undefined;\n\n\t\t\t// With the edge off, the honest local answer is the address that\n\t\t\t// works: a path under the object store. It is not the deployed shape,\n\t\t\t// and the application still cannot tell — it reads the key it was\n\t\t\t// given and never composes one.\n\t\t\treturn plan.containers.includes('caddy')\n\t\t\t\t? `https://${hostFor(resource, project)}:${port}`\n\t\t\t\t: `http://${LOCAL_HOST}:${port}/${origin.name}`;\n\t\t}\n\n\t\tcase 'cache':\n\t\t\t// The scheme is the backend, and the backend is the same one deployed —\n\t\t\t// which is what lets a driver registered at build time match the URL\n\t\t\t// resolved at run time.\n\t\t\t//\n\t\t\t// A cache in a database has no address of its own: no second host and\n\t\t\t// no second credential, just its parent's URL. Reached by following\n\t\t\t// `of` and nothing else — `planFor` has already resolved a\n\t\t\t// `cache: 'db'` backend into that edge, so there is one way a cache\n\t\t\t// lands in a database and one way to find out.\n\t\t\tif (resource.of) {\n\t\t\t\tconst parent = plan.resources.find((r) => r.id === resource.of);\n\t\t\t\tconst url = parent\n\t\t\t\t\t? urlFor(parent, plan, ports, project, addresses)\n\t\t\t\t\t: undefined;\n\n\t\t\t\t// The parent's address plus the one thing that distinguishes this\n\t\t\t\t// cache from another in the same database: its table. Two caches on\n\t\t\t\t// one database resolve the same connection string, so without it a\n\t\t\t\t// client built from the URL cannot tell them apart.\n\t\t\t\treturn url\n\t\t\t\t\t? withTable(url, resource.table ?? cacheTable(resource.id))\n\t\t\t\t\t: undefined;\n\t\t\t}\n\n\t\t\tswitch (plan.cache) {\n\t\t\t\tcase 'elasticache':\n\t\t\t\t\t// The wire protocol, unauthenticated locally. Deployed it is\n\t\t\t\t\t// `rediss://` inside a VPC; the client is the same either way.\n\t\t\t\t\treturn `redis://${LOCAL_HOST}:${port}`;\n\n\t\t\t\tdefault:\n\t\t\t\t\t// The token in the userinfo, because an address and the credential\n\t\t\t\t\t// that opens it are one fact. Deployed the scheme is https and the\n\t\t\t\t\t// host is the provider's; nothing else differs.\n\t\t\t\t\treturn `http://:${LOCAL_TOKEN}@${LOCAL_HOST}:${port}`;\n\t\t\t}\n\n\t\tcase 'queue':\n\t\tcase 'topic':\n\t\t\treturn broker(plan, port);\n\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\n/**\n * The producer's connection string, composed from the backend the plan chose.\n *\n * The protocol is what picks the transport, so a producer never branches: the\n * same `.publish()` reaches pg-boss here and SQS deployed because the string it\n * was handed said so.\n */\nfunction broker(plan: Plan, port: number): string {\n\tswitch (plan.events) {\n\t\tcase 'pgboss': {\n\t\t\t// A schema tenant of the database the app already declared, which is\n\t\t\t// why nothing here invents a Postgres of its own.\n\t\t\tconst database = plan.resources.find((r) => r.kind === 'database');\n\t\t\tif (!database) throw new PgBossNeedsDatabase([]);\n\n\t\t\treturn `pgboss://${LOCAL_USER}:${LOCAL_USER}@${LOCAL_HOST}:${port}/${database.name}?schema=${PGBOSS_SCHEMA}`;\n\t\t}\n\n\t\tcase 'rabbitmq':\n\t\t\t// The exchange is declared by whichever client connects first, so\n\t\t\t// naming it here is the whole of the setup.\n\t\t\treturn `rabbitmq://${LOCAL_USER}:${LOCAL_USER}@${LOCAL_HOST}:${port}?exchange=${RABBITMQ_EXCHANGE}`;\n\n\t\tcase 'sns':\n\t\t\t// An SNS URL carries the ARN of a topic that has to exist first, so\n\t\t\t// this needs a provisioning step LocalStack has not been given yet.\n\t\t\t// Failing here names the gap; composing a URL without the ARN would\n\t\t\t// fail at the first publish instead.\n\t\t\tthrow new UnprovisionedEventsBackend('sns');\n\t}\n}\n\n/** A local Postgres URL, master credential and all. */\nfunction postgres(\n\tport: number,\n\tdatabase: string,\n\tcredential: { user: string; password: string } = {\n\t\tuser: LOCAL_USER,\n\t\tpassword: LOCAL_USER,\n\t},\n): string {\n\treturn `postgres://${credential.user}:${credential.password}@${LOCAL_HOST}:${port}/${database}`;\n}\n\n/**\n * The role a handler connects as, for one database or tenant.\n *\n * The construct's id, lowercased and stage-scoped exactly as its database or\n * schema is — so `Orders` in the `test` stage connects as `orders_test`, and two\n * stages sharing one cluster cannot share a credential. Roles are cluster-scoped\n * in Postgres, which is why the suffix is not optional here the way it is for a\n * schema.\n */\nexport function localRole(resource: PlannedResource): string {\n\treturn resource.name;\n}\n\n/**\n * The password for a derived local role.\n *\n * Derived rather than random, for the same reason the local secret is: a\n * password that changed on every `gkm dev` would lock a developer out of the\n * data they had a moment ago. Seeded by the project so two checkouts do not\n * share one, and by the stage so `test` and `development` do not.\n *\n * It is a *local* credential by construction — the cluster is on loopback and\n * the seed is a checkout path — and no deployed target uses this function.\n */\nexport function localRolePassword(\n\tproject: string,\n\tplan: Plan,\n\trole: string,\n): string {\n\treturn createHash('sha256')\n\t\t.update(`${project}:${plan.stage}:role:${role}`)\n\t\t.digest('base64url')\n\t\t.slice(0, 32);\n}\n\n/**\n * A cache's address: its database's URL, carrying the table it reads.\n *\n * Appended rather than kept beside the URL as a second key, for the reason the\n * design gives everywhere else — an address and what it opens are one fact, and\n * two keys are one more thing to keep in step.\n */\nfunction withTable(url: string, table: string): string {\n\tconst parsed = new URL(url);\n\tparsed.searchParams.set('table', table);\n\n\treturn parsed.toString();\n}\n\n/**\n * The physical database a derived resource lives in.\n *\n * Walks `of` to the top rather than reading the immediate parent, because a\n * reader's parent may itself be a schema tenant — two hops from the database\n * that actually exists.\n */\nexport function rootDatabase(resource: PlannedResource, plan: Plan): string {\n\tconst byId = new Map(plan.resources.map((r) => [r.id, r]));\n\tlet current = resource;\n\n\twhile (current.of) {\n\t\tconst parent = byId.get(current.of);\n\t\tif (!parent) break;\n\t\tcurrent = parent;\n\t}\n\n\treturn current.name;\n}\n\n/** The schema a derived resource puts on its search path, if it has one. */\nfunction schemaOf(resource: PlannedResource, plan: Plan): string | undefined {\n\tconst byId = new Map(plan.resources.map((r) => [r.id, r]));\n\tlet current: PlannedResource | undefined = resource;\n\n\t// A reader has no schema of its own; it reads whatever its parent addresses.\n\twhile (current) {\n\t\tif (current.kind === 'database-schema') return current.schema;\n\t\tcurrent = current.of ? byId.get(current.of) : undefined;\n\t}\n\n\treturn undefined;\n}\n\n/**\n * A backend whose local addresses cannot be derived yet.\n *\n * SNS and SQS are addressed by ARN, which means the topic and queue have to be\n * created in LocalStack before a URL can name them. Until that provisioning\n * lands, saying so is better than handing out a string that fails on first use.\n */\nexport class UnprovisionedEventsBackend extends Error {\n\tconstructor(readonly backend: string) {\n\t\tsuper(\n\t\t\t`The local target cannot derive addresses for '${backend}' yet — its ` +\n\t\t\t\t`topics and queues are named by ARN, which nothing creates locally. ` +\n\t\t\t\t`Use 'pgboss' (the default) or 'rabbitmq' for local development.`,\n\t\t);\n\t\tthis.name = 'UnprovisionedEventsBackend';\n\t}\n}\n\n/**\n * The value a secret resolves to locally.\n *\n * A hash of what identifies it, so it is stable, distinct per project, stage\n * and construct, and never written to disk as a literal. Deployed this is the\n * one kind whose value does *not* come from here — a secret manager generates\n * it once and the target reads it back.\n */\nfunction localSecret(\n\tproject: string,\n\tplan: Plan,\n\tresource: PlannedResource,\n): string {\n\treturn createHash('sha256')\n\t\t.update(`${project}:${plan.stage}:${resource.id}`)\n\t\t.digest('base64url')\n\t\t.slice(0, 43);\n}\n","/**\n * Port allocation — why two projects can run at once.\n *\n * Publishing `5432:5432` means the second project on your machine cannot start.\n * Nothing solves that except giving each project its own ports, and what makes\n * it *possible* is the single-`<NAME>_URL` rule: an app reads `ORDERS_URL` and\n * never sees a host or a port, so reconcile is free to bind whatever is\n * available and compose the URL from what it bound. An app reading `DB_HOST` and\n * `DB_PORT` separately could not be moved this way.\n *\n * Assignments are **persisted, not recomputed**. Recomputing means adding a\n * construct renumbers its neighbours, and every running container, saved\n * connection string, and `psql` history goes stale at once.\n *\n * Pure, with the liveness probe injected — a collision with another project's\n * stack is a fact about the machine, and keeping it a parameter is what lets the\n * rules be asserted without binding a socket.\n */\n\nimport { createHash } from 'node:crypto';\nimport { createServer } from 'node:net';\n\n/** What was assigned, by container name. Persisted between runs. */\nexport type PortAssignments = Readonly<Record<string, number>>;\n\n/**\n * The window ports are drawn from.\n *\n * Above the registered range so nothing here needs privileges, and below the\n * ephemeral range both Linux (32768+) and macOS (49152+) allocate from — a port\n * the kernel might hand to an outgoing connection is one that is free when\n * checked and taken by the time it is bound.\n */\nconst WINDOW = { min: 20000, max: 32000 } as const;\n\n/**\n * How far apart two projects start.\n *\n * Wide enough that a project can add containers without walking into its\n * neighbour's block, narrow enough that the window holds many projects.\n */\nconst BLOCK = 50;\n\n/**\n * Where a project's ports start.\n *\n * Derived from the name rather than assigned sequentially so that a fresh clone\n * of the same project on the same machine lands on the same block — the first\n * run has nothing persisted to read, and hashing is what keeps that first run\n * from colliding with whatever else is already allocated.\n *\n * @example startingPort('toolbox') // stable for a given name\n */\nexport function startingPort(project: string): number {\n\tconst digest = createHash('sha256').update(project).digest();\n\tconst blocks = Math.floor((WINDOW.max - WINDOW.min) / BLOCK);\n\n\treturn WINDOW.min + (digest.readUInt32BE(0) % blocks) * BLOCK;\n}\n\n/**\n * Whether a port can be bound right now.\n *\n * Async because the only way to answer it is to try binding, and\n * `server.listen` is asynchronous — there is no synchronous form to wrap. A\n * probe that pretended otherwise would have to lie.\n */\nexport type PortProbe = (port: number) => Promise<boolean>;\n\n/**\n * Assign a port to every container that has none, keeping the ones that do.\n *\n * Existing assignments are returned untouched even when the container is no\n * longer in the plan: removing a construct must not renumber what is left, and\n * keeping the entry means re-adding it later restores the same port rather than\n * inventing a new one.\n *\n * @param probe - reports what is actually listening. A port assigned to this\n * project is skipped without probing, because on a converged reconcile our own\n * container is the thing listening on it.\n * @throws {NoPortAvailable} when the window is exhausted.\n */\nexport async function allocate(\n\tproject: string,\n\tcontainers: readonly string[],\n\texisting: PortAssignments = {},\n\tprobe: PortProbe = isPortFree,\n): Promise<PortAssignments> {\n\tconst assignments: Record<string, number> = { ...existing };\n\tconst taken = new Set(Object.values(assignments));\n\n\t// Sorted so the order containers happen to appear in the plan cannot change\n\t// which port each one gets.\n\tfor (const container of [...containers].sort()) {\n\t\tif (assignments[container] !== undefined) continue;\n\n\t\tconst port = await nextFree(startingPort(project), taken, probe);\n\t\tassignments[container] = port;\n\t\ttaken.add(port);\n\t}\n\n\treturn assignments;\n}\n\n/** The window was exhausted. */\nexport class NoPortAvailable extends Error {\n\t/** Where the search started. */\n\treadonly from: number;\n\t/** The window searched, so the caller can report what was full. */\n\treadonly window: { min: number; max: number } = WINDOW;\n\n\tconstructor(from: number) {\n\t\tsuper('No free port is available in the local port range');\n\t\tthis.name = 'NoPortAvailable';\n\t\tthis.from = from;\n\t}\n}\n\n/**\n * The first port at or after `from` that is neither ours nor in use.\n *\n * Wraps at the top of the window rather than stopping, so a project whose block\n * sits near the end is not the one that fails.\n */\nasync function nextFree(\n\tfrom: number,\n\ttaken: ReadonlySet<number>,\n\tprobe: PortProbe,\n): Promise<number> {\n\tconst span = WINDOW.max - WINDOW.min;\n\n\tfor (let offset = 0; offset < span; offset++) {\n\t\tconst port = WINDOW.min + ((from - WINDOW.min + offset) % span);\n\t\tif (taken.has(port)) continue;\n\t\tif (await probe(port)) return port;\n\t}\n\n\tthrow new NoPortAvailable(from);\n}\n\n/**\n * Whether nothing is listening on a port.\n *\n * Binds rather than connects: a refused connection says nothing about whether\n * the port can be *published*, which is the question Docker will ask. Binds on\n * `0.0.0.0` for the same reason — a port free on loopback can still be taken on\n * the address Docker publishes to.\n *\n * Inherently racy, and that is acceptable: Docker failing to bind stays the real\n * check. This exists so the common case surfaces as a message at reconcile\n * rather than as a connection failure ten minutes later.\n */\nexport function isPortFree(port: number): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst server = createServer();\n\n\t\tserver.once('error', () => resolve(false));\n\t\tserver.once('listening', () => server.close(() => resolve(true)));\n\t\tserver.listen(port, '0.0.0.0');\n\t});\n}\n","/**\n * Creating what the URLs name.\n *\n * Convergent and idempotent: every statement asks whether the thing exists\n * first, so reconciling repeatedly is free and a half-applied run recovers by\n * being run again. That property is what lets `gkm dev` do this on every start.\n *\n * The line the design draws is at data, not at side effects — this creates\n * databases, schemas, and buckets, and it never seeds, resets, or drops. A\n * developer who wants their data gone asks for it explicitly.\n *\n * Statement generation is separated from application for the reason the design\n * gives: the same DDL has to run in-process locally and from a provisioner in a\n * VPC, and one implementation is what stops \"works locally, fails deployed\".\n * When the role DDL lands in `@geekmidas/db` this delegates to it rather than\n * growing its own.\n */\n\nimport { cacheTableStatements } from '@geekmidas/cache/postgres';\nimport { ownerRole, readerRole, roleStatements } from '@geekmidas/db/pg/roles';\nimport { cacheTable } from '@geekmidas/manifest';\nimport { localRole, localRolePassword, rootDatabase } from './env';\nimport type { Plan, PlannedResource } from './plan';\n\n/** One thing to create, and how to tell whether it already exists. */\nexport interface Statement {\n\t/** The construct this is for, so a failure can name it. */\n\tid: string;\n\t/** What it does, in the line reconcile prints. */\n\tdescribe: string;\n\t/** The database to connect to. Absent means the cluster's default. */\n\tdatabase?: string;\n\t/**\n\t * Answers \"does this already exist\", parameterised.\n\t *\n\t * Absent for a statement that is idempotent in itself — a `GRANT` re-granting\n\t * what is already granted is a no-op, and checking would cost a round trip to\n\t * learn nothing. Those run every time and report unchanged.\n\t */\n\texists?: { sql: string; values: unknown[] };\n\t/**\n\t * The DDL, run only when `exists` returns nothing.\n\t *\n\t * Not parameterised, because identifiers cannot be — which is why\n\t * {@link quoteIdentifier} exists and why names are canonicalised long before\n\t * they reach here.\n\t */\n\tcreate: string;\n}\n\n/**\n * The Postgres work a plan implies.\n *\n * Databases before the schemas inside them, which `provisionOrder` already\n * guarantees by putting parents first.\n */\nexport function postgresStatements(\n\tplan: Plan,\n\t/**\n\t * Seeds the derived role passwords. The same project seeds the same\n\t * passwords, so a restart does not lock a developer out of their own data —\n\t * and two checkouts do not share a credential.\n\t */\n\tproject = '',\n): Statement[] {\n\tconst statements: Statement[] = [];\n\n\tfor (const resource of plan.resources) {\n\t\tif (!resource.provisions || resource.container !== 'postgres') continue;\n\n\t\tif (resource.kind === 'database') {\n\t\t\tstatements.push({\n\t\t\t\tid: resource.id,\n\t\t\t\tdescribe: `database ${resource.name}`,\n\t\t\t\texists: {\n\t\t\t\t\tsql: 'SELECT 1 FROM pg_database WHERE datname = $1',\n\t\t\t\t\tvalues: [resource.name],\n\t\t\t\t},\n\t\t\t\t// `CREATE DATABASE` cannot run inside a transaction, which is why\n\t\t\t\t// each statement is applied on its own rather than batched.\n\t\t\t\tcreate: `CREATE DATABASE ${quoteIdentifier(resource.name)}`,\n\t\t\t});\n\n\t\t\t// After the database, never before: roles are cluster-scoped but their\n\t\t\t// grants are not, so every statement after the first two has to run\n\t\t\t// against a database that exists.\n\t\t\tif (resource.schema) {\n\t\t\t\tstatements.push(...rolesFor(resource, resource.schema, plan, project));\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (resource.kind === 'database-schema' && resource.schema) {\n\t\t\t// The schema *and* the roles that reach it, from one generator shared\n\t\t\t// with the deploy target — the same DDL has to run in-process here and\n\t\t\t// from a provisioner in a VPC, and one implementation is what stops\n\t\t\t// \"works locally, fails deployed\".\n\t\t\tstatements.push(...rolesFor(resource, resource.schema, plan, project));\n\t\t}\n\n\t\t// A cache that lives in a database is a table in it, and a table is DDL —\n\t\t// so the owner creates it, not the handler's role, which may not create\n\t\t// anything. The same reason the driver does not do it lazily.\n\t\t//\n\t\t// `of` is the whole test: `planFor` resolves a `cache: 'db'` backend into\n\t\t// that edge before anything reads the plan, so a cache lands in a\n\t\t// database exactly one way and this does not have to know the config\n\t\t// existed.\n\t\tconst cacheHome =\n\t\t\tresource.kind === 'cache' && resource.of\n\t\t\t\t? plan.resources.find((r) => r.id === resource.of)\n\t\t\t\t: undefined;\n\n\t\tif (cacheHome) {\n\t\t\tstatements.push(...cacheTableDdl(resource, cacheHome, plan));\n\t\t}\n\n\t\t// A reader provisions nothing: it is a set of grants on an endpoint that\n\t\t// already exists, and falling back to the writer where no replica exists\n\t\t// stays safe because read-only is enforced by the role.\n\t}\n\n\treturn statements;\n}\n\n/**\n * A cache's table, in the schema the role that reads it resolves names in.\n *\n * Two things have to line up, and neither is the table's contents. The driver\n * names the table unqualified and lets the connection's `search_path` place it\n * — which the role carries — so creating it from the master connection, whose\n * path is `public`, leaves a table the application cannot see and a\n * `relation \"cache\" does not exist` on the first request. And the default\n * privileges that make a tenant's tables reachable are granted *for the owner\n * role*, so nothing covers a table the master created.\n *\n * Hence: qualified with the tenant's schema, handed to its owner, and granted\n * explicitly — `roleStatements` grants what exists at the time it runs, and\n * this table does not exist yet.\n *\n * A database that opted out of roles needs none of it: everything connects as\n * the master, whose `search_path` finds `public`, and the table it creates\n * there is the table it reads.\n */\nfunction cacheTableDdl(\n\tcache: PlannedResource,\n\thome: PlannedResource,\n\tplan: Plan,\n): Statement[] {\n\tconst database = rootDatabase(home, plan);\n\tconst schema = home.roles === false ? undefined : home.schema;\n\t// The same default the URL carries — `cacheTable` is read by both, so the\n\t// table a client reads and the table a target creates cannot drift.\n\tconst name = cache.table ?? cacheTable(cache.id);\n\tconst table = schema ? `${schema}.${name}` : name;\n\n\tconst created = cacheTableStatements({ table }).map((statement) => ({\n\t\tid: cache.id,\n\t\tdescribe: statement.describe,\n\t\tdatabase,\n\t\t...(statement.exists ? { exists: statement.exists } : {}),\n\t\tcreate: statement.sql,\n\t}));\n\n\tif (!schema) return created;\n\n\tconst runtime = localRole(home);\n\tconst owner = ownerRole(runtime);\n\tconst qualified = table\n\t\t.split('.')\n\t\t.map((part) => quoteIdentifier(part))\n\t\t.join('.');\n\n\treturn [\n\t\t...created,\n\t\t{\n\t\t\tid: cache.id,\n\t\t\tdescribe: `cache table ${table} is owned by ${owner}`,\n\t\t\tdatabase,\n\t\t\tcreate: `ALTER TABLE ${qualified} OWNER TO ${quoteIdentifier(owner)}`,\n\t\t},\n\t\t{\n\t\t\tid: cache.id,\n\t\t\tdescribe: `${runtime} may read and write ${table}`,\n\t\t\tdatabase,\n\t\t\tcreate:\n\t\t\t\t`GRANT SELECT, INSERT, UPDATE, DELETE ON ${qualified} ` +\n\t\t\t\t`TO ${quoteIdentifier(runtime)}`,\n\t\t},\n\t];\n}\n\n/**\n * The roles and schema for one database or tenant.\n *\n * Skipped entirely when the construct declared `roles: false` — the documented\n * downgrade, where both URLs fall back to the cluster's master credential. It\n * is a choice someone made rather than a default they got.\n */\nfunction rolesFor(\n\tresource: PlannedResource,\n\tschema: string,\n\tplan: Plan,\n\tproject: string,\n): Statement[] {\n\tif (resource.roles === false) return [];\n\n\tconst runtime = localRole(resource);\n\tconst owner = ownerRole(runtime);\n\n\t// A reader role only where something reads through one. Creating roles\n\t// nothing connects as is noise in `\\du` and one more thing to explain.\n\tconst reader = plan.resources.some(\n\t\t(r) => r.kind === 'database-reader' && r.of === resource.id,\n\t)\n\t\t? readerRole(runtime)\n\t\t: undefined;\n\n\treturn roleStatements({\n\t\truntime,\n\t\towner,\n\t\t...(reader ? { reader } : {}),\n\t\tschema,\n\t\tpasswords: {\n\t\t\truntime: localRolePassword(project, plan, runtime),\n\t\t\towner: localRolePassword(project, plan, owner),\n\t\t\t...(reader ? { reader: localRolePassword(project, plan, reader) } : {}),\n\t\t},\n\t}).map((statement) => ({\n\t\tid: resource.id,\n\t\tdescribe: statement.describe,\n\t\t// Inside the parent's database — a tenant is a schema, never a database\n\t\t// of its own. Roles are cluster-scoped, but the grants are not, so all of\n\t\t// it has to run against the database the objects live in.\n\t\tdatabase: rootDatabase(resource, plan),\n\t\t...(statement.exists ? { exists: statement.exists } : {}),\n\t\tcreate: statement.sql,\n\t}));\n}\n\n/** The buckets a plan implies. */\nexport function bucketNames(plan: Plan): string[] {\n\treturn plan.resources\n\t\t.filter((r) => r.provisions && r.kind === 'objects')\n\t\t.map((r) => r.name);\n}\n\n/**\n * Quote an identifier for use in DDL.\n *\n * Names reaching here are already canonical and stage-suffixed, so this is\n * defence rather than sanitisation — but DDL cannot be parameterised, and an\n * unquoted identifier is the one place a name could ever be more than a name.\n */\nexport function quoteIdentifier(name: string): string {\n\treturn `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/** What applying a statement did. */\nexport interface Applied {\n\tid: string;\n\tdescribe: string;\n\t/** False when it already existed — the converged case. */\n\tcreated: boolean;\n}\n\n/** The database operations the applier needs, so it can be run against a fake. */\nexport interface SqlClient {\n\t/** Run a query against one database, returning its rows. */\n\tquery(\n\t\tdatabase: string | undefined,\n\t\tsql: string,\n\t\tvalues?: unknown[],\n\t): Promise<unknown[]>;\n}\n\n/**\n * Apply the plan's Postgres statements.\n *\n * Each is checked before it is run, so this converges rather than erroring on a\n * second pass.\n */\nexport async function applyPostgres(\n\tclient: SqlClient,\n\tstatements: readonly Statement[],\n): Promise<Applied[]> {\n\tconst applied: Applied[] = [];\n\n\tfor (const statement of statements) {\n\t\t// No check means the statement is idempotent in itself — a GRANT\n\t\t// re-granting what is already granted is a no-op — so it runs and\n\t\t// reports unchanged, and a converged reconcile still says converged.\n\t\tif (!statement.exists) {\n\t\t\tawait client.query(statement.database, statement.create);\n\t\t\tapplied.push({\n\t\t\t\tid: statement.id,\n\t\t\t\tdescribe: statement.describe,\n\t\t\t\tcreated: false,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst rows = await client.query(\n\t\t\tstatement.database,\n\t\t\tstatement.exists.sql,\n\t\t\tstatement.exists.values,\n\t\t);\n\n\t\tif (rows.length > 0) {\n\t\t\tapplied.push({\n\t\t\t\tid: statement.id,\n\t\t\t\tdescribe: statement.describe,\n\t\t\t\tcreated: false,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tawait client.query(statement.database, statement.create);\n\t\tapplied.push({\n\t\t\tid: statement.id,\n\t\t\tdescribe: statement.describe,\n\t\t\tcreated: true,\n\t\t});\n\t}\n\n\treturn applied;\n}\n\n/**\n * What each bucket's open paths are, if anything serves it.\n *\n * Keyed by the *origin*, because that is where the policy lives: two servers\n * over one bucket contribute to one policy, so the patterns are unioned rather\n * than the last one winning.\n */\nexport function bucketPolicies(\n\tplan: Plan,\n): { bucket: string; open: string[] }[] {\n\tconst byBucket = new Map<string, Set<string>>();\n\n\tfor (const resource of plan.resources) {\n\t\tif (resource.kind !== 'file-server' || !resource.open?.length) continue;\n\n\t\tconst origin = plan.resources.find((r) => r.id === resource.of);\n\t\tif (!origin) continue;\n\n\t\tconst patterns = byBucket.get(origin.name) ?? new Set<string>();\n\t\tfor (const pattern of resource.open) patterns.add(pattern);\n\t\tbyBucket.set(origin.name, patterns);\n\t}\n\n\treturn [...byBucket]\n\t\t.map(([bucket, open]) => ({ bucket, open: [...open].sort() }))\n\t\t.sort((a, b) => a.bucket.localeCompare(b.bucket));\n}\n\n/**\n * One bucket's open paths as an S3 bucket policy.\n *\n * Anonymous `s3:GetObject` on the named prefixes and nothing else — which is\n * what \"open\" means everywhere: the server serves that path without a\n * signature, and the bucket stays private for everything else.\n *\n * One fidelity note, stated rather than discovered: an S3 policy resource's `*`\n * crosses `/`, so a single-star pattern is *wider* here than the construct's own\n * runtime check, which stops at a segment boundary. The construct is the\n * stricter of the two, so a key it refuses to serve unsigned is never one this\n * policy was relied on to refuse — but a key fetched directly, bypassing the\n * client, can be admitted by the policy where the client would have said no.\n * Prefer `**` where crossing segments is what you meant.\n */\nexport function bucketPolicy(bucket: string, open: readonly string[]): string {\n\treturn JSON.stringify({\n\t\tVersion: '2012-10-17',\n\t\tStatement: [\n\t\t\t{\n\t\t\t\tSid: 'GkmOpenPaths',\n\t\t\t\tEffect: 'Allow',\n\t\t\t\tPrincipal: { AWS: ['*'] },\n\t\t\t\tAction: ['s3:GetObject'],\n\t\t\t\tResource: open.map(\n\t\t\t\t\t(pattern) =>\n\t\t\t\t\t\t`arn:aws:s3:::${bucket}/${pattern.replace(/\\*\\*/g, '*')}`,\n\t\t\t\t),\n\t\t\t},\n\t\t],\n\t});\n}\n\n/** The object-storage operations the applier needs. */\nexport interface BucketClient {\n\texists(bucket: string): Promise<boolean>;\n\tcreate(bucket: string): Promise<void>;\n\t/** The bucket's current policy, or `undefined` where it has none. */\n\tpolicy(bucket: string): Promise<string | undefined>;\n\tsetPolicy(bucket: string, policy: string): Promise<void>;\n}\n\n/**\n * Apply each served bucket's open-path policy, skipping those already correct.\n *\n * Compared as parsed JSON rather than as text: the same policy re-serialised\n * with different key order is the same policy, and rewriting it every reconcile\n * would report a change on every start.\n */\nexport async function applyPolicies(\n\tclient: BucketClient,\n\tpolicies: readonly { bucket: string; open: string[] }[],\n): Promise<Applied[]> {\n\tconst applied: Applied[] = [];\n\n\tfor (const { bucket, open } of policies) {\n\t\tconst wanted = bucketPolicy(bucket, open);\n\t\tconst describe = `open paths on ${bucket}`;\n\n\t\tif (equivalent(await client.policy(bucket), wanted)) {\n\t\t\tapplied.push({ id: `${bucket}:policy`, describe, created: false });\n\t\t\tcontinue;\n\t\t}\n\n\t\tawait client.setPolicy(bucket, wanted);\n\t\tapplied.push({ id: `${bucket}:policy`, describe, created: true });\n\t}\n\n\treturn applied;\n}\n\n/** Whether two policy documents say the same thing. */\nfunction equivalent(current: string | undefined, wanted: string): boolean {\n\tif (!current) return false;\n\n\ttry {\n\t\treturn (\n\t\t\tJSON.stringify(JSON.parse(current)) === JSON.stringify(JSON.parse(wanted))\n\t\t);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Create every bucket the plan names, skipping those that exist. */\nexport async function applyBuckets(\n\tclient: BucketClient,\n\tbuckets: readonly string[],\n): Promise<Applied[]> {\n\tconst applied: Applied[] = [];\n\n\tfor (const bucket of buckets) {\n\t\tif (await client.exists(bucket)) {\n\t\t\tapplied.push({\n\t\t\t\tid: bucket,\n\t\t\t\tdescribe: `bucket ${bucket}`,\n\t\t\t\tcreated: false,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tawait client.create(bucket);\n\t\tapplied.push({ id: bucket, describe: `bucket ${bucket}`, created: true });\n\t}\n\n\treturn applied;\n}\n\n/** Everything a plan implies, for a caller that wants to show it. */\nexport function summarise(plan: Plan): string[] {\n\treturn [\n\t\t...postgresStatements(plan).map((s) => s.describe),\n\t\t...bucketNames(plan).map((b) => `bucket ${b}`),\n\t\t...bucketPolicies(plan).map((p) => `open paths on ${p.bucket}`),\n\t];\n}\n\n/** Resources the plan resolves a URL for but creates nothing for. */\nexport function unprovisioned(plan: Plan): PlannedResource[] {\n\treturn plan.resources.filter((r) => !r.provisions);\n}\n","/**\n * What reconcile records, and the hash that lets it do nothing.\n *\n * Reconciling on every `gkm dev` is only acceptable if the converged case is\n * free, so the desired plan is hashed: matching hash plus healthy containers\n * means there is nothing to do. Without that, the automatic reconcile the design\n * asks for would tax every start.\n *\n * Almost nothing needs recording — roles are queried, migrations have their own\n * table, containers are remembered by Docker. Only ports and this hash have no\n * other owner.\n */\n\nimport { createHash } from 'node:crypto';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport type { ComposeFile } from './compose';\nimport type { Plan } from './plan';\n\n/** What was true at the end of the last successful reconcile. */\nexport interface ReconcileState {\n\t/** The hash of the plan that was applied. */\n\thash: string;\n\t/** The stage it was applied for, so a mismatched file is ignored. */\n\tstage: string;\n}\n\nconst STATE_DIR = '.gkm/reconcile';\n\n/**\n * The hash of everything a reconcile would act on.\n *\n * Covers the compose document rather than the plan alone, because the plan is\n * unchanged when only an image pin moves — and a changed image is exactly a\n * container that must be recreated. `stringify` is stable here because\n * `composeFor` sorts its containers, which is what makes the hash meaningful.\n *\n * Deliberately a plain function over serialisable input: the deployed target\n * keys its provisioner invocation on the same idea, and the two must not be able\n * to disagree about what counts as a change.\n */\nexport function planHash(\n\tplan: Plan,\n\tcompose: ComposeFile,\n\t/**\n\t * Anything else generated from the plan that a container reads.\n\t *\n\t * The edge's Caddyfile is the case: a file server added or a bucket renamed\n\t * changes the routing without changing a single container, so a hash over\n\t * the compose document alone would report convergence and leave the old\n\t * routes serving.\n\t */\n\tgenerated: Readonly<Record<string, string>> = {},\n): string {\n\treturn createHash('sha256')\n\t\t.update(JSON.stringify({ stage: plan.stage, plan, compose, generated }))\n\t\t.digest('hex');\n}\n\n/** Read the recorded state, or nothing if this stage has never reconciled. */\nexport async function loadState(\n\troot: string,\n\tstage: string,\n): Promise<ReconcileState | undefined> {\n\ttry {\n\t\tconst raw = await readFile(statePath(root, stage), 'utf-8');\n\t\tconst state = JSON.parse(raw) as ReconcileState;\n\n\t\t// A file written for another stage is not this stage's state. Treating it\n\t\t// as one is how `gkm test` would skip work `gkm dev` did.\n\t\treturn state.stage === stage ? state : undefined;\n\t} catch {\n\t\t// Unreadable, absent, or corrupt all mean the same thing: reconcile. The\n\t\t// loop is convergent, so the cost of being wrong here is one slow start.\n\t\treturn undefined;\n\t}\n}\n\n/** Record what was applied, so the next run can skip it. */\nexport async function saveState(\n\troot: string,\n\tstate: ReconcileState,\n): Promise<void> {\n\tconst path = statePath(root, state.stage);\n\n\tawait mkdir(dirname(path), { recursive: true });\n\tawait writeFile(path, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nfunction statePath(root: string, stage: string): string {\n\treturn join(root, STATE_DIR, `${stage}.json`);\n}\n","/**\n * Reconcile — the one convergent function `gkm dev`, `gkm test`, and `gkm setup`\n * all call.\n *\n * Three phases, and only this one has side effects: declaring is pure (`gkm\n * build` and the dev watcher), running reads `<NAME>_URL` and provisions\n * nothing. Reconcile computes the desired state, compares, and applies the\n * difference — which is what makes \"when does it run\" answerable with\n * *whenever, repeatedly*.\n *\n * It is safe to run automatically because its blast radius is entirely local:\n * this project's containers, this project's `.gkm/`. Nothing it does can reach a\n * cloud. The line is drawn at data, not at side effects — it allocates ports,\n * writes compose, starts containers, and creates roles; it never seeds, resets,\n * or drops.\n *\n * Docker is injected rather than imported so the rules above can be asserted\n * without a daemon. The default implementation is the real one.\n */\n\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { type ConstructManifest, provisionOrder } from '@geekmidas/manifest';\nimport type { CacheBackend, EventsBackend } from '../types';\nimport { caddyfileRoot, sitesFor, toCaddyfile } from './caddyfile';\nimport { bucketClient, pgClient } from './clients';\nimport { type ComposeFile, composeFor, toYaml } from './compose';\nimport { portKeys, portsOf, primaryPortKey } from './containers';\nimport { dockerCli } from './docker';\nimport { envFor } from './env';\nimport { type Plan, planFor } from './plan';\nimport {\n\tallocate,\n\tisPortFree,\n\ttype PortAssignments,\n\ttype PortProbe,\n} from './ports';\nimport {\n\ttype Applied,\n\tapplyBuckets,\n\tapplyPolicies,\n\tapplyPostgres,\n\ttype BucketClient,\n\tbucketNames,\n\tbucketPolicies,\n\tpostgresStatements,\n\ttype SqlClient,\n} from './provision';\nimport { loadState, planHash, saveState } from './state';\n\nexport type { ComposeFile } from './compose';\nexport type { Plan, PlannedResource } from './plan';\nexport type { PortAssignments } from './ports';\n\n/** Where the generated compose file is written, relative to the project root. */\nexport const COMPOSE_PATH = '.gkm/docker-compose.yml';\n\n/**\n * The local edge's config, beside the compose file that mounts it.\n *\n * Relative in the compose volume, so it resolves against `.gkm/` wherever the\n * project lives.\n */\nexport const CADDYFILE_PATH = '.gkm/Caddyfile';\n\n/**\n * Where one stage's routes live, imported by the file above.\n *\n * Per stage because one edge serves every stage, the same way one Postgres\n * holds `orders` and `orders_test`. A single file would mean `gkm test`\n * deleting the routes `gkm dev` is serving.\n */\nexport const caddySitesPath = (stage: string) =>\n\t`.gkm/caddy-sites/${stage}.caddy`;\n\n/**\n * Where the local edge's root certificate is copied to.\n *\n * Exported so a process can be pointed at it without installing anything:\n * `NODE_EXTRA_CA_CERTS` is the whole of the trust story for Node, and it needs\n * no sudo. A browser is the case that still wants the root in the system store,\n * which is a one-time `caddy trust` rather than something a reconcile should do\n * on a developer's behalf.\n */\nexport const LOCAL_CA_PATH = '.gkm/caddy-root.crt';\n\n/** Where Caddy keeps the root of the CA it generated. */\nconst CADDY_ROOT_IN_CONTAINER = '/data/caddy/pki/authorities/local/root.crt';\n\n/**\n * The Docker operations reconcile needs.\n *\n * Small on purpose: everything else about containers Docker already remembers,\n * so there is no state here to keep in step with it.\n */\nexport interface Docker {\n\t/**\n\t * The host port a running container publishes for one of its internal ports,\n\t * or `undefined` if it is not running.\n\t *\n\t * Consulted before the saved assignments so that reconcile converges against\n\t * what is actually running rather than only against its own file — a\n\t * container someone started by hand is still the container serving the app.\n\t */\n\tpublishedPort(\n\t\tcomposePath: string,\n\t\tservice: string,\n\t\tinside: number,\n\t): Promise<number | undefined>;\n\t/** Bring the named services up, detached. */\n\tup(composePath: string, services: readonly string[]): Promise<void>;\n\t/** Whether every named service is running and passing its health check. */\n\thealthy(composePath: string, services: readonly string[]): Promise<boolean>;\n\t/**\n\t * Copy a file out of a running container.\n\t *\n\t * One case: the certificate authority the local edge generates. It lives in\n\t * the container's volume, and everything that has to *trust* it — Node, the\n\t * test suite, curl — lives outside.\n\t */\n\tcopyOut(\n\t\tcomposePath: string,\n\t\tservice: string,\n\t\tfrom: string,\n\t\tto: string,\n\t): Promise<void>;\n\t/**\n\t * Ask a running service to re-read its configuration.\n\t *\n\t * The local edge is the case: its routes are a mounted file, and a container\n\t * that is already up will not notice one changing.\n\t */\n\treload(composePath: string, service: string): Promise<void>;\n}\n\nexport interface ReconcileOptions {\n\t/** The project root — where `.gkm/` lives. */\n\troot: string;\n\t/**\n\t * The project name, which seeds port allocation and names the compose\n\t * project. Two checkouts of the same repo share it deliberately.\n\t */\n\tproject: string;\n\t/** The construct manifest — what the app declared. */\n\tmanifest: ConstructManifest;\n\t/** The stage being reconciled: `development` for dev, `test` for test. */\n\tstage: string;\n\t/** The events backend, until `topic` and `queue` are kinds. */\n\tevents?: EventsBackend;\n\t/** Where a declared cache lives — see {@link CacheBackend}. */\n\tcache?: CacheBackend;\n\t/** Containers no construct implies — the config exceptions. */\n\textraContainers?: readonly string[];\n\t/** Whether the local edge fronts surfaces, sites and file servers. */\n\tedge?: boolean;\n\t/** Per-container image pins. */\n\timages?: Readonly<Record<string, string>>;\n\t/** Ports assigned by previous runs, from `.gkm/ports.json`. */\n\tsaved?: PortAssignments;\n\t/** Start containers and wait for health. Off answers \"what would change\". */\n\tstart?: boolean;\n\t/**\n\t * Create the databases, schemas, and buckets the plan names.\n\t *\n\t * Requires the containers to be running, so it is skipped when `start` is\n\t * off. Never seeds, resets, or drops — the line is at data, not side effects.\n\t */\n\tprovision?: boolean;\n\t/** The address mail is sent from locally. */\n\tmailFrom?: string;\n\t/**\n\t * Where each address-owning construct answers, keyed by id.\n\t *\n\t * Surfaces and sites. Assigned by whatever starts them, which is why it\n\t * arrives here rather than being read off a published container port.\n\t */\n\taddresses?: Readonly<Record<string, string>>;\n\tdocker?: Docker;\n\tprobe?: PortProbe;\n\t/** Injected for tests; the defaults talk to the containers just started. */\n\tsql?: (port: number) => SqlClient;\n\tbuckets?: (port: number) => BucketClient;\n}\n\nexport interface ReconcileResult {\n\tstage: string;\n\tplan: Plan;\n\tcompose: ComposeFile;\n\t/** Every assigned port, keyed by port key. Persist this. */\n\tports: PortAssignments;\n\t/** The address of each container, keyed by container. */\n\taddresses: Readonly<Record<string, string>>;\n\t/** The `<NAME>_URL` values this stage resolves. */\n\tenv: Readonly<Record<string, string>>;\n\t/** What the applier created, or found already there. */\n\tprovisioned: Applied[];\n\t/** The hash recorded for this state. */\n\thash: string;\n\t/**\n\t * Whether anything was applied. False is the fast path — the recorded hash\n\t * matched and the containers were healthy, so nothing was written or started.\n\t */\n\tchanged: boolean;\n}\n\n/**\n * Converge the local target on what the manifest declares.\n *\n * Cheap when there is nothing to do: the hash is compared before the compose\n * file is written or a container is touched, and a converged project pays one\n * hash and one health check.\n */\nexport async function reconcile(\n\toptions: ReconcileOptions,\n): Promise<ReconcileResult> {\n\tconst {\n\t\troot,\n\t\tproject,\n\t\tmanifest,\n\t\tstage,\n\t\tstart = true,\n\t\tprovision = true,\n\t\tdocker = dockerCli,\n\t\tprobe = isPortFree,\n\t\tsql = pgClient,\n\t\tbuckets = bucketClient,\n\t} = options;\n\n\tconst plan = planFor(manifest, stage, provisionOrder(manifest), {\n\t\tevents: options.events,\n\t\tcache: options.cache,\n\t\textraContainers: options.extraContainers,\n\t\t...(options.edge === undefined ? {} : { edge: options.edge }),\n\t});\n\n\tconst composePath = join(root, COMPOSE_PATH);\n\n\t// What is already running wins over what was recorded: a container on a port\n\t// is the fact, and the file is only a memory of one.\n\tconst observed = await observedPorts(docker, composePath, plan.containers);\n\tconst ports = await allocate(\n\t\tproject,\n\t\tportKeys(plan.containers),\n\t\t{ ...options.saved, ...observed },\n\t\tprobe,\n\t);\n\n\tconst compose = composeFor(plan, {\n\t\tproject,\n\t\tports,\n\t\t...(options.images ? { images: options.images } : {}),\n\t});\n\n\t// The edge's config is part of what \"converged\" means: a file server added or\n\t// a bucket renamed changes the routing without changing a container, and a\n\t// hash that ignored it would leave the old routes in place.\n\tconst caddyfile = toCaddyfile(sitesFor(plan, project, options.addresses));\n\tconst hash = planHash(plan, compose, { caddyfile });\n\tconst addresses = addressesFor(plan.containers, ports);\n\tconst env = envFor(plan, {\n\t\tports,\n\t\tproject,\n\t\t...(options.mailFrom ? { mailFrom: options.mailFrom } : {}),\n\t\t...(options.addresses ? { addresses: options.addresses } : {}),\n\t});\n\t// Pointed at whether or not it exists yet: the copy below fills it in, and\n\t// anything that reads the environment starts after this returns.\n\tif (plan.containers.includes('caddy')) {\n\t\tenv.NODE_EXTRA_CA_CERTS = join(root, LOCAL_CA_PATH);\n\t}\n\n\tconst result: ReconcileResult = {\n\t\tstage,\n\t\tplan,\n\t\tcompose,\n\t\tports,\n\t\taddresses,\n\t\tenv,\n\t\tprovisioned: [],\n\t\thash,\n\t\tchanged: false,\n\t};\n\n\tconst recorded = await loadState(root, stage);\n\tconst converged =\n\t\trecorded?.hash === hash &&\n\t\t(!start || (await docker.healthy(composePath, plan.containers)));\n\n\t// The fast path, and the reason reconciling on every start is acceptable.\n\tif (converged) return result;\n\n\tawait write(composePath, toYaml(compose));\n\n\t// Before the containers: Caddy reads these at startup, and mounting a file\n\t// that does not exist yet gets a directory instead.\n\tif (plan.containers.includes('caddy')) {\n\t\tawait write(join(root, CADDYFILE_PATH), caddyfileRoot());\n\t\tawait write(join(root, caddySitesPath(stage)), caddyfile);\n\t}\n\n\tif (start && plan.containers.length > 0) {\n\t\tawait docker.up(composePath, plan.containers);\n\t}\n\n\t// A running Caddy does not notice a changed import, so the routes this\n\t// reconcile just wrote are inert until it is told. Failing is not fatal: the\n\t// container may have only this second come up, in which case it already read\n\t// them.\n\tif (start && plan.containers.includes('caddy')) {\n\t\tawait docker.reload(composePath, 'caddy').catch(() => {});\n\t}\n\n\t// After `up`, because Caddy generates its CA on first start — and only then,\n\t// so copying earlier gets nothing.\n\tif (start && plan.containers.includes('caddy')) {\n\t\tawait docker\n\t\t\t.copyOut(\n\t\t\t\tcomposePath,\n\t\t\t\t'caddy',\n\t\t\t\tCADDY_ROOT_IN_CONTAINER,\n\t\t\t\tjoin(root, LOCAL_CA_PATH),\n\t\t\t)\n\t\t\t.catch(() => {\n\t\t\t\t// Not fatal. Without it, `https://` local addresses fail to verify\n\t\t\t\t// and say so clearly; taking the whole reconcile down would be a\n\t\t\t\t// worse trade for a developer who is not using them yet.\n\t\t\t});\n\t}\n\n\t// Only once the containers are up: there is nothing to create inside a\n\t// container that is not running.\n\tconst provisioned =\n\t\tstart && provision ? await create(plan, ports, sql, buckets, project) : [];\n\n\tawait saveState(root, { hash, stage });\n\n\treturn { ...result, provisioned, changed: true };\n}\n\n/**\n * Create the databases, schemas, and buckets the plan names.\n *\n * Idempotent throughout, so this runs on every non-converged reconcile rather\n * than being something to remember.\n */\nasync function create(\n\tplan: Plan,\n\tports: PortAssignments,\n\tsql: (port: number) => SqlClient,\n\tbuckets: (port: number) => BucketClient,\n\t/** Seeds the derived role passwords — see `localRolePassword`. */\n\tproject: string,\n): Promise<Applied[]> {\n\tconst applied: Applied[] = [];\n\n\tconst postgresPort = ports[primaryPortKey('postgres')];\n\tconst statements = postgresStatements(plan, project);\n\tif (postgresPort !== undefined && statements.length > 0) {\n\t\tapplied.push(...(await applyPostgres(sql(postgresPort), statements)));\n\t}\n\n\tconst minioPort = ports[primaryPortKey('minio')];\n\tconst names = bucketNames(plan);\n\tconst policies = bucketPolicies(plan);\n\tif (minioPort !== undefined && (names.length > 0 || policies.length > 0)) {\n\t\tconst client = buckets(minioPort);\n\n\t\t// Buckets first: a policy names a bucket, and applying one to a bucket\n\t\t// that does not exist yet fails on the first reconcile of a new project.\n\t\tif (names.length > 0) applied.push(...(await applyBuckets(client, names)));\n\t\tif (policies.length > 0)\n\t\t\tapplied.push(...(await applyPolicies(client, policies)));\n\t}\n\n\treturn applied;\n}\n\n/**\n * The host ports of containers already running.\n *\n * Keyed the same way allocation keys them, so the two merge without either\n * knowing about the other.\n */\nasync function observedPorts(\n\tdocker: Docker,\n\tcomposePath: string,\n\tcontainers: readonly string[],\n): Promise<PortAssignments> {\n\tconst observed: Record<string, number> = {};\n\n\tfor (const container of containers) {\n\t\tfor (const port of portsOf(container)) {\n\t\t\tconst running = await docker.publishedPort(\n\t\t\t\tcomposePath,\n\t\t\t\tcontainer,\n\t\t\t\tport.inside,\n\t\t\t);\n\t\t\tif (running !== undefined) observed[port.key] = running;\n\t\t}\n\t}\n\n\treturn observed;\n}\n\n/**\n * Where each container can be reached on this machine.\n *\n * The primary port only — a container's console is for a human to open, not\n * something an app connects to.\n */\nfunction addressesFor(\n\tcontainers: readonly string[],\n\tports: PortAssignments,\n): Record<string, string> {\n\tconst addresses: Record<string, string> = {};\n\n\tfor (const container of containers) {\n\t\tconst port = ports[primaryPortKey(container)];\n\t\tif (port !== undefined) addresses[container] = `localhost:${port}`;\n\t}\n\n\treturn addresses;\n}\n\n/** Write a file only when its content would change, so mtimes stay meaningful. */\nasync function write(path: string, content: string): Promise<void> {\n\ttry {\n\t\tif ((await readFile(path, 'utf-8')) === content) return;\n\t} catch {\n\t\t// Absent or unreadable — write it.\n\t}\n\n\tawait mkdir(dirname(path), { recursive: true });\n\tawait writeFile(path, content);\n}\n","/**\n * Workspace config → reconcile.\n *\n * The one place the old `services:` block is read, and what it becomes: images\n * stay explicit config, the events backend stays config until `topic` and\n * `queue` are kinds, and anything no construct implies is listed as an\n * exception. What is *derived* — which containers exist at all — comes from the\n * manifest and is never read from here.\n *\n * Shared by `gkm setup`, `gkm dev`, and `gkm test` so all three converge on the\n * same state, differing only in the stage they pass.\n */\n\nimport { type ConstructManifest, provisionOrder } from '@geekmidas/manifest';\nimport { loadPortState, savePortState } from '../credentials/index.js';\nimport { cacheBackendOf, providerOf } from '../workspace/backends.js';\nimport { appKey, hostOf } from '../workspace/derive.js';\nimport { allConstructGlobs } from '../workspace/index.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport { discover } from './discover.js';\nimport { type ReconcileResult, reconcile } from './index.js';\nimport { planFor } from './plan.js';\n\n/**\n * Every constructs glob in the workspace, resolved against its app.\n *\n * Absolute because discovery globs from a single cwd while apps live in\n * different directories.\n */\nexport function constructGlobs(workspace: NormalizedWorkspace): string[] {\n\treturn allConstructGlobs(workspace);\n}\n\n/** Whether any app has adopted the constructs glob. */\nexport function usesConstructs(workspace: NormalizedWorkspace): boolean {\n\treturn constructGlobs(workspace).length > 0;\n}\n\n/**\n * Image pins, by container. The config half of the derived/config split.\n *\n * Read from `services.images` rather than from the backend keys beside it.\n * They used to share one key, which is how `cache: true` came to mean \"start a\n * Redis\" — the last way a container could exist because config asked for one\n * instead of because something declared it.\n */\nexport function imagePins(\n\tworkspace: NormalizedWorkspace,\n): Record<string, string> {\n\treturn { ...workspace.services.images };\n}\n\n/**\n * The containers a workspace's declarations imply, without reconciling.\n *\n * The same derivation `reconcileWorkspace` makes, available to callers that\n * need to know *what would run* without starting anything or writing ports —\n * `gkm setup` deciding which credentials to generate, `gkm docker` writing a\n * compose file. Those used to each read a boolean per service out of config,\n * which is how a container could exist because config asked for one rather than\n * because something declared it.\n */\nexport async function derivedContainers(\n\tworkspace: NormalizedWorkspace,\n\tstage: string,\n\tmanifest?: ConstructManifest,\n): Promise<readonly string[]> {\n\tconst found =\n\t\tmanifest ??\n\t\t(await discover({\n\t\t\tpatterns: constructGlobs(workspace),\n\t\t\tcwd: workspace.root,\n\t\t}));\n\n\treturn planFor(found, stage, provisionOrder(found), {\n\t\tevents: workspace.services.events,\n\t\tcache: cacheBackendOf(workspace.services.cache, providerOf(workspace)),\n\t}).containers;\n}\n\nexport interface WorkspaceReconcileOptions {\n\tstage: string;\n\t/** Start containers and wait for health. */\n\tstart?: boolean;\n\t/** A manifest already in hand — the dev watcher has one; setup does not. */\n\tmanifest?: ConstructManifest;\n}\n\n/**\n * Reconcile a workspace for one stage.\n *\n * Ports are loaded and saved around the call rather than inside it, so the loop\n * stays a pure-ish function of its inputs and the store keeps its single owner.\n */\nexport async function reconcileWorkspace(\n\tworkspace: NormalizedWorkspace,\n\toptions: WorkspaceReconcileOptions,\n): Promise<ReconcileResult> {\n\tconst manifest =\n\t\toptions.manifest ??\n\t\t(await discover({\n\t\t\tpatterns: constructGlobs(workspace),\n\t\t\tcwd: workspace.root,\n\t\t}));\n\n\tconst result = await reconcile({\n\t\troot: workspace.root,\n\t\tproject: workspace.name,\n\t\tmanifest,\n\t\tstage: options.stage,\n\t\tevents: workspace.services.events,\n\t\t// A backend name, not an image pin. `cache: 'db'` says where the cache\n\t\t// lives and implies no container at all.\n\t\tcache: cacheBackendOf(workspace.services.cache, providerOf(workspace)),\n\t\timages: imagePins(workspace),\n\t\tsaved: await loadPortState(workspace.root),\n\t\taddresses: surfaceAddresses(workspace, manifest),\n\t\t...(options.start === undefined ? {} : { start: options.start }),\n\t});\n\n\tawait savePortState(workspace.root, { ...result.ports });\n\n\treturn result;\n}\n\n/**\n * Where each declared surface and site answers locally.\n *\n * The addresses come from the ports the workspace already assigns; which\n * construct sits at which one comes from the manifest. That split is the point:\n * this used to answer \"who may call whom\" by listing every app the workspace\n * runs, which is a different question that happened to give the same answer in\n * a single-repo workspace and no answer at all deployed. Now it answers only\n * \"where does this construct answer\", and the graph answers the rest.\n *\n * A site is matched to its app by `path`, because that is the one thing a site\n * declaration and a workspace app both name. Surfaces are matched to the\n * backend app: every `rest-api` in a process answers on that process's port, so\n * an app serving both its own API and an auth server publishes one address\n * twice — which is exactly what it does at runtime.\n */\nfunction surfaceAddresses(\n\tworkspace: NormalizedWorkspace,\n\tmanifest: ConstructManifest,\n): Record<string, string> {\n\tconst addresses: Record<string, string> = {};\n\n\tfor (const [id, declaration] of Object.entries(manifest)) {\n\t\tif (declaration.kind !== 'rest-api' && declaration.kind !== 'site')\n\t\t\tcontinue;\n\n\t\t// The app serving it, by the same name the derivation gave it — a surface\n\t\t// mounted into another answers at its host's port, not at one of its own.\n\t\tconst host =\n\t\t\tdeclaration.kind === 'site' ? appKey(id) : appKeyOfHost(manifest, id);\n\t\tconst app = host ? workspace.apps[host] : undefined;\n\n\t\tif (app?.port) addresses[id] = localAddress(app.port);\n\t}\n\n\treturn addresses;\n}\n\n/** The app key serving a surface, following a mount to its host. */\nfunction appKeyOfHost(\n\tmanifest: ConstructManifest,\n\tid: string,\n): string | undefined {\n\tconst host = hostOf(manifest, id);\n\treturn host ? appKey(host) : undefined;\n}\n\n/** Where a local process answers, given the port the workspace gave it. */\nfunction localAddress(port: number): string {\n\treturn `http://localhost:${port}`;\n}\n\n/** A workspace path and a declared path, comparable. */\nfunction _normalizePath(path: string): string {\n\treturn path.replace(/^\\.\\//, '').replace(/\\/+$/, '');\n}\n","import { execSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { createServer } from 'node:net';\nimport { dirname, join, resolve } from 'node:path';\nimport { config as dotenvConfig } from 'dotenv';\nimport { parse as parseYaml } from 'yaml';\nimport {\n\tgetAppNameFromCwd,\n\tloadWorkspaceAppInfo,\n\ttype WorkspaceAppInfo,\n} from '../config';\nimport { usesConstructs } from '../reconcile/workspace.js';\nimport { generatePgBossUrl } from '../secrets/generator.js';\nimport {\n\treadStageSecrets,\n\tsecretsExist,\n\ttoEmbeddableSecrets,\n} from '../secrets/storage.js';\nimport { getDependencyEnvVars } from '../workspace/index.js';\n\nconst logger = console;\n\n// ---------------------------------------------------------------------------\n// Environment files\n// ---------------------------------------------------------------------------\n\n/**\n * Load environment files\n * @internal Exported for testing\n */\nexport function loadEnvFiles(\n\tenvConfig: string | string[] | undefined,\n\tcwd: string = process.cwd(),\n): { loaded: string[]; missing: string[] } {\n\tconst loaded: string[] = [];\n\tconst missing: string[] = [];\n\n\t// Normalize to array\n\tconst envFiles = envConfig\n\t\t? Array.isArray(envConfig)\n\t\t\t? envConfig\n\t\t\t: [envConfig]\n\t\t: ['.env'];\n\n\t// Load each env file in order (later files override earlier)\n\tfor (const envFile of envFiles) {\n\t\tconst envPath = resolve(cwd, envFile);\n\t\tif (existsSync(envPath)) {\n\t\t\tdotenvConfig({ path: envPath, override: true, quiet: true });\n\t\t\tloaded.push(envFile);\n\t\t} else if (envConfig) {\n\t\t\t// Only report as missing if explicitly configured\n\t\t\tmissing.push(envFile);\n\t\t}\n\t}\n\n\treturn { loaded, missing };\n}\n\n// ---------------------------------------------------------------------------\n// Port utilities\n// ---------------------------------------------------------------------------\n\n/**\n * Check if a port is available\n * @internal Exported for testing\n */\nexport async function isPortAvailable(port: number): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst server = createServer();\n\n\t\tserver.once('error', (err: NodeJS.ErrnoException) => {\n\t\t\tif (err.code === 'EADDRINUSE') {\n\t\t\t\tresolve(false);\n\t\t\t} else {\n\t\t\t\tresolve(false);\n\t\t\t}\n\t\t});\n\n\t\tserver.once('listening', () => {\n\t\t\tserver.close();\n\t\t\tresolve(true);\n\t\t});\n\n\t\tserver.listen(port);\n\t});\n}\n\n/**\n * Find an available port starting from the preferred port\n * @internal Exported for testing\n */\nexport async function findAvailablePort(\n\tpreferredPort: number,\n\tmaxAttempts = 10,\n): Promise<number> {\n\tfor (let i = 0; i < maxAttempts; i++) {\n\t\tconst port = preferredPort + i;\n\t\tif (await isPortAvailable(port)) {\n\t\t\treturn port;\n\t\t}\n\t\tlogger.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);\n\t}\n\n\tthrow new Error(\n\t\t`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`,\n\t);\n}\n\n// ---------------------------------------------------------------------------\n// Docker Compose port mapping\n// ---------------------------------------------------------------------------\n\n/**\n * A port mapping extracted from docker-compose.yml.\n * Only entries using env var interpolation (e.g., `${VAR:-default}:container`) are captured.\n */\nexport interface ComposePortMapping {\n\tservice: string;\n\tenvVar: string;\n\tdefaultPort: number;\n\tcontainerPort: number;\n}\n\n/** Port state persisted to .gkm/ports.json, keyed by env var name. */\nexport type PortState = Record<string, number>;\n\nexport interface ResolvedServicePorts {\n\tdockerEnv: Record<string, string>;\n\tports: PortState;\n\tmappings: ComposePortMapping[];\n}\n\nconst PORT_STATE_PATH = '.gkm/ports.json';\n\n/**\n * Parse docker-compose.yml and extract all port mappings that use env var interpolation.\n * Entries like `'${POSTGRES_HOST_PORT:-5432}:5432'` are captured.\n * Fixed port mappings like `'5050:80'` are skipped.\n * @internal Exported for testing\n */\nexport function parseComposePortMappings(\n\tcomposePath: string,\n): ComposePortMapping[] {\n\tif (!existsSync(composePath)) {\n\t\treturn [];\n\t}\n\n\tconst content = readFileSync(composePath, 'utf-8');\n\tconst compose = parseYaml(content) as {\n\t\tservices?: Record<string, { ports?: string[] }>;\n\t};\n\n\tif (!compose?.services) {\n\t\treturn [];\n\t}\n\n\tconst results: ComposePortMapping[] = [];\n\n\tfor (const [serviceName, serviceConfig] of Object.entries(compose.services)) {\n\t\tfor (const portMapping of serviceConfig?.ports ?? []) {\n\t\t\tconst match = String(portMapping).match(/\\$\\{(\\w+):-(\\d+)\\}:(\\d+)/);\n\t\t\tif (match?.[1] && match[2] && match[3]) {\n\t\t\t\tresults.push({\n\t\t\t\t\tservice: serviceName,\n\t\t\t\t\tenvVar: match[1],\n\t\t\t\t\tdefaultPort: Number(match[2]),\n\t\t\t\t\tcontainerPort: Number(match[3]),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn results;\n}\n\n/**\n * Load saved port state from .gkm/ports.json.\n * @internal Exported for testing\n */\nexport async function loadPortState(workspaceRoot: string): Promise<PortState> {\n\ttry {\n\t\tconst raw = await readFile(join(workspaceRoot, PORT_STATE_PATH), 'utf-8');\n\t\treturn JSON.parse(raw) as PortState;\n\t} catch {\n\t\treturn {};\n\t}\n}\n\n/**\n * Save port state to .gkm/ports.json.\n * @internal Exported for testing\n */\nexport async function savePortState(\n\tworkspaceRoot: string,\n\tports: PortState,\n): Promise<void> {\n\tconst dir = join(workspaceRoot, '.gkm');\n\tawait mkdir(dir, { recursive: true });\n\tawait writeFile(\n\t\tjoin(workspaceRoot, PORT_STATE_PATH),\n\t\t`${JSON.stringify(ports, null, 2)}\\n`,\n\t);\n}\n\n/**\n * Check if a project's own Docker container is running and return its host port.\n * Uses `docker compose port` scoped to the project's compose file.\n * @internal Exported for testing\n */\nexport function getContainerHostPort(\n\tworkspaceRoot: string,\n\tservice: string,\n\tcontainerPort: number,\n): number | null {\n\ttry {\n\t\tconst result = execSync(`docker compose port ${service} ${containerPort}`, {\n\t\t\tcwd: workspaceRoot,\n\t\t\tstdio: 'pipe',\n\t\t})\n\t\t\t.toString()\n\t\t\t.trim();\n\t\tconst match = result.match(/:(\\d+)$/);\n\t\treturn match ? Number(match[1]) : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Resolve host ports for Docker services by parsing docker-compose.yml.\n * Priority: running container → saved state → find available port.\n * Persists resolved ports to .gkm/ports.json.\n * @internal Exported for testing\n */\nexport async function resolveServicePorts(\n\tworkspaceRoot: string,\n): Promise<ResolvedServicePorts> {\n\tconst composePath = join(workspaceRoot, 'docker-compose.yml');\n\tconst mappings = parseComposePortMappings(composePath);\n\n\tif (mappings.length === 0) {\n\t\treturn { dockerEnv: {}, ports: {}, mappings: [] };\n\t}\n\n\tconst savedState = await loadPortState(workspaceRoot);\n\tconst dockerEnv: Record<string, string> = {};\n\tconst ports: PortState = {};\n\t// Track ports assigned in this cycle to avoid duplicates\n\tconst assignedPorts = new Set<number>();\n\n\tlogger.log('\\n🔌 Resolving service ports...');\n\n\tfor (const mapping of mappings) {\n\t\t// 1. Check if own container is already running\n\t\tconst containerPort = getContainerHostPort(\n\t\t\tworkspaceRoot,\n\t\t\tmapping.service,\n\t\t\tmapping.containerPort,\n\t\t);\n\t\tif (containerPort !== null) {\n\t\t\tports[mapping.envVar] = containerPort;\n\t\t\tdockerEnv[mapping.envVar] = String(containerPort);\n\t\t\tassignedPorts.add(containerPort);\n\t\t\tlogger.log(\n\t\t\t\t` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`,\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// 2. Check saved port state\n\t\tconst savedPort = savedState[mapping.envVar];\n\t\tif (\n\t\t\tsavedPort &&\n\t\t\t!assignedPorts.has(savedPort) &&\n\t\t\t(await isPortAvailable(savedPort))\n\t\t) {\n\t\t\tports[mapping.envVar] = savedPort;\n\t\t\tdockerEnv[mapping.envVar] = String(savedPort);\n\t\t\tassignedPorts.add(savedPort);\n\t\t\tlogger.log(\n\t\t\t\t` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`,\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// 3. Find available port (skipping ports already assigned this cycle)\n\t\tlet resolvedPort = await findAvailablePort(mapping.defaultPort);\n\t\twhile (assignedPorts.has(resolvedPort)) {\n\t\t\tresolvedPort = await findAvailablePort(resolvedPort + 1);\n\t\t}\n\t\tports[mapping.envVar] = resolvedPort;\n\t\tdockerEnv[mapping.envVar] = String(resolvedPort);\n\t\tassignedPorts.add(resolvedPort);\n\n\t\tif (resolvedPort !== mapping.defaultPort) {\n\t\t\tlogger.log(\n\t\t\t\t` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`,\n\t\t\t);\n\t\t} else {\n\t\t\tlogger.log(\n\t\t\t\t` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tawait savePortState(workspaceRoot, ports);\n\n\treturn { dockerEnv, ports, mappings };\n}\n\n// ---------------------------------------------------------------------------\n// URL rewriting\n// ---------------------------------------------------------------------------\n\n/**\n * Replace a port in a URL string.\n * Handles both `hostname:port` and `localhost:port` patterns.\n * @internal Exported for testing\n */\nexport function replacePortInUrl(\n\turl: string,\n\toldPort: number,\n\tnewPort: number,\n): string {\n\tif (oldPort === newPort) return url;\n\t// Replace literal :port (in authority section)\n\tlet result = url.replace(\n\t\tnew RegExp(`:${oldPort}(?=[/?#]|$)`, 'g'),\n\t\t`:${newPort}`,\n\t);\n\t// Replace URL-encoded :port (e.g., in query params like endpoint=http%3A%2F%2Flocalhost%3A4566)\n\tresult = result.replace(\n\t\tnew RegExp(`%3A${oldPort}(?=[%/?#&]|$)`, 'gi'),\n\t\t`%3A${newPort}`,\n\t);\n\treturn result;\n}\n\n/**\n * Rewrite connection URLs and port vars in secrets with resolved ports.\n * Uses the parsed compose mappings to determine which default ports to replace.\n * Pure transform — does not modify secrets on disk.\n * @internal Exported for testing\n */\nexport function rewriteUrlsWithPorts(\n\tsecrets: Record<string, string>,\n\tresolvedPorts: ResolvedServicePorts,\n): Record<string, string> {\n\tconst { ports, mappings } = resolvedPorts;\n\tconst result = { ...secrets };\n\n\t// Build a map of defaultPort → resolvedPort for all changed ports\n\tconst portReplacements: { defaultPort: number; resolvedPort: number }[] = [];\n\t// Collect Docker service names for hostname rewriting\n\tconst serviceNames = new Set<string>();\n\tfor (const mapping of mappings) {\n\t\tserviceNames.add(mapping.service);\n\t\tconst resolved = ports[mapping.envVar];\n\t\tif (resolved !== undefined) {\n\t\t\tportReplacements.push({\n\t\t\t\tdefaultPort: mapping.defaultPort,\n\t\t\t\tresolvedPort: resolved,\n\t\t\t});\n\t\t}\n\t}\n\n\t// Rewrite _HOST env vars that use Docker service names\n\tfor (const [key, value] of Object.entries(result)) {\n\t\tif (!key.endsWith('_HOST')) continue;\n\t\tif (serviceNames.has(value)) {\n\t\t\tresult[key] = 'localhost';\n\t\t}\n\t}\n\n\t// Rewrite _PORT env vars whose values match a default port\n\tfor (const [key, value] of Object.entries(result)) {\n\t\tif (!key.endsWith('_PORT')) continue;\n\t\tfor (const { defaultPort, resolvedPort } of portReplacements) {\n\t\t\tif (value === String(defaultPort)) {\n\t\t\t\tresult[key] = String(resolvedPort);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Rewrite URLs: replace Docker service hostnames with localhost and fix ports\n\tfor (const [key, value] of Object.entries(result)) {\n\t\tif (\n\t\t\t!key.endsWith('_URL') &&\n\t\t\t!key.endsWith('_ENDPOINT') &&\n\t\t\t!key.endsWith('_CONNECTION_STRING') &&\n\t\t\tkey !== 'DATABASE_URL'\n\t\t)\n\t\t\tcontinue;\n\n\t\tlet rewritten = value;\n\t\tfor (const name of serviceNames) {\n\t\t\trewritten = rewritten.replace(\n\t\t\t\tnew RegExp(`@${name}:`, 'g'),\n\t\t\t\t'@localhost:',\n\t\t\t);\n\t\t}\n\t\tfor (const { defaultPort, resolvedPort } of portReplacements) {\n\t\t\trewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);\n\t\t}\n\t\tresult[key] = rewritten;\n\t}\n\n\treturn result;\n}\n\n// ---------------------------------------------------------------------------\n// Docker Compose services\n// ---------------------------------------------------------------------------\n\n/**\n * Build the environment variables to pass to `docker compose up`.\n * Merges process.env, secrets, and port mappings so that Docker Compose\n * can interpolate variables like ${POSTGRES_USER} correctly.\n * @internal Exported for testing\n */\nexport function buildDockerComposeEnv(\n\tsecretsEnv?: Record<string, string>,\n\tportEnv?: Record<string, string>,\n): Record<string, string | undefined> {\n\treturn { ...process.env, ...secretsEnv, ...portEnv };\n}\n\n/**\n * Parse all service names from a docker-compose.yml file.\n * @internal Exported for testing\n */\nexport function parseComposeServiceNames(composePath: string): string[] {\n\tif (!existsSync(composePath)) {\n\t\treturn [];\n\t}\n\n\tconst content = readFileSync(composePath, 'utf-8');\n\tconst compose = parseYaml(content) as {\n\t\tservices?: Record<string, unknown>;\n\t};\n\n\treturn Object.keys(compose?.services ?? {});\n}\n\n/**\n * Start docker-compose services for a single-app project (no workspace config).\n * Starts all services defined in docker-compose.yml.\n */\nexport async function startComposeServices(\n\tcwd: string,\n\tportEnv?: Record<string, string>,\n\tsecretsEnv?: Record<string, string>,\n): Promise<void> {\n\tconst composeFile = join(cwd, 'docker-compose.yml');\n\tif (!existsSync(composeFile)) {\n\t\treturn;\n\t}\n\n\tconst servicesToStart = parseComposeServiceNames(composeFile);\n\tif (servicesToStart.length === 0) {\n\t\treturn;\n\t}\n\n\tlogger.log(`🐳 Starting services: ${servicesToStart.join(', ')}`);\n\n\ttry {\n\t\texecSync(`docker compose up -d ${servicesToStart.join(' ')}`, {\n\t\t\tcwd,\n\t\t\tstdio: 'inherit',\n\t\t\tenv: buildDockerComposeEnv(secretsEnv, portEnv),\n\t\t});\n\n\t\tlogger.log('✅ Services started');\n\t} catch (error) {\n\t\tlogger.error('❌ Failed to start services:', (error as Error).message);\n\t\tthrow error;\n\t}\n}\n\n/**\n * Start docker-compose services for a workspace.\n * Discovers all services from docker-compose.yml and starts everything\n * except app services (which are managed by turbo).\n * @internal Exported for testing\n */\nexport async function startWorkspaceServices(\n\tworkspace: { root: string; apps: Record<string, unknown> },\n\tportEnv?: Record<string, string>,\n\tsecretsEnv?: Record<string, string>,\n): Promise<void> {\n\tconst composeFile = join(workspace.root, 'docker-compose.yml');\n\tif (!existsSync(composeFile)) {\n\t\treturn;\n\t}\n\n\t// Discover all services from docker-compose.yml\n\tconst allServices = parseComposeServiceNames(composeFile);\n\n\t// Exclude app services (managed by turbo, not docker)\n\tconst appNames = new Set(Object.keys(workspace.apps));\n\tconst servicesToStart = allServices.filter((name) => !appNames.has(name));\n\n\tif (servicesToStart.length === 0) {\n\t\treturn;\n\t}\n\n\tlogger.log(`🐳 Starting services: ${servicesToStart.join(', ')}`);\n\n\ttry {\n\t\t// Start services with docker-compose, passing secrets so that\n\t\t// POSTGRES_USER, POSTGRES_PASSWORD, etc. are interpolated correctly\n\t\texecSync(`docker compose up -d ${servicesToStart.join(' ')}`, {\n\t\t\tcwd: workspace.root,\n\t\t\tstdio: 'inherit',\n\t\t\tenv: buildDockerComposeEnv(secretsEnv, portEnv),\n\t\t});\n\n\t\tlogger.log('✅ Services started');\n\t} catch (error) {\n\t\tlogger.error('❌ Failed to start services:', (error as Error).message);\n\t\tthrow error;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Secrets loading\n// ---------------------------------------------------------------------------\n\n/**\n * Load and flatten secrets for an app from encrypted storage.\n * For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.\n * @internal Exported for testing\n */\nexport async function loadSecretsForApp(\n\tsecretsRoot: string,\n\tappName?: string,\n\tstages: string[] = ['dev', 'development'],\n): Promise<Record<string, string>> {\n\tlet secrets: Record<string, string> = {};\n\n\tfor (const stage of stages) {\n\t\tif (secretsExist(stage, secretsRoot)) {\n\t\t\tconst stageSecrets = await readStageSecrets(stage, secretsRoot);\n\t\t\tif (stageSecrets) {\n\t\t\t\tlogger.log(`🔐 Loading secrets from stage: ${stage}`);\n\t\t\t\tsecrets = toEmbeddableSecrets(stageSecrets);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (Object.keys(secrets).length === 0) {\n\t\treturn {};\n\t}\n\n\t// Single app mode - no mapping needed\n\tif (!appName) {\n\t\treturn secrets;\n\t}\n\n\t// Workspace app mode - map {APP}_* to generic names\n\tconst prefix = appName.toUpperCase();\n\tconst mapped = { ...secrets };\n\n\t// Map {APP}_DATABASE_URL → DATABASE_URL\n\tconst appDbUrl = secrets[`${prefix}_DATABASE_URL`];\n\tif (appDbUrl) {\n\t\tmapped.DATABASE_URL = appDbUrl;\n\t}\n\n\treturn mapped;\n}\n\n/**\n * Walk up the directory tree to find the root containing .gkm/secrets/.\n * @internal Exported for testing\n */\nexport function findSecretsRoot(startDir: string): string {\n\tlet dir = startDir;\n\twhile (dir !== '/') {\n\t\tif (existsSync(join(dir, '.gkm', 'secrets'))) {\n\t\t\treturn dir;\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) break;\n\t\tdir = parent;\n\t}\n\treturn startDir;\n}\n\n// ---------------------------------------------------------------------------\n// Credentials preload / injection\n// ---------------------------------------------------------------------------\n\n/**\n * Generate the credentials injection code snippet.\n * This is the common logic used by both entry wrapper and exec preload.\n * @internal\n */\nfunction generateCredentialsInjection(secretsJsonPath: string): string {\n\treturn `import { existsSync, readFileSync } from 'node:fs';\n\n// Inject dev secrets via globalThis and process.env\n// Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where\n// Object.assign on the Credentials export only mutates one module copy.\nconst secretsPath = '${secretsJsonPath}';\nif (existsSync(secretsPath)) {\n const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));\n globalThis.__gkm_credentials__ = secrets;\n Object.assign(process.env, secrets);\n}\n`;\n}\n\n/**\n * Create a preload script that injects secrets into Credentials.\n * Used by `gkm exec` to inject secrets before running any command.\n * @internal Exported for testing\n */\nexport async function createCredentialsPreload(\n\tpreloadPath: string,\n\tsecretsJsonPath: string,\n): Promise<void> {\n\tconst content = `/**\n * Credentials preload generated by 'gkm exec'\n * This file is loaded via NODE_OPTIONS=\"--import <path>\"\n */\n${generateCredentialsInjection(secretsJsonPath)}`;\n\n\tawait writeFile(preloadPath, content);\n}\n\n/**\n * Create a wrapper script that injects secrets before importing the entry file.\n * @internal Exported for testing\n */\nexport async function createEntryWrapper(\n\twrapperPath: string,\n\tentryPath: string,\n\tsecretsJsonPath?: string,\n): Promise<void> {\n\tconst credentialsInjection = secretsJsonPath\n\t\t? `${generateCredentialsInjection(secretsJsonPath)}\n`\n\t\t: '';\n\n\t// Use dynamic import() to ensure secrets are assigned before the entry file loads\n\t// Static imports are hoisted, so Object.assign would run after the entry file is loaded\n\tconst content = `#!/usr/bin/env node\n/**\n * Entry wrapper generated by 'gkm dev --entry'\n */\n${credentialsInjection}// Import and run the user's entry file (dynamic import ensures secrets load first)\nawait import('${entryPath}');\n`;\n\n\tawait writeFile(wrapperPath, content);\n}\n\n// ---------------------------------------------------------------------------\n// Prepare credentials (shared by dev, exec, test)\n// ---------------------------------------------------------------------------\n\n/**\n * Result of preparing credentials.\n */\nexport interface EntryCredentialsResult {\n\t/** Credentials to inject (secrets + PORT) */\n\tcredentials: Record<string, string>;\n\t/** Resolved port (from --port, workspace config, or default 3000) */\n\tresolvedPort: number;\n\t/** Path where credentials JSON was written */\n\tsecretsJsonPath: string;\n\t/** Resolved app name (if in workspace) */\n\tappName: string | undefined;\n\t/** Secrets root directory */\n\tsecretsRoot: string;\n\t/** Workspace app info (if in a workspace) */\n\tappInfo?: WorkspaceAppInfo;\n\t/**\n\t * The keys the manifest declared, as opposed to those a secret store held.\n\t *\n\t * Kept apart because they cannot be *sniffed*. A construct reads its own key\n\t * inside `@geekmidas/constructs`, not in application code, so a walk of the\n\t * app finds no `get('MAIL_URL')` to find — and a caller that filters the\n\t * environment down to what it sniffed would drop every URL the target just\n\t * resolved. `gkm test` did exactly that.\n\t */\n\tdeclaredKeys: string[];\n}\n\n/**\n * Prepare credentials for dev/exec/test modes.\n * Loads workspace config, secrets, resolves Docker ports, rewrites URLs,\n * injects PORT, dependency URLs, and writes credentials JSON.\n *\n * @param options.resolveDockerPorts - How to resolve Docker ports:\n * - `'full'` (default): probe running containers, saved state, then find available ports. Used by dev/test.\n * - `'readonly'`: check running containers and saved state only, never probe for new ports. Used by exec.\n * @param options.stages - Secret stages to try, in order. Default: ['dev', 'development'].\n * @param options.startDocker - Start Docker Compose services after port resolution. Default: false.\n * @param options.secretsFileName - Custom secrets JSON filename. Default: 'dev-secrets-{appName}.json' or 'dev-secrets.json'.\n * @internal Exported for testing\n */\nexport async function prepareEntryCredentials(options: {\n\texplicitPort?: number;\n\tcwd?: string;\n\tresolveDockerPorts?: 'full' | 'readonly';\n\t/** Secret stages to try, in order. Default: ['dev', 'development'] */\n\tstages?: string[];\n\t/** Start Docker Compose services after port resolution. Default: false */\n\tstartDocker?: boolean;\n\t/** Custom secrets JSON filename. Default: 'dev-secrets-{appName}.json' or 'dev-secrets.json' */\n\tsecretsFileName?: string;\n\t/**\n\t * The stage to reconcile the local target for.\n\t *\n\t * `gkm test` passes `test` and gets the same containers with suffixed\n\t * resources; `gkm dev` passes `development` and gets unsuffixed ones. Only\n\t * read by workspaces that have adopted the constructs glob.\n\t */\n\treconcileStage?: string;\n}): Promise<EntryCredentialsResult> {\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst portMode = options.resolveDockerPorts ?? 'full';\n\n\t// Try to get workspace app config for port and secrets\n\tlet workspaceAppPort: number | undefined;\n\tlet secretsRoot: string = cwd;\n\tlet appName: string | undefined;\n\tlet appInfo: WorkspaceAppInfo | undefined;\n\n\ttry {\n\t\tappInfo = await loadWorkspaceAppInfo(cwd);\n\t\tworkspaceAppPort = appInfo.app.port;\n\t\tsecretsRoot = appInfo.workspaceRoot;\n\t\tappName = appInfo.appName;\n\t} catch {\n\t\t// Not in a workspace - use defaults (expected for non-gkm apps using gkm exec)\n\t\tsecretsRoot = findSecretsRoot(cwd);\n\t\tappName = getAppNameFromCwd(cwd) ?? undefined;\n\t}\n\n\t// Determine port: explicit --port > workspace config > default 3000\n\tconst resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3000;\n\n\t// Load secrets and inject PORT\n\tconst credentials = await loadSecretsForApp(\n\t\tsecretsRoot,\n\t\tappName,\n\t\toptions.stages,\n\t);\n\n\t// Always inject PORT into credentials so apps can read it\n\tcredentials.PORT = String(resolvedPort);\n\n\tconst declaredKeys: string[] = [];\n\n\t// An app that has adopted the constructs glob derives its containers, ports,\n\t// and URLs from what it declares. The branch below is what it replaces:\n\t// parsing a hand-written compose file for ports and rewriting URLs to match.\n\tif (appInfo && usesConstructs(appInfo.workspace)) {\n\t\tconst { reconcileWorkspace } = await import('../reconcile/workspace.js');\n\t\tconst reconciled = await reconcileWorkspace(appInfo.workspace, {\n\t\t\tstage: options.reconcileStage ?? 'development',\n\t\t\tstart: options.startDocker ?? false,\n\t\t});\n\n\t\t// Declared URLs win over anything sniffed or stored: the manifest is the\n\t\t// statement of what exists, and a stale secret naming an old port is\n\t\t// exactly the drift this replaces.\n\t\tObject.assign(credentials, reconciled.env);\n\t\tdeclaredKeys.push(...Object.keys(reconciled.env));\n\n\t\tif (Object.keys(reconciled.env).length > 0) {\n\t\t\tlogger.log(\n\t\t\t\t`🔌 Resolved ${Object.keys(reconciled.env).length} declared URL(s)`,\n\t\t\t);\n\t\t}\n\t} else {\n\t\t// Resolve Docker ports and rewrite connection URLs\n\t\tconst composePath = join(secretsRoot, 'docker-compose.yml');\n\t\tconst mappings = parseComposePortMappings(composePath);\n\t\tif (mappings.length > 0) {\n\t\t\tlet resolvedPorts: ResolvedServicePorts;\n\n\t\t\tif (portMode === 'full') {\n\t\t\t\t// Full resolution: probe containers, saved state, find available ports\n\t\t\t\tresolvedPorts = await resolveServicePorts(secretsRoot);\n\t\t\t} else {\n\t\t\t\t// Readonly: check running containers and saved state only\n\t\t\t\tconst savedPorts = await loadPortState(secretsRoot);\n\t\t\t\tconst ports: PortState = {};\n\n\t\t\t\tfor (const mapping of mappings) {\n\t\t\t\t\tconst containerPort = getContainerHostPort(\n\t\t\t\t\t\tsecretsRoot,\n\t\t\t\t\t\tmapping.service,\n\t\t\t\t\t\tmapping.containerPort,\n\t\t\t\t\t);\n\t\t\t\t\tif (containerPort !== null) {\n\t\t\t\t\t\tports[mapping.envVar] = containerPort;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst saved = savedPorts[mapping.envVar];\n\t\t\t\t\t\tif (saved !== undefined) {\n\t\t\t\t\t\t\tports[mapping.envVar] = saved;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tresolvedPorts = { dockerEnv: {}, ports, mappings };\n\t\t\t}\n\n\t\t\t// Start Docker services if requested (between port resolution and URL rewriting)\n\t\t\t// Docker needs raw secrets (POSTGRES_USER, etc.) + resolved port env for compose interpolation\n\t\t\tif (options.startDocker) {\n\t\t\t\tif (appInfo) {\n\t\t\t\t\tawait startWorkspaceServices(\n\t\t\t\t\t\tappInfo.workspace,\n\t\t\t\t\t\tresolvedPorts.dockerEnv,\n\t\t\t\t\t\tcredentials,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tawait startComposeServices(\n\t\t\t\t\t\tsecretsRoot,\n\t\t\t\t\t\tresolvedPorts.dockerEnv,\n\t\t\t\t\t\tcredentials,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (Object.keys(resolvedPorts.ports).length > 0) {\n\t\t\t\tconst rewritten = rewriteUrlsWithPorts(credentials, resolvedPorts);\n\t\t\t\tObject.assign(credentials, rewritten);\n\t\t\t\tlogger.log(\n\t\t\t\t\t`🔌 Applied ${Object.keys(resolvedPorts.ports).length} port mapping(s)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Inject dependency URLs (works for both frontend and backend apps)\n\tif (appInfo?.appName) {\n\t\tconst depEnv = getDependencyEnvVars(appInfo.workspace, appInfo.appName);\n\t\tObject.assign(credentials, depEnv);\n\t}\n\n\t// Default event connection strings to pgboss when postgres is available.\n\t// Normally these come from secrets (gkm setup always creates pgboss credentials),\n\t// but this handles old secrets files that predate the pgboss default.\n\tif (\n\t\t!credentials.EVENT_PUBLISHER_CONNECTION_STRING &&\n\t\t!credentials.EVENT_SUBSCRIBER_CONNECTION_STRING &&\n\t\tcredentials.PGBOSS_DB_USER\n\t) {\n\t\tconst pgbossUrl = generatePgBossUrl({\n\t\t\tusername: credentials.PGBOSS_DB_USER,\n\t\t\tpassword: credentials.PGBOSS_DB_PASSWORD ?? '',\n\t\t\thost: credentials.POSTGRES_HOST ?? 'localhost',\n\t\t\tport: Number(credentials.POSTGRES_PORT ?? '5432'),\n\t\t\tdatabase: credentials.POSTGRES_DB ?? 'app',\n\t\t});\n\t\tcredentials.EVENT_PUBLISHER_CONNECTION_STRING = pgbossUrl;\n\t\tcredentials.EVENT_SUBSCRIBER_CONNECTION_STRING = pgbossUrl;\n\t}\n\n\t// Write secrets to temp JSON file (always write since we have PORT)\n\t// Use app-specific filename to avoid race conditions when running multiple apps via turbo\n\tconst secretsDir = join(secretsRoot, '.gkm');\n\tawait mkdir(secretsDir, { recursive: true });\n\tconst secretsFileName =\n\t\toptions.secretsFileName ??\n\t\t(appName ? `dev-secrets-${appName}.json` : 'dev-secrets.json');\n\tconst secretsJsonPath = join(secretsDir, secretsFileName);\n\tawait writeFile(secretsJsonPath, JSON.stringify(credentials, null, 2));\n\n\treturn {\n\t\tcredentials,\n\t\tresolvedPort,\n\t\tsecretsJsonPath,\n\t\tappName,\n\t\tsecretsRoot,\n\t\tappInfo,\n\t\tdeclaredKeys,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
2
|
const require_discover = require('./discover-DTmFeloL.cjs');
|
|
3
|
-
const require_config = require('./config-
|
|
4
|
-
const require_workspace = require('./workspace-
|
|
3
|
+
const require_config = require('./config-CNz5KaSR.cjs');
|
|
4
|
+
const require_workspace = require('./workspace-LKiNyaDy.cjs');
|
|
5
5
|
const require_generator = require('./generator-CNzBw7g5.cjs');
|
|
6
6
|
const require_storage = require('./storage-BJmzYxt8.cjs');
|
|
7
7
|
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
@@ -349,7 +349,7 @@ function postgresImage(version) {
|
|
|
349
349
|
*/
|
|
350
350
|
const DEFAULT_IMAGES = {
|
|
351
351
|
postgres: postgresImage(__geekmidas_manifest.DEFAULT_POSTGRES_VERSION),
|
|
352
|
-
minio: "
|
|
352
|
+
minio: "pgsty/minio:RELEASE.2026-08-04T00-00-00Z",
|
|
353
353
|
mailpit: "axllent/mailpit:latest",
|
|
354
354
|
redis: "redis:8-alpine",
|
|
355
355
|
"redis-http": "hiett/serverless-redis-http:latest",
|
|
@@ -2473,7 +2473,7 @@ async function prepareEntryCredentials(options) {
|
|
|
2473
2473
|
credentials.PORT = String(resolvedPort);
|
|
2474
2474
|
const declaredKeys = [];
|
|
2475
2475
|
if (appInfo && usesConstructs(appInfo.workspace)) {
|
|
2476
|
-
const { reconcileWorkspace: reconcileWorkspace$1 } = await Promise.resolve().then(() => require("./workspace-
|
|
2476
|
+
const { reconcileWorkspace: reconcileWorkspace$1 } = await Promise.resolve().then(() => require("./workspace-CGaUAw5A.cjs"));
|
|
2477
2477
|
const reconciled = await reconcileWorkspace$1(appInfo.workspace, {
|
|
2478
2478
|
stage: options.reconcileStage ?? "development",
|
|
2479
2479
|
start: options.startDocker ?? false
|
|
@@ -2677,4 +2677,4 @@ Object.defineProperty(exports, 'usesConstructs', {
|
|
|
2677
2677
|
return usesConstructs;
|
|
2678
2678
|
}
|
|
2679
2679
|
});
|
|
2680
|
-
//# sourceMappingURL=credentials-
|
|
2680
|
+
//# sourceMappingURL=credentials-DzsXZzkx.cjs.map
|