@geekmidas/cli 10.0.0-alpha.120 → 10.0.0-alpha.122
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/{GoDaddyProvider-3C8U_tVu.cjs → GoDaddyProvider-BJ9j9mR7.cjs} +2 -2
- package/dist/{GoDaddyProvider-3C8U_tVu.cjs.map → GoDaddyProvider-BJ9j9mR7.cjs.map} +1 -1
- package/dist/{GoDaddyProvider-D0MQoOM4.mjs → GoDaddyProvider-wIKYdm7V.mjs} +2 -2
- package/dist/{GoDaddyProvider-D0MQoOM4.mjs.map → GoDaddyProvider-wIKYdm7V.mjs.map} +1 -1
- package/dist/{LegacyStateStore-CRi7TosX.mjs → LegacyStateStore-CkXUJP7i.mjs} +3 -3
- package/dist/{LegacyStateStore-CRi7TosX.mjs.map → LegacyStateStore-CkXUJP7i.mjs.map} +1 -1
- package/dist/{LegacyStateStore-DHsoQGOo.cjs → LegacyStateStore-D1S87I_B.cjs} +3 -3
- package/dist/{LegacyStateStore-DHsoQGOo.cjs.map → LegacyStateStore-D1S87I_B.cjs.map} +1 -1
- package/dist/{LocalStateStore-Dkth0jXX.mjs → LocalStateStore-BWoWeSq0.mjs} +2 -2
- package/dist/{LocalStateStore-Dkth0jXX.mjs.map → LocalStateStore-BWoWeSq0.mjs.map} +1 -1
- package/dist/{LocalStateStore-BQXTkeoS.cjs → LocalStateStore-DmXfpvu4.cjs} +2 -2
- package/dist/{LocalStateStore-BQXTkeoS.cjs.map → LocalStateStore-DmXfpvu4.cjs.map} +1 -1
- package/dist/{S3StateStore-DGKZgyKz.cjs → S3StateStore-BEdsjjP4.cjs} +2 -2
- package/dist/{S3StateStore-DGKZgyKz.cjs.map → S3StateStore-BEdsjjP4.cjs.map} +1 -1
- package/dist/{S3StateStore-pZZnLLBd.mjs → S3StateStore-D19JSGE9.mjs} +2 -2
- package/dist/{S3StateStore-pZZnLLBd.mjs.map → S3StateStore-D19JSGE9.mjs.map} +1 -1
- package/dist/{SSMStateStore-DzkyjQ5D.cjs → SSMStateStore--2bcxoiH.cjs} +2 -2
- package/dist/{SSMStateStore-DzkyjQ5D.cjs.map → SSMStateStore--2bcxoiH.cjs.map} +1 -1
- package/dist/{SSMStateStore-CZRMERDA.mjs → SSMStateStore-7xKw0Zz9.mjs} +2 -2
- package/dist/{SSMStateStore-CZRMERDA.mjs.map → SSMStateStore-7xKw0Zz9.mjs.map} +1 -1
- package/dist/{add-C0kXOsWS.mjs → add-Bu6FkaTn.mjs} +3 -3
- package/dist/{add-C0kXOsWS.mjs.map → add-Bu6FkaTn.mjs.map} +1 -1
- package/dist/{add-Df8cmz0H.cjs → add-DlFUKdjX.cjs} +3 -3
- package/dist/{add-Df8cmz0H.cjs.map → add-DlFUKdjX.cjs.map} +1 -1
- package/dist/{bundler-Dx_grUmc.cjs → bundler-Bg0lPDoi.cjs} +5 -5
- package/dist/{bundler-Dx_grUmc.cjs.map → bundler-Bg0lPDoi.cjs.map} +1 -1
- package/dist/{bundler-B-I4Du99.mjs → bundler-CgE5XTyr.mjs} +5 -5
- package/dist/{bundler-B-I4Du99.mjs.map → bundler-CgE5XTyr.mjs.map} +1 -1
- package/dist/{cli-Cd9exatJ.cjs → cli-Cvjlnpon.cjs} +2 -2
- package/dist/{cli-Cd9exatJ.cjs.map → cli-Cvjlnpon.cjs.map} +1 -1
- package/dist/{cli-DL7GxGAe.mjs → cli-z5S3bHyK.mjs} +2 -2
- package/dist/{cli-DL7GxGAe.mjs.map → cli-z5S3bHyK.mjs.map} +1 -1
- package/dist/{compose-C43c03WZ.mjs → compose-CgBPA44v.mjs} +28 -5
- package/dist/{compose-C43c03WZ.mjs.map → compose-CgBPA44v.mjs.map} +1 -1
- package/dist/{compose-Mdgea4Yt.cjs → compose-bKLAtHLj.cjs} +28 -5
- package/dist/compose-bKLAtHLj.cjs.map +1 -0
- package/dist/deploy/public.cjs +6 -6
- package/dist/deploy/public.mjs +6 -6
- package/dist/{deploy-BMM2hx-K.cjs → deploy-CI85F6dr.cjs} +5 -5
- package/dist/{deploy-BMM2hx-K.cjs.map → deploy-CI85F6dr.cjs.map} +1 -1
- package/dist/{deploy-D5YhwDng.mjs → deploy-Ddy8UBoE.mjs} +5 -5
- package/dist/{deploy-D5YhwDng.mjs.map → deploy-Ddy8UBoE.mjs.map} +1 -1
- package/dist/{dns-agf2VCdV.mjs → dns-BgxvvcQa.mjs} +164 -26
- package/dist/dns-BgxvvcQa.mjs.map +1 -0
- package/dist/{dns-BIYf20p7.cjs → dns-CfEZHt7r.cjs} +164 -26
- package/dist/dns-CfEZHt7r.cjs.map +1 -0
- package/dist/{dokploy-2p4PK6ej.mjs → dokploy-Boj1NSHV.mjs} +2 -2
- package/dist/{dokploy-2p4PK6ej.mjs.map → dokploy-Boj1NSHV.mjs.map} +1 -1
- package/dist/{dokploy-BANMTroy.cjs → dokploy-CiozgdKO.cjs} +2 -2
- package/dist/{dokploy-BANMTroy.cjs.map → dokploy-CiozgdKO.cjs.map} +1 -1
- package/dist/{eas-CNi16SoO.cjs → eas-BmBjIFf_.cjs} +2 -2
- package/dist/{eas-CNi16SoO.cjs.map → eas-BmBjIFf_.cjs.map} +1 -1
- package/dist/{eas-iT2JDHCh.mjs → eas-DCg41jKK.mjs} +2 -2
- package/dist/{eas-iT2JDHCh.mjs.map → eas-DCg41jKK.mjs.map} +1 -1
- package/dist/{encryption-d2lLtkYx.cjs → encryption-CwLWV2ig.cjs} +2 -2
- package/dist/{encryption-d2lLtkYx.cjs.map → encryption-CwLWV2ig.cjs.map} +1 -1
- package/dist/{encryption-DkXVbyZc.mjs → encryption-QOQbGXgV.mjs} +2 -2
- package/dist/{encryption-DkXVbyZc.mjs.map → encryption-QOQbGXgV.mjs.map} +1 -1
- package/dist/{engine-DlEHexED.cjs → engine-D7YNY3v7.cjs} +3 -3
- package/dist/{engine-DlEHexED.cjs.map → engine-D7YNY3v7.cjs.map} +1 -1
- package/dist/{engine-CatKW6E-.mjs → engine-HVqo3mQu.mjs} +3 -3
- package/dist/{engine-CatKW6E-.mjs.map → engine-HVqo3mQu.mjs.map} +1 -1
- package/dist/{github-BAeaC1Pl.cjs → github-CO8W9SDs.cjs} +4 -4
- package/dist/{github-BAeaC1Pl.cjs.map → github-CO8W9SDs.cjs.map} +1 -1
- package/dist/{github-DErsqKGN.mjs → github-CQt3xc3o.mjs} +4 -4
- package/dist/{github-DErsqKGN.mjs.map → github-CQt3xc3o.mjs.map} +1 -1
- package/dist/{googlePlay-BXJ5ZjMK.mjs → googlePlay-Cfu17aEp.mjs} +3 -3
- package/dist/{googlePlay-BXJ5ZjMK.mjs.map → googlePlay-Cfu17aEp.mjs.map} +1 -1
- package/dist/{googlePlay-BVcfFWRO.cjs → googlePlay-DSuF6jbF.cjs} +3 -3
- package/dist/{googlePlay-BVcfFWRO.cjs.map → googlePlay-DSuF6jbF.cjs.map} +1 -1
- package/dist/index.cjs +51 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +51 -51
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-s4QizIoM.mjs → migrate-Bkz8BGaZ.mjs} +2 -2
- package/dist/{migrate-s4QizIoM.mjs.map → migrate-Bkz8BGaZ.mjs.map} +1 -1
- package/dist/{migrate-Bb1MmGHl.mjs → migrate-CGkir5LM.mjs} +2 -2
- package/dist/{migrate-Bb1MmGHl.mjs.map → migrate-CGkir5LM.mjs.map} +1 -1
- package/dist/{migrate-L4a273dY.cjs → migrate-CXsMaHbb.cjs} +2 -2
- package/dist/{migrate-L4a273dY.cjs.map → migrate-CXsMaHbb.cjs.map} +1 -1
- package/dist/{migrate-D79GI0XM.cjs → migrate-dbAI2XF8.cjs} +2 -2
- package/dist/{migrate-D79GI0XM.cjs.map → migrate-dbAI2XF8.cjs.map} +1 -1
- package/dist/{publish-ydscCYsW.cjs → publish-Bbd-2PI2.cjs} +2 -2
- package/dist/{publish-ydscCYsW.cjs.map → publish-Bbd-2PI2.cjs.map} +1 -1
- package/dist/{publish-CcTlmJT6.mjs → publish-eUcWvSdP.mjs} +2 -2
- package/dist/{publish-CcTlmJT6.mjs.map → publish-eUcWvSdP.mjs.map} +1 -1
- package/dist/{resolve-BgL-brF9.cjs → resolve-CyprRcQo.cjs} +3 -3
- package/dist/{resolve-BgL-brF9.cjs.map → resolve-CyprRcQo.cjs.map} +1 -1
- package/dist/{resolve-BLp2trep.mjs → resolve-DSilYpzm.mjs} +3 -3
- package/dist/{resolve-BLp2trep.mjs.map → resolve-DSilYpzm.mjs.map} +1 -1
- package/dist/{rollback-DwqZ8zzj.mjs → rollback-C4ri3h7i.mjs} +4 -4
- package/dist/{rollback-DwqZ8zzj.mjs.map → rollback-C4ri3h7i.mjs.map} +1 -1
- package/dist/{rollback-2cyLXEH1.cjs → rollback-DfwsnwGO.cjs} +4 -4
- package/dist/{rollback-2cyLXEH1.cjs.map → rollback-DfwsnwGO.cjs.map} +1 -1
- package/dist/{seeds-C2w4UqVP.cjs → seeds-CFG-f24B.cjs} +9 -3
- package/dist/{seeds-C2w4UqVP.cjs.map → seeds-CFG-f24B.cjs.map} +1 -1
- package/dist/{seeds-Cwtv-U9-.mjs → seeds-oY0qDqui.mjs} +4 -4
- package/dist/{seeds-Cwtv-U9-.mjs.map → seeds-oY0qDqui.mjs.map} +1 -1
- package/dist/{serverSetup-CKs4K9gD.cjs → serverSetup-CQZt0OT6.cjs} +6 -5
- package/dist/serverSetup-CQZt0OT6.cjs.map +1 -0
- package/dist/{serverSetup-P68H5Lal.mjs → serverSetup-D8nngcNL.mjs} +6 -5
- package/dist/serverSetup-D8nngcNL.mjs.map +1 -0
- package/dist/{sniffer-CF7wNxMo.cjs → sniffer-BAmtXzou.cjs} +6 -6
- package/dist/{sniffer-CF7wNxMo.cjs.map → sniffer-BAmtXzou.cjs.map} +1 -1
- package/dist/{sniffer-8C4q6Szd.mjs → sniffer-Cgx608Tl.mjs} +6 -6
- package/dist/{sniffer-8C4q6Szd.mjs.map → sniffer-Cgx608Tl.mjs.map} +1 -1
- package/dist/{stage-BvM_u2z4.mjs → stage-BKDdQm65.mjs} +4 -4
- package/dist/{stage-BvM_u2z4.mjs.map → stage-BKDdQm65.mjs.map} +1 -1
- package/dist/{stage-DEQJGW3r.cjs → stage-CJysBXWB.cjs} +4 -4
- package/dist/{stage-DEQJGW3r.cjs.map → stage-CJysBXWB.cjs.map} +1 -1
- package/dist/target/public.cjs +1 -1
- package/dist/target/public.mjs +1 -1
- package/package.json +15 -15
- package/dist/compose-Mdgea4Yt.cjs.map +0 -1
- package/dist/dns-BIYf20p7.cjs.map +0 -1
- package/dist/dns-agf2VCdV.mjs.map +0 -1
- package/dist/serverSetup-CKs4K9gD.cjs.map +0 -1
- package/dist/serverSetup-P68H5Lal.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dns-BIYf20p7.cjs","names":["checkStageBackups","kebabCase","GkmError","postgresRoleSlug","dirname","fileURLToPath","join","existsSync","readFileSync","createRequire","DEFAULT_POSTGRES_VERSION","postgresImage","createHash","localRolePassword","quoteIdentifier","SERVER_IPV4_KEY","GkmError","SERVER_IPV6_KEY","isIPv6","isIPv4","normalizeDnsConfig","extractSubdomain","findRootDomain","cnameTarget","createDnsProvider","DnsRecordsUnreadable","dnsLookup"],"sources":["../src/providers/dns.ts","../src/backups/config.ts","../src/backups/service.ts","../src/compose/dns.ts"],"sourcesContent":["/**\n * What the deploy's DNS step needs to know about a workspace before its\n * stack is run: whether it deploys with compose.\n *\n * The step itself is `stageDns` in `compose/dns.ts`, run by every deploy\n * (`gkm deploy`, and `gkm compose`, the same run) where the deploy runs — a\n * CI runner or a developer's machine, which hold the DNS provider's token.\n * The server never does.\n */\n\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\n/** Whether the workspace deploys through the compose target. */\nexport function deploysWithCompose(workspace: NormalizedWorkspace): boolean {\n\treturn (\n\t\tworkspace.deploy?.default === 'compose' ||\n\t\tObject.values(workspace.apps).some(\n\t\t\t(app) => app.resolvedDeployTarget === 'compose',\n\t\t)\n\t);\n}\n","/**\n * Which stages take backups, and where they are kept.\n *\n * A deployed compose stage whose stack runs Postgres is backed up by\n * default — every day at 02:00 UTC, kept 30 days — unless\n * `deploy.backups.<stage>` says otherwise. The local stage never is.\n *\n * Every backup is kept in the project bucket (`providers/projectBucket.ts`),\n * beside the stage's deploy state, one folder per run and one file per\n * database:\n *\n * ```\n * gkm/<project>/<stage>/backups/2026-10-10/02-00-00Z/database.sql.gz\n * gkm/<project>/<stage>/backups/2026-10-10/02-00-00Z/auth-database.sql.gz\n * ```\n *\n * The file is named after the database's construct, the way its migrations\n * folder is: `AuthDatabase` is `db/auth-database`, and `auth-database.sql.gz`.\n */\n\nimport { type ConstructManifest, kebabCase } from '@geekmidas/manifest';\nimport { deploysWithCompose } from '../providers/dns.js';\nimport type { PrefixExpiry } from '../providers/projectBucket.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport { checkStageBackups, type StageBackups } from './schedule.js';\n\n/** What a stage's backups come to. */\nexport type StageBackupsChoice =\n\t| {\n\t\t\tmode: 'on';\n\t\t\tbackups: StageBackups;\n\t\t\t/** Whether the stage named it, or took the default. */\n\t\t\tsource: 'config' | 'default';\n\t }\n\t/** `deploy.backups.<stage>: false`. */\n\t| { mode: 'disabled' }\n\t/** Nothing to back up, or nowhere it applies. */\n\t| { mode: 'none'; reason: 'local' | 'not-compose' | 'no-postgres' };\n\n/** The workspace parts {@link stageBackups} reads. */\nexport type BackupsWorkspace = Pick<\n\tNormalizedWorkspace,\n\t'name' | 'apps' | 'deploy' | 'stages' | 'state'\n>;\n\n/** Whether the stack runs Postgres: some construct is a database. */\nexport function runsPostgres(manifest: ConstructManifest): boolean {\n\treturn Object.values(manifest).some((d) => d.kind === 'database');\n}\n\n/**\n * What `stage`'s backups are: on — by its entry or the default — off by\n * `false`, or none, for the local stage, a stage compose does not deploy,\n * and a stack with no database.\n */\nexport function stageBackups(\n\tworkspace: BackupsWorkspace,\n\tmanifest: ConstructManifest,\n\tstage: string,\n): StageBackupsChoice {\n\tif (stage === workspace.stages?.local) {\n\t\treturn { mode: 'none', reason: 'local' };\n\t}\n\tif (!deploysWithCompose(workspace as NormalizedWorkspace)) {\n\t\treturn { mode: 'none', reason: 'not-compose' };\n\t}\n\tif (!runsPostgres(manifest)) return { mode: 'none', reason: 'no-postgres' };\n\tconst value = (\n\t\tworkspace.deploy?.backups as Record<string, unknown> | undefined\n\t)?.[stage];\n\tconst checked = checkStageBackups(stage, value ?? {});\n\tif (checked === false) return { mode: 'disabled' };\n\treturn {\n\t\tmode: 'on',\n\t\tbackups: checked,\n\t\tsource: value === undefined ? 'default' : 'config',\n\t};\n}\n\n/**\n * Where a stage's backups are, in the project bucket — under the same\n * `<prefix>/<project>/<stage>` its deploy state is: `gkm/shop/production/backups`.\n */\nexport function backupsPrefix(\n\tworkspace: Pick<NormalizedWorkspace, 'name' | 'state'>,\n\tstage: string,\n): string {\n\tconst state = workspace.state;\n\tconst prefix =\n\t\tstate?.provider === 's3' && state.prefix !== undefined\n\t\t\t? state.prefix.replace(/\\/+$/, '')\n\t\t\t: 'gkm';\n\treturn `${prefix ? `${prefix}/` : ''}${workspace.name}/${stage}/backups`;\n}\n\n/** One run's folder: `2026-10-10/02-00-00Z`, for the time it started. */\nexport function runFolder(at: Date): string {\n\tconst iso = at.toISOString();\n\treturn `${iso.slice(0, 10)}/${iso.slice(11, 19).replace(/:/g, '-')}Z`;\n}\n\n/** A run folder's time back, or `undefined` for a key that is not one. */\nexport function runTime(folder: string): Date | undefined {\n\tconst match = /^(\\d{4}-\\d{2}-\\d{2})\\/(\\d{2})-(\\d{2})-(\\d{2})Z$/.exec(folder);\n\tif (!match) return undefined;\n\tconst at = new Date(`${match[1]}T${match[2]}:${match[3]}:${match[4]}Z`);\n\treturn Number.isNaN(at.getTime()) ? undefined : at;\n}\n\n/** One database the stack backs up. */\nexport interface BackupDatabase {\n\t/** Its construct: `AuthDatabase`. */\n\tid: string;\n\t/** Its file in a run's folder, less `.sql.gz`: `auth-database`. */\n\tfile: string;\n\t/** Its name in the stack's Postgres: `auth_database_production`. */\n\tname: string;\n}\n\n/** A database's file in a run's folder: `auth-database.sql.gz`. */\nexport function backupFile(database: Pick<BackupDatabase, 'file'>): string {\n\treturn `${database.file}.sql.gz`;\n}\n\n/** Each database the stack runs, by its construct and its name in Postgres. */\nexport function backupDatabases(\n\tresources: readonly { id: string; kind: string; name: string }[],\n): BackupDatabase[] {\n\treturn resources\n\t\t.filter((r) => r.kind === 'database')\n\t\t.map((r) => ({ id: r.id, file: kebabCase(r.id), name: r.name }))\n\t\t.sort((a, b) => a.file.localeCompare(b.file));\n}\n\n/**\n * The expiry of every stage's backups, by each stage's own entry — what the\n * project bucket's lifecycle carries beside its own rule.\n *\n * A put replaces a bucket's whole lifecycle, so this is built from every\n * deployed stage each time, never from the stage being deployed alone: one\n * stage's deploy keeps every other's rule. A stage set to `false` has none,\n * and its backups are kept until someone deletes them.\n */\nexport function backupExpiries(\n\tworkspace: BackupsWorkspace,\n\tmanifest: ConstructManifest,\n): PrefixExpiry[] {\n\tconst expiries: PrefixExpiry[] = [];\n\tfor (const stage of [...(workspace.stages?.deployed ?? [])].sort()) {\n\t\tconst choice = stageBackups(workspace, manifest, stage);\n\t\tif (choice.mode !== 'on') continue;\n\t\texpiries.push({\n\t\t\tid: `gkm-backups-${stage}`,\n\t\t\tprefix: `${backupsPrefix(workspace, stage)}/`,\n\t\t\tdays: choice.backups.keepDays,\n\t\t\tabortIncompleteDays: 1,\n\t\t});\n\t}\n\treturn expiries;\n}\n","/**\n * The stack's `backups` service: what a deployed compose stage with Postgres\n * runs beside its apps, unless `deploy.backups.<stage>` is `false`.\n *\n * - **The image** is built from the stack's own Postgres image —\n * `postgres:<major>-alpine`, already on the server for the stack, and with\n * the `pg_dump` that matches the server — with Node added and gkm's runner\n * (`runner.ts`) copied in. Nothing is pulled that the stack does not have,\n * and nothing is mounted from the host: the build context is two files.\n * - **Its key** is `BACKUPS_URL`, from the stage's secrets, like any app's:\n * allowed to put objects under the stage's backups prefix and nothing else.\n * The server holds no AWS credentials of its own.\n * - **Its login** is a role of its own, `<project>_backups`, granted\n * `pg_read_all_data` — it can read every database and write none. Postgres\n * before 14 has no such role, and there it signs in as the stack's\n * superuser.\n * - **Its health** is the last good run: unhealthy once that is more than\n * twice the schedule's longest gap old, after a startup grace.\n */\n\nimport { createHash } from 'node:crypto';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { DEFAULT_POSTGRES_VERSION } from '@geekmidas/manifest';\nimport { GkmError } from '../errors';\nimport { postgresImage } from '../reconcile/containers.js';\nimport { localRolePassword } from '../reconcile/env.js';\nimport { postgresRoleSlug } from '../reconcile/localCredentials.js';\nimport type { Plan } from '../reconcile/plan.js';\nimport { quoteIdentifier, type Statement } from '../reconcile/provision.js';\nimport {\n\ttype BackupDatabase,\n\ttype BackupsWorkspace,\n\tbackupDatabases,\n\tbackupsPrefix,\n} from './config.js';\nimport type { StageBackups } from './schedule.js';\n\n/** The service's name in the stack. */\nexport const BACKUPS_SERVICE = 'backups';\n\n/** The secret its key is written to. */\nexport const BACKUPS_URL_KEY = 'BACKUPS_URL';\n\n/** Where the runner is, in the image. */\nexport const RUNNER_PATH = '/gkm/backup.mjs';\n\n/** The first Postgres with `pg_read_all_data`. */\nconst READ_ALL_DATA_SINCE = 14;\n\n/** A stage that backs up, and no key to back up with. */\nexport class BackupsUrlMissing extends GkmError {\n\tconstructor(readonly stage: string) {\n\t\tsuper(\n\t\t\t`'${stage}' runs Postgres, so its stack backs it up, and its secrets ` +\n\t\t\t\t`hold no ${BACKUPS_URL_KEY} to back up with. The deploy writes it — ` +\n\t\t\t\t\"the IAM user and key it creates in the stage's AWS account — so \" +\n\t\t\t\t'run the deploy with the ' +\n\t\t\t\t\"account's credentials. To take no backups on this stage, set \" +\n\t\t\t\t`deploy.backups.${stage}: false.`,\n\t\t);\n\t\tthis.name = 'BackupsUrlMissing';\n\t}\n}\n\n/** The backups role: `<project>_backups`, as the superuser is `<project>_admin`. */\nexport function backupsRole(project: string): string {\n\treturn `${postgresRoleSlug(project)}_backups`;\n}\n\n/** The stack's backups, as its compose file and env file have them. */\nexport interface StackBackups {\n\tbackups: StageBackups;\n\tdatabases: BackupDatabase[];\n\t/** `gkm/<project>/<stage>/backups`. */\n\tprefix: string;\n\t/** The Postgres image it is built from. */\n\tfrom: string;\n\t/** `<project>-backups:pg18-<hash>`: built on the server, never pushed. */\n\timage: string;\n\t/** Its build context: the Dockerfile and the runner. */\n\tfiles: { Dockerfile: string; 'backup.mjs': string };\n\t/** What it signs in to Postgres as. */\n\tlogin: { user: string; password: string };\n\t/** Whether that is its own read-only role (else the superuser). */\n\treadOnly: boolean;\n\t/** Its environment — empty in a stack that only builds. */\n\tenv: Record<string, string>;\n}\n\n/**\n * The runner's source, as JavaScript: the built `backups/runner.mjs` beside\n * the CLI's bundle, or — run from source — `runner.ts`, with its types\n * stripped by esbuild.\n */\nexport function runnerSource(): string {\n\tconst here = dirname(fileURLToPath(import.meta.url));\n\tfor (const path of [\n\t\tjoin(here, 'backups', 'runner.mjs'),\n\t\tjoin(here, 'runner.mjs'),\n\t\tjoin(here, '..', 'backups', 'runner.mjs'),\n\t]) {\n\t\tif (existsSync(path)) return readFileSync(path, 'utf-8');\n\t}\n\tconst { transformSync } = createRequire(import.meta.url)(\n\t\t'esbuild',\n\t) as typeof import('esbuild');\n\treturn transformSync(readFileSync(join(here, 'runner.ts'), 'utf-8'), {\n\t\tloader: 'ts',\n\t\tformat: 'esm',\n\t\ttarget: 'node22',\n\t}).code;\n}\n\n/** The image's Dockerfile: the stack's Postgres, Node, and the runner. */\nexport function backupsDockerfile(from: string): string {\n\treturn `# Generated by gkm — the stage's backups: pg_dump from ${from}, and gkm's uploader.\nFROM ${from}\nRUN apk add --no-cache nodejs\nCOPY backup.mjs ${RUNNER_PATH}\nUSER postgres\nENTRYPOINT []\nCMD [\"node\", \"${RUNNER_PATH}\", \"serve\"]\n`;\n}\n\nexport interface StackBackupsInput {\n\tworkspace: BackupsWorkspace;\n\tstage: string;\n\tproject: string;\n\tplan: Plan;\n\tbackups: StageBackups;\n\t/** The stage's secrets, by key. */\n\tcustom: Readonly<Record<string, string>>;\n\tseed: string;\n\t/** The stack's superuser. */\n\tsuperuser: { user: string; password: string };\n\t/** What the stage's telemetry hands a process, when it is on. */\n\ttelemetry?: Readonly<Record<string, string>>;\n\t/** The stack only builds: no environment, no key needed. */\n\tbuildOnly?: boolean;\n}\n\n/**\n * The stack's backups service, for a stage whose backups are on.\n *\n * @throws {BackupsUrlMissing} when the stage's secrets hold no key\n */\nexport function stackBackups(input: StackBackupsInput): StackBackups {\n\tconst { plan, stage } = input;\n\tconst version =\n\t\tplan.resources.find((r) => r.version)?.version ?? DEFAULT_POSTGRES_VERSION;\n\tconst from = postgresImage(version);\n\tconst script = runnerSource();\n\tconst dockerfile = backupsDockerfile(from);\n\tconst hash = createHash('sha256')\n\t\t.update(dockerfile)\n\t\t.update(script)\n\t\t.digest('hex')\n\t\t.slice(0, 12);\n\n\tconst readOnly = version >= READ_ALL_DATA_SINCE;\n\tconst role = backupsRole(input.workspace.name);\n\tconst login = readOnly\n\t\t? {\n\t\t\t\tuser: role,\n\t\t\t\tpassword: localRolePassword(\n\t\t\t\t\tinput.workspace.name,\n\t\t\t\t\tplan,\n\t\t\t\t\trole,\n\t\t\t\t\tinput.seed,\n\t\t\t\t),\n\t\t\t}\n\t\t: input.superuser;\n\n\tconst databases = backupDatabases(plan.resources);\n\tconst prefix = backupsPrefix(input.workspace, stage);\n\tconst { kind, ...schedule } = input.backups.schedule;\n\n\tlet env: Record<string, string> = {};\n\tif (!input.buildOnly) {\n\t\tconst url = input.custom[BACKUPS_URL_KEY];\n\t\tif (!url) throw new BackupsUrlMissing(stage);\n\t\tenv = {\n\t\t\tBACKUPS_URL: url,\n\t\t\tBACKUPS_PREFIX: prefix,\n\t\t\tBACKUPS_DATABASES: JSON.stringify(\n\t\t\t\tdatabases.map(({ file, name }) => ({ file, name })),\n\t\t\t),\n\t\t\tBACKUPS_SCHEDULE: JSON.stringify(\n\t\t\t\tkind === 'cron'\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\tminutes: input.backups.schedule.minutes,\n\t\t\t\t\t\t\thours: input.backups.schedule.hours,\n\t\t\t\t\t\t\tdays: input.backups.schedule.days,\n\t\t\t\t\t\t\tmonths: input.backups.schedule.months,\n\t\t\t\t\t\t\tweekdays: input.backups.schedule.weekdays,\n\t\t\t\t\t\t}\n\t\t\t\t\t: { kind, ...schedule },\n\t\t\t),\n\t\t\tBACKUPS_MAX_GAP_SECONDS: String(input.backups.maxGapSeconds),\n\t\t\tPGHOST: 'postgres',\n\t\t\tPGPORT: '5432',\n\t\t\tPGUSER: login.user,\n\t\t\tPGPASSWORD: login.password,\n\t\t\t...(input.telemetry?.OTEL_EXPORTER_OTLP_ENDPOINT\n\t\t\t\t? {\n\t\t\t\t\t\tOTEL_EXPORTER_OTLP_ENDPOINT:\n\t\t\t\t\t\t\tinput.telemetry.OTEL_EXPORTER_OTLP_ENDPOINT,\n\t\t\t\t\t\t...(input.telemetry.OTEL_EXPORTER_OTLP_HEADERS\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tOTEL_EXPORTER_OTLP_HEADERS:\n\t\t\t\t\t\t\t\t\t\tinput.telemetry.OTEL_EXPORTER_OTLP_HEADERS,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\tOTEL_SERVICE_NAME: BACKUPS_SERVICE,\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t};\n\t}\n\n\treturn {\n\t\tbackups: input.backups,\n\t\tdatabases,\n\t\tprefix,\n\t\tfrom,\n\t\timage: `${input.project}-backups:pg${version}-${hash}`,\n\t\tfiles: { Dockerfile: dockerfile, 'backup.mjs': script },\n\t\tlogin,\n\t\treadOnly,\n\t\tenv,\n\t};\n}\n\n/** The service in the stack's compose file. */\nexport function backupsService(backups: StackBackups, envFiles: boolean) {\n\treturn {\n\t\timage: backups.image,\n\t\t// Built on the server from two files; there is nothing to pull.\n\t\tbuild: { context: `./${BACKUPS_SERVICE}`, dockerfile: 'Dockerfile' },\n\t\tpull_policy: 'build' as const,\n\t\trestart: 'unless-stopped',\n\t\t...(envFiles\n\t\t\t? {\n\t\t\t\t\tenv_file: [\n\t\t\t\t\t\t{ path: `./${BACKUPS_SERVICE}.env`, format: 'raw' as const },\n\t\t\t\t\t],\n\t\t\t\t}\n\t\t\t: {}),\n\t\tdepends_on: { postgres: { condition: 'service_healthy' as const } },\n\t\thealthcheck: {\n\t\t\ttest: ['CMD', 'node', RUNNER_PATH, 'health'],\n\t\t\tinterval: '1m',\n\t\t\ttimeout: '10s',\n\t\t\tretries: 3,\n\t\t\tstart_period: '30s',\n\t\t},\n\t};\n}\n\n/**\n * The backups role, created and kept: able to sign in and read every\n * database, and nothing else. None when it signs in as the superuser.\n */\nexport function backupsRoleStatements(backups: StackBackups): Statement[] {\n\tif (!backups.readOnly) return [];\n\tconst role = quoteIdentifier(backups.login.user);\n\tconst password = `'${backups.login.password.replace(/'/g, \"''\")}'`;\n\treturn [\n\t\t{\n\t\t\tid: BACKUPS_SERVICE,\n\t\t\tdescribe: `role ${backups.login.user}`,\n\t\t\texists: {\n\t\t\t\tsql: 'SELECT 1 FROM pg_roles WHERE rolname = $1',\n\t\t\t\tvalues: [backups.login.user],\n\t\t\t},\n\t\t\tcreate: `CREATE ROLE ${role} LOGIN PASSWORD ${password}`,\n\t\t},\n\t\t{\n\t\t\tid: BACKUPS_SERVICE,\n\t\t\tdescribe: `role ${backups.login.user}: its password`,\n\t\t\tcreate: `ALTER ROLE ${role} WITH LOGIN PASSWORD ${password}`,\n\t\t},\n\t\t{\n\t\t\tid: BACKUPS_SERVICE,\n\t\t\tdescribe: `role ${backups.login.user}: read every database`,\n\t\t\tcreate: `GRANT pg_read_all_data TO ${role}`,\n\t\t},\n\t];\n}\n","/**\n * A compose stage's DNS: the records its public hosts need, and the check that\n * they resolve to its server before a certificate is asked for.\n *\n * - **The server** (`serverAddress`): the stage's own secrets hold its public\n * addresses — `GKM_SERVER_IPV4`, and `GKM_SERVER_IPV6` where it has one.\n * Reserved for gkm: never handed to an app.\n * - **The plan** (`planStackDns`, pure): every public host the stack serves —\n * its apex, each app, each file server, the public log UI — pointed at the\n * server. An A record per host (and an AAAA with an IPv6 address), or, where\n * the domain's `dns` entry says `records: { mode: 'cname', target }`, one A\n * record for the target and a CNAME to it for every other host but the\n * apex, which DNS forbids a CNAME at.\n * - **Applying it** (`applyStackDns`): through the domain's provider in `dns`,\n * a record set at a time. A record with the right value is left alone; one\n * with another value is replaced and the change printed. A `manual` domain\n * is printed and nothing else. A dry run reads, prints and writes nothing.\n * - **The check** (`checkStackDns`): each host resolved with the system\n * resolver. One that is not the server stops the deploy with\n * {@link HostNotPointingAtServer}, before Caddy or Traefik ask Let's Encrypt\n * for a certificate it cannot get — and use up the rate limit trying.\n * - **The step** (`stageDns`): all three, as every deploy runs them — the plan applied through each provider,\n * what was written read back from the provider, and only the hosts no\n * provider wrote resolved publicly. A brand-new name is never looked up\n * before its record exists, so no resolver caches it as missing.\n */\n\nimport { lookup as dnsLookup } from 'node:dns/promises';\nimport { isIPv4, isIPv6 } from 'node:net';\nimport type {\n\tCredentialProvider,\n\tGoDaddyCredential,\n\tHostingerCredential,\n} from '../deploy/credentials';\nimport {\n\tcreateDnsProvider,\n\ttype DnsProvider,\n\ttype DnsRecord,\n\tDnsRecordsUnreadable,\n\ttype DnsConfig as SchemaDnsConfig,\n} from '../target/dokploy/dns/DnsProvider';\nimport type { GoDaddyProviderOptions } from '../target/dokploy/dns/GoDaddyProvider';\nimport {\n\textractSubdomain,\n\tfindRootDomain,\n\tnormalizeDnsConfig,\n} from '../target/dokploy/dns/index';\nimport type {\n\tDnsProvider as DnsProviderConfig,\n\tWorkspaceDnsConfig,\n} from '../workspace/types';\nimport {\n\tcnameTarget,\n\ttype DnsRecordsMode,\n\tisReservedStageKey,\n\tSERVER_IPV4_KEY,\n\tSERVER_IPV6_KEY,\n} from './dnsConfig';\n\nexport { isReservedStageKey, SERVER_IPV4_KEY, SERVER_IPV6_KEY };\n\nimport { GkmError } from '../errors';\nimport type { ComposeStack } from './stack';\n\n/** A deployed stage's server: what its hosts' records point at. */\nexport interface ServerAddress {\n\tipv4: string;\n\tipv6?: string;\n}\n\n/** The line that says how to set the server's address. */\nexport function serverAddressHint(stage: string): string {\n\treturn `gkm secrets:set ${SERVER_IPV4_KEY} '<ip>' --stage ${stage}`;\n}\n\n/** A server address in the stage's secrets that is not an address. */\nexport class ServerAddressInvalid extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly key: string,\n\t\treadonly value: string,\n\t) {\n\t\tconst ipv4 = key === SERVER_IPV4_KEY;\n\t\tsuper(\n\t\t\t`${key} in the '${stage}' stage's secrets is ${JSON.stringify(value)}, ` +\n\t\t\t\t`which is not an ${ipv4 ? 'IPv4 address (like 203.0.113.10)' : 'IPv6 address (like 2001:db8::10)'}. ` +\n\t\t\t\t`Set the server's public address: gkm secrets:set ${key} '<ip>' --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'ServerAddressInvalid';\n\t}\n}\n\n/**\n * A compose stage that serves real domains with no server address: its hosts\n * cannot be pointed or checked, and a certificate would be asked for blind.\n */\nexport class ServerAddressMissing extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly domain: string,\n\t) {\n\t\tsuper(\n\t\t\t`The '${stage}' stage serves ${domain} and its secrets hold no ` +\n\t\t\t\t`${SERVER_IPV4_KEY} — the public address of the server its stack runs ` +\n\t\t\t\t\"on, which its hosts' DNS records point at and every deploy checks \" +\n\t\t\t\t`before asking for certificates. Set it: ${serverAddressHint(stage)} ` +\n\t\t\t\t`(and ${SERVER_IPV6_KEY} for an IPv6 address).`,\n\t\t);\n\t\tthis.name = 'ServerAddressMissing';\n\t}\n}\n\n/**\n * The server's address from the stage's secrets, checked — `undefined` when\n * `GKM_SERVER_IPV4` is not set.\n *\n * @throws {ServerAddressInvalid} for a value that is not an address\n */\nexport function serverAddress(\n\tstage: string,\n\tcustom: Readonly<Record<string, string>> | undefined,\n): ServerAddress | undefined {\n\tconst ipv4 = custom?.[SERVER_IPV4_KEY]?.trim();\n\tconst ipv6 = custom?.[SERVER_IPV6_KEY]?.trim();\n\tif (ipv6 && !isIPv6(ipv6)) {\n\t\tthrow new ServerAddressInvalid(stage, SERVER_IPV6_KEY, ipv6);\n\t}\n\tif (!ipv4) return undefined;\n\tif (!isIPv4(ipv4))\n\t\tthrow new ServerAddressInvalid(stage, SERVER_IPV4_KEY, ipv4);\n\treturn { ipv4, ...(ipv6 ? { ipv6 } : {}) };\n}\n\n/**\n * A name that resolves to this machine by definition — `localhost` and every\n * `*.localhost` — and is in nobody's DNS.\n */\nexport function isLocalHost(host: string): boolean {\n\tconst name = host.toLowerCase().replace(/\\.$/, '');\n\treturn name === 'localhost' || name.endsWith('.localhost');\n}\n\n/**\n * The server's address, required where the stage serves real domains — not\n * `*.localhost` ones, which point at this machine already.\n *\n * @throws {ServerAddressMissing} when the stage has a domain and no address\n * @throws {ServerAddressInvalid} for a value that is not an address\n */\nexport function requiredServerAddress(\n\tstage: string,\n\tdomain: string | undefined,\n\tcustom: Readonly<Record<string, string>> | undefined,\n): ServerAddress | undefined {\n\tconst server = serverAddress(stage, custom);\n\tif (!server && domain && !isLocalHost(domain)) {\n\t\tthrow new ServerAddressMissing(stage, domain);\n\t}\n\treturn server;\n}\n\n/** The record types the DNS step writes. */\nexport type StackRecordType = 'A' | 'AAAA' | 'CNAME';\n\nconst MANAGED: readonly StackRecordType[] = ['A', 'AAAA', 'CNAME'];\n\n/** One record a stack's host needs. */\nexport interface PlannedDnsRecord {\n\t/** The full name: `api.example.com`. */\n\thost: string;\n\t/** The root domain in `dns` it is under. */\n\tdomain: string;\n\t/** Relative to the domain, as providers take it: `api`, `@`. */\n\tname: string;\n\ttype: StackRecordType;\n\tvalue: string;\n\tttl: number;\n}\n\n/** One `dns` domain's share of the plan. */\nexport interface DomainDnsPlan {\n\tdomain: string;\n\t/** `godaddy`, `route53`, `manual`, … — `custom` for an object. */\n\tprovider: string;\n\tconfig: DnsProviderConfig;\n\trecords: PlannedDnsRecord[];\n}\n\nexport interface StackDnsPlan {\n\tstage: string;\n\tserver: ServerAddress;\n\tdomains: DomainDnsPlan[];\n\t/** Hosts under no `dns` domain: theirs to point by hand. */\n\tuncovered: string[];\n}\n\n/** Every public host the stack serves, once each, sorted. */\nexport function stackHosts(stack: Pick<ComposeStack, 'routes'>): string[] {\n\treturn [...new Set(stack.routes.map((route) => route.host))].sort();\n}\n\n/** The TTL a domain's records are written with. */\nexport function recordTtl(config: DnsProviderConfig): number {\n\tconst ttl = 'ttl' in config ? config.ttl : undefined;\n\tif (ttl) return ttl;\n\treturn config.provider === 'godaddy' ? 600 : 300;\n}\n\nfunction providerName(config: DnsProviderConfig): string {\n\treturn typeof config.provider === 'string' ? config.provider : 'custom';\n}\n\nexport interface PlanStackDnsInput {\n\tstage: string;\n\thosts: readonly string[];\n\tserver: ServerAddress;\n\tdns: WorkspaceDnsConfig | undefined;\n}\n\n/**\n * A wildcard host: gkm writes one record per public host, so a stage on\n * another server — or a name someone else points elsewhere — is never caught\n * by a record it did not ask for.\n */\nexport class DnsWildcardRefused extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly host: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' would point ${host} at its server, and gkm never writes a ` +\n\t\t\t\t'wildcard record: each public host gets its own. Give the app a ' +\n\t\t\t\t'subdomain of its own instead.',\n\t\t);\n\t\tthis.name = 'DnsWildcardRefused';\n\t}\n}\n\n/** The records a stage's hosts need — pure. */\nexport function planStackDns(input: PlanStackDnsInput): StackDnsPlan {\n\tconst { stage, server } = input;\n\tconst dns = input.dns ? normalizeDnsConfig(input.dns) : {};\n\tconst records = new Map<string, PlannedDnsRecord>();\n\tconst uncovered: string[] = [];\n\n\tconst add = (\n\t\thost: string,\n\t\tdomain: string,\n\t\ttype: StackRecordType,\n\t\tvalue: string,\n\t) => {\n\t\tconst name = extractSubdomain(host, domain);\n\t\trecords.set(`${domain} ${name} ${type}`, {\n\t\t\thost,\n\t\t\tdomain,\n\t\t\tname,\n\t\t\ttype,\n\t\t\tvalue,\n\t\t\tttl: recordTtl(dns[domain]!),\n\t\t});\n\t};\n\tconst pointAt = (host: string, domain: string) => {\n\t\tadd(host, domain, 'A', server.ipv4);\n\t\tif (server.ipv6) add(host, domain, 'AAAA', server.ipv6);\n\t};\n\n\tfor (const raw of input.hosts) {\n\t\tconst host = raw.toLowerCase();\n\t\tif (host.includes('*')) throw new DnsWildcardRefused(stage, host);\n\t\tconst domain = findRootDomain(host, dns);\n\t\tif (!domain) {\n\t\t\tuncovered.push(host);\n\t\t\tcontinue;\n\t\t}\n\t\t// A target under the domain itself — the schema refuses one that is\n\t\t// not — gets the A record every other host's CNAME points at.\n\t\tconst target = cnameTarget(\n\t\t\t(dns[domain] as { records?: DnsRecordsMode }).records,\n\t\t\tstage,\n\t\t);\n\t\tif (target) pointAt(target, domain);\n\t\t// The apex can hold no CNAME — it has the zone's SOA and NS — so it\n\t\t// is always an A record.\n\t\tif (target && host !== domain && host !== target) {\n\t\t\tadd(host, domain, 'CNAME', target);\n\t\t} else {\n\t\t\tpointAt(host, domain);\n\t\t}\n\t}\n\n\tconst domains = new Map<string, DomainDnsPlan>();\n\tfor (const record of [...records.values()].sort(\n\t\t(a, b) =>\n\t\t\ta.domain.localeCompare(b.domain) ||\n\t\t\ta.name.localeCompare(b.name) ||\n\t\t\ta.type.localeCompare(b.type),\n\t)) {\n\t\tconst config = dns[record.domain]!;\n\t\tconst plan = domains.get(record.domain) ?? {\n\t\t\tdomain: record.domain,\n\t\t\tprovider: providerName(config),\n\t\t\tconfig,\n\t\t\trecords: [],\n\t\t};\n\t\tplan.records.push(record);\n\t\tdomains.set(record.domain, plan);\n\t}\n\n\treturn {\n\t\tstage,\n\t\tserver,\n\t\tdomains: [...domains.values()],\n\t\tuncovered: uncovered.sort(),\n\t};\n}\n\n/** One change applying a plan makes — or, in a dry run, would. */\nexport interface DnsChange {\n\tdomain: string;\n\tname: string;\n\ttype: StackRecordType;\n\t/**\n\t * `write`: the provider's key cannot read records, so the record is\n\t * written without knowing what is there — idempotent, and not diffed.\n\t */\n\taction: 'create' | 'update' | 'delete' | 'unchanged' | 'write' | 'manual';\n\t/** The value it is set to — absent for a delete. */\n\tvalue?: string;\n\t/** What it was, for an update or a delete. */\n\tprevious?: string;\n\tttl?: number;\n}\n\nexport interface ApplyStackDnsOptions {\n\tdryRun?: boolean;\n\tlog?: (line: string) => void;\n\t/** Where the GoDaddy provider reads its key: the environment and the CLI's home. */\n\tcredentials?: GoDaddyProviderOptions;\n\t/** The Hostinger token, when the caller already has it. */\n\thostinger?: HostingerCredential;\n\t/** The provider for a domain — `createDnsProvider` by default. */\n\tproviderFor?: (config: DnsProviderConfig) => Promise<DnsProvider | null>;\n\t/**\n\t * Read every record written back from its provider, and throw\n\t * {@link DnsRecordsNotConfirmed} for one that does not hold its value.\n\t */\n\tconfirm?: boolean;\n}\n\n/** What applying a plan did, and what of it the providers confirmed. */\nexport interface StackDnsApplied {\n\t/** Every change, the unchanged included; a dry run makes none of them. */\n\tchanges: DnsChange[];\n\t/**\n\t * Hosts whose every record the provider holds with its value — read\n\t * before the run (unchanged) or after it wrote them (`confirm`).\n\t */\n\tconfirmed: string[];\n\t/**\n\t * Hosts no provider vouches for: a `manual` domain's, those under no\n\t * domain in `dns`, and those of a key that can write and not read.\n\t */\n\tunconfirmed: string[];\n}\n\n/** One record a provider did not hold with its value after it was written. */\nexport interface UnconfirmedRecord {\n\thost: string;\n\ttype: StackRecordType;\n\texpected: string;\n\t/** What the provider read back — empty when the record is not there. */\n\tfound: string[];\n}\n\n/** Records a provider accepted and then did not read back with their value. */\nexport class DnsRecordsNotConfirmed extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly domain: string,\n\t\treadonly provider: string,\n\t\treadonly records: readonly UnconfirmedRecord[],\n\t) {\n\t\tconst lines = records.map(\n\t\t\t(r) =>\n\t\t\t\t` - ${r.host} ${r.type}: ${r.found.length > 0 ? r.found.join(', ') : 'nothing'}, expected ${r.expected}`,\n\t\t);\n\t\tsuper(\n\t\t\t`${provider} accepted '${stage}''s records for ${domain}, and reading them ` +\n\t\t\t\t`back does not show them:\\n${lines.join('\\n')}\\n` +\n\t\t\t\t\"Deploy again; if it persists, check the records in the provider's \" +\n\t\t\t\t'console — another tool may be writing the same names.',\n\t\t);\n\t\tthis.name = 'DnsRecordsNotConfirmed';\n\t}\n}\n\n/** `api.example.com A 203.0.113.10 (TTL 600)` */\nfunction describe(record: PlannedDnsRecord): string {\n\treturn `${record.host.padEnd(32)} ${record.type.padEnd(5)} ${record.value} (TTL ${record.ttl})`;\n}\n\nfunction printManual(\n\tplan: DomainDnsPlan,\n\tlog: (line: string) => void,\n\twhy: string,\n): DnsChange[] {\n\tlog(` ${plan.domain}: ${why} — create these records at its DNS host:`);\n\tfor (const record of plan.records) log(` ${describe(record)}`);\n\treturn plan.records.map((r) => ({\n\t\tdomain: r.domain,\n\t\tname: r.name,\n\t\ttype: r.type,\n\t\taction: 'manual',\n\t\tvalue: r.value,\n\t\tttl: r.ttl,\n\t}));\n}\n\n/** The existing records of every planned name, for the types gkm manages. */\nasync function existingRecords(\n\tprovider: DnsProvider,\n\tplan: DomainDnsPlan,\n): Promise<DnsRecord[]> {\n\tconst names = [...new Set(plan.records.map((r) => r.name))];\n\tconst wanted = names.flatMap((name) =>\n\t\tMANAGED.map((type) => ({ name, type })),\n\t);\n\tif (provider.readRecords) return provider.readRecords(plan.domain, wanted);\n\tconst all = await provider.getRecords(plan.domain);\n\treturn all.filter(\n\t\t(r) =>\n\t\t\tnames.includes(r.name) && (MANAGED as readonly string[]).includes(r.type),\n\t);\n}\n\nconst same = (a: string, b: string) =>\n\ta.trim().replace(/\\.$/, '').toLowerCase() ===\n\tb.trim().replace(/\\.$/, '').toLowerCase();\n\n/**\n * What each domain's records need, compared with what is there: creates,\n * updates, and the deletes a change of mode needs — a name moving from an A\n * record to a CNAME, or back, cannot hold both. Nothing else is deleted.\n */\nexport function diffDomainDns(\n\tplan: DomainDnsPlan,\n\texisting: readonly DnsRecord[],\n): DnsChange[] {\n\tconst changes: DnsChange[] = [];\n\tconst find = (name: string, type: string) =>\n\t\texisting.find((r) => r.name === name && r.type === type);\n\n\tconst planned = new Map<string, Set<StackRecordType>>();\n\tfor (const record of plan.records) {\n\t\tconst types = planned.get(record.name) ?? new Set();\n\t\ttypes.add(record.type);\n\t\tplanned.set(record.name, types);\n\t}\n\n\t// The deletes first: a CNAME where an A record goes, A and AAAA where a\n\t// CNAME goes.\n\tfor (const [name, types] of planned) {\n\t\tconst conflicting: StackRecordType[] = types.has('CNAME')\n\t\t\t? ['A', 'AAAA']\n\t\t\t: ['CNAME'];\n\t\tfor (const type of conflicting) {\n\t\t\tconst found = find(name, type);\n\t\t\tif (!found || found.values.length === 0) continue;\n\t\t\tchanges.push({\n\t\t\t\tdomain: plan.domain,\n\t\t\t\tname,\n\t\t\t\ttype,\n\t\t\t\taction: 'delete',\n\t\t\t\tprevious: found.values.join(', '),\n\t\t\t});\n\t\t}\n\t}\n\n\tfor (const record of plan.records) {\n\t\tconst found = find(record.name, record.type);\n\t\tconst base = {\n\t\t\tdomain: plan.domain,\n\t\t\tname: record.name,\n\t\t\ttype: record.type,\n\t\t\tvalue: record.value,\n\t\t\tttl: record.ttl,\n\t\t};\n\t\tif (!found || found.values.length === 0) {\n\t\t\tchanges.push({ ...base, action: 'create' });\n\t\t} else if (\n\t\t\tfound.values.length === 1 &&\n\t\t\tsame(found.values[0]!, record.value)\n\t\t) {\n\t\t\tchanges.push({ ...base, action: 'unchanged' });\n\t\t} else {\n\t\t\tchanges.push({\n\t\t\t\t...base,\n\t\t\t\taction: 'update',\n\t\t\t\tprevious: found.values.join(', '),\n\t\t\t});\n\t\t}\n\t}\n\treturn changes;\n}\n\n/** A host's full name from a domain and a relative name. */\nfunction fullName(domain: string, name: string): string {\n\treturn name === '@' ? domain : `${name}.${domain}`;\n}\n\nfunction printChange(\n\tchange: DnsChange,\n\tdryRun: boolean,\n\tlog: (line: string) => void,\n) {\n\tconst host = fullName(change.domain, change.name).padEnd(32);\n\tconst type = change.type.padEnd(5);\n\tconst would = dryRun ? 'would ' : '';\n\tswitch (change.action) {\n\t\tcase 'unchanged':\n\t\t\tlog(\n\t\t\t\t` ✓ ${host} ${type} ${change.value} (TTL ${change.ttl}) — up to date`,\n\t\t\t);\n\t\t\tbreak;\n\t\tcase 'create':\n\t\t\tlog(\n\t\t\t\t` + ${host} ${type} ${change.value} (TTL ${change.ttl}) — ${would}create`,\n\t\t\t);\n\t\t\tbreak;\n\t\tcase 'update':\n\t\t\tlog(\n\t\t\t\t` ~ ${host} ${type} ${change.previous} → ${change.value} (TTL ${change.ttl}) — ${would}update`,\n\t\t\t);\n\t\t\tbreak;\n\t\tcase 'write':\n\t\t\tlog(\n\t\t\t\tdryRun\n\t\t\t\t\t? ` ? ${host} ${type} ${change.value} (TTL ${change.ttl}) — would write; current value unknown (key cannot read DNS)`\n\t\t\t\t\t: ` ! ${host} ${type} ${change.value} (TTL ${change.ttl}) — written (not diffed)`,\n\t\t\t);\n\t\t\tbreak;\n\t\tcase 'delete':\n\t\t\tlog(\n\t\t\t\t` - ${host} ${type} ${change.previous} — ${would}delete (a name cannot hold a CNAME and an A record)`,\n\t\t\t);\n\t\t\tbreak;\n\t}\n}\n\n/**\n * Each domain's records, through its provider. Returns every change, the\n * unchanged included; a dry run makes none of them.\n */\nexport async function applyStackDns(\n\tplan: StackDnsPlan,\n\toptions: ApplyStackDnsOptions = {},\n): Promise<DnsChange[]> {\n\treturn (await settleStackDns(plan, options)).changes;\n}\n\n/**\n * Each domain's records, through its provider — and, with `confirm`, what was\n * written read back from it. A record already holding its value is neither\n * written nor read again.\n *\n * @throws {DnsRecordsNotConfirmed} when a written record does not read back\n */\nexport async function settleStackDns(\n\tplan: StackDnsPlan,\n\toptions: ApplyStackDnsOptions = {},\n): Promise<StackDnsApplied> {\n\tconst dryRun = options.dryRun === true;\n\tconst log = options.log ?? ((line: string) => console.log(line));\n\tconst providerFor =\n\t\toptions.providerFor ??\n\t\t((config: DnsProviderConfig) =>\n\t\t\tcreateDnsProvider({\n\t\t\t\tconfig: config as SchemaDnsConfig,\n\t\t\t\t...(options.credentials ? { godaddy: options.credentials } : {}),\n\t\t\t\t...(options.hostinger ? { hostinger: options.hostinger } : {}),\n\t\t\t}));\n\n\tconst changes: DnsChange[] = [];\n\tconst confirmed = new Set<string>();\n\tconst unconfirmed = new Set<string>();\n\tconst hostsOf = (domain: DomainDnsPlan) =>\n\t\tnew Set(domain.records.map((r) => r.host));\n\tfor (const host of plan.uncovered) {\n\t\tlog(\n\t\t\t` ⚠ ${host} is under no domain in dns — point it at ${plan.server.ipv4} yourself`,\n\t\t);\n\t}\n\n\tfor (const domain of plan.domains) {\n\t\tif (domain.provider === 'manual') {\n\t\t\tchanges.push(...printManual(domain, log, 'manual'));\n\t\t\tfor (const host of hostsOf(domain)) unconfirmed.add(host);\n\t\t\tcontinue;\n\t\t}\n\t\tconst provider = await providerFor(domain.config);\n\t\tif (!provider) {\n\t\t\tchanges.push(...printManual(domain, log, 'manual'));\n\t\t\tfor (const host of hostsOf(domain)) unconfirmed.add(host);\n\t\t\tcontinue;\n\t\t}\n\n\t\tlog(` ${domain.domain} (${provider.name})${dryRun ? ' — dry run' : ''}`);\n\t\tlet existing: DnsRecord[];\n\t\tlet diff: DnsChange[];\n\t\tlet unreadable = false;\n\t\ttry {\n\t\t\texisting = await existingRecords(provider, domain);\n\t\t\tdiff = diffDomainDns(domain, existing);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof DnsRecordsUnreadable)) throw error;\n\t\t\tlog(\n\t\t\t\t` ℹ The ${provider.name} key cannot read ${domain.domain}'s DNS records, so changes cannot be diffed: every record is written (idempotent).`,\n\t\t\t);\n\t\t\tunreadable = true;\n\t\t\texisting = [];\n\t\t\tdiff = domain.records.map((r) => ({\n\t\t\t\tdomain: r.domain,\n\t\t\t\tname: r.name,\n\t\t\t\ttype: r.type,\n\t\t\t\taction: 'write' as const,\n\t\t\t\tvalue: r.value,\n\t\t\t\tttl: r.ttl,\n\t\t\t}));\n\t\t}\n\t\tfor (const change of diff) printChange(change, dryRun, log);\n\n\t\t// An AAAA nobody planned, at a name that points here: IPv6 clients —\n\t\t// Let's Encrypt among them — would go somewhere else.\n\t\tif (!plan.server.ipv6) {\n\t\t\tfor (const name of new Set(domain.records.map((r) => r.name))) {\n\t\t\t\tconst aaaa = existing.find((r) => r.name === name && r.type === 'AAAA');\n\t\t\t\tconst planned = domain.records.find(\n\t\t\t\t\t(r) => r.name === name && r.type === 'A',\n\t\t\t\t);\n\t\t\t\tif (aaaa && planned && aaaa.values.length > 0) {\n\t\t\t\t\tlog(\n\t\t\t\t\t\t` ⚠ ${fullName(domain.domain, name)} also has AAAA ${aaaa.values.join(', ')}: ` +\n\t\t\t\t\t\t\t`set ${SERVER_IPV6_KEY} in the '${plan.stage}' stage's secrets to the server's IPv6 address, ` +\n\t\t\t\t\t\t\t'or remove that record, or IPv6 clients go elsewhere',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tchanges.push(...diff);\n\n\t\t// A host every one of whose records was already right is confirmed by\n\t\t// the read that found it so; one with a write is confirmed by reading\n\t\t// it back — or, with a key that cannot read, by nobody.\n\t\tconst written = new Set(\n\t\t\tdiff\n\t\t\t\t.filter((c) => c.action !== 'unchanged')\n\t\t\t\t.map((c) => fullName(c.domain, c.name)),\n\t\t);\n\t\tfor (const host of hostsOf(domain)) {\n\t\t\t// A dry run writes nothing, so nothing it would write is resolved:\n\t\t\t// a name looked up before its record exists is cached as missing.\n\t\t\tif (unreadable) {\n\t\t\t\tif (!dryRun) unconfirmed.add(host);\n\t\t\t} else if (!written.has(host)) confirmed.add(host);\n\t\t}\n\t\tif (dryRun) continue;\n\n\t\tconst deletes = diff.filter((c) => c.action === 'delete');\n\t\tif (deletes.length > 0) {\n\t\t\tawait provider.deleteRecords(\n\t\t\t\tdomain.domain,\n\t\t\t\tdeletes.map((c) => ({ name: c.name, type: c.type })),\n\t\t\t);\n\t\t}\n\t\tconst writes = diff.filter(\n\t\t\t(c) =>\n\t\t\t\tc.action === 'create' || c.action === 'update' || c.action === 'write',\n\t\t);\n\t\tif (writes.length > 0) {\n\t\t\tawait provider.upsertRecords(\n\t\t\t\tdomain.domain,\n\t\t\t\twrites.map((c) => ({\n\t\t\t\t\tname: c.name,\n\t\t\t\t\ttype: c.type,\n\t\t\t\t\tvalue: c.value!,\n\t\t\t\t\tttl: c.ttl!,\n\t\t\t\t})),\n\t\t\t);\n\t\t}\n\n\t\tif (!options.confirm || unreadable || written.size === 0) continue;\n\t\tconst rewritten = {\n\t\t\t...domain,\n\t\t\trecords: domain.records.filter((r) => written.has(r.host)),\n\t\t};\n\t\tconst after = await existingRecords(provider, rewritten);\n\t\tconst missing: UnconfirmedRecord[] = [];\n\t\tfor (const record of rewritten.records) {\n\t\t\tconst found = after.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\t\t\tconst values = found?.values ?? [];\n\t\t\tif (values.length !== 1 || !same(values[0]!, record.value)) {\n\t\t\t\tmissing.push({\n\t\t\t\t\thost: record.host,\n\t\t\t\t\ttype: record.type,\n\t\t\t\t\texpected: record.value,\n\t\t\t\t\tfound: values,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (missing.length > 0) {\n\t\t\tthrow new DnsRecordsNotConfirmed(\n\t\t\t\tplan.stage,\n\t\t\t\tdomain.domain,\n\t\t\t\tprovider.name,\n\t\t\t\tmissing,\n\t\t\t);\n\t\t}\n\t\tfor (const host of written) confirmed.add(host);\n\t\tlog(\n\t\t\t` ✓ ${written.size} host${written.size === 1 ? '' : 's'} read back from ${provider.name}`,\n\t\t);\n\t}\n\tfor (const host of plan.uncovered) unconfirmed.add(host);\n\treturn {\n\t\tchanges,\n\t\tconfirmed: [...confirmed].sort(),\n\t\tunconfirmed: [...unconfirmed].filter((h) => !confirmed.has(h)).sort(),\n\t};\n}\n\n// ============================================================================\n// The check\n// ============================================================================\n\n/** Resolves a host to every address it has — `dns.promises.lookup`, `all`. */\nexport type HostLookup = (host: string) => Promise<string[]>;\n\n/** The system resolver: what the server's own clients would be told. */\nexport const systemLookup: HostLookup = async (host) => {\n\tconst found = await dnsLookup(host, { all: true });\n\treturn found.map((a) => a.address);\n};\n\n/** One host that does not resolve to the server. */\nexport interface MisdirectedHost {\n\thost: string;\n\t/** What it resolves to — empty when it does not resolve. */\n\tresolved: string[];\n\t/** Why it did not resolve, when it did not. */\n\terror?: string;\n}\n\n/** Hosts that do not resolve to the stage's server. */\nexport class HostNotPointingAtServer extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly hosts: readonly MisdirectedHost[],\n\t\treadonly expected: { ipv4: string; ipv6?: string },\n\t\t/** The records to create, where `dns` covers none of them. */\n\t\treadonly fix: string,\n\t) {\n\t\tconst want = expected.ipv6\n\t\t\t? `${expected.ipv4} (and ${expected.ipv6})`\n\t\t\t: expected.ipv4;\n\t\tconst lines = hosts.map(\n\t\t\t(h) =>\n\t\t\t\t` - ${h.host} → ${h.resolved.length > 0 ? h.resolved.join(', ') : `nothing${h.error ? ` (${h.error})` : ''}`}, expected ${want}`,\n\t\t);\n\t\tsuper(\n\t\t\t`${hosts.length === 1 ? 'A host' : `${hosts.length} hosts`} of '${stage}' ` +\n\t\t\t\t`do${hosts.length === 1 ? 'es' : ''} not resolve to its server, so a ` +\n\t\t\t\t`certificate for ${hosts.length === 1 ? 'it' : 'them'} cannot be issued:\\n` +\n\t\t\t\t`${lines.join('\\n')}\\n${fix}\\n` +\n\t\t\t\t'Records can take a few minutes to propagate. With a CDN or proxy in ' +\n\t\t\t\t'front of the server, pass --skip-dns.',\n\t\t);\n\t\tthis.name = 'HostNotPointingAtServer';\n\t}\n}\n\nexport interface CheckStackDnsInput {\n\tstage: string;\n\thosts: readonly string[];\n\tserver: ServerAddress;\n\tdns: WorkspaceDnsConfig | undefined;\n\tlookup?: HostLookup;\n}\n\n/**\n * Every host resolved: each must resolve to the server's IPv4 address and to\n * nothing but the server's addresses.\n *\n * @throws {HostNotPointingAtServer} naming every host that does not\n */\nexport async function checkStackDns(\n\tinput: CheckStackDnsInput,\n): Promise<string[]> {\n\tconst lookup = input.lookup ?? systemLookup;\n\tconst { server } = input;\n\tconst ours = new Set(\n\t\t[server.ipv4, server.ipv6].filter(Boolean).map((a) => a!.toLowerCase()),\n\t);\n\tconst bad: MisdirectedHost[] = [];\n\tconst ok: string[] = [];\n\tfor (const host of input.hosts) {\n\t\tlet resolved: string[];\n\t\ttry {\n\t\t\tresolved = [...new Set(await lookup(host))];\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\terror && typeof error === 'object' && 'code' in error\n\t\t\t\t\t? String((error as { code: unknown }).code)\n\t\t\t\t\t: error instanceof Error\n\t\t\t\t\t\t? error.message\n\t\t\t\t\t\t: String(error);\n\t\t\tbad.push({ host, resolved: [], error: code });\n\t\t\tcontinue;\n\t\t}\n\t\tconst pointsHere =\n\t\t\tresolved.some((a) => a.toLowerCase() === server.ipv4.toLowerCase()) &&\n\t\t\tresolved.every((a) => ours.has(a.toLowerCase()));\n\t\tif (pointsHere) ok.push(host);\n\t\telse bad.push({ host, resolved });\n\t}\n\n\tif (bad.length > 0) {\n\t\tthrow new HostNotPointingAtServer(\n\t\t\tinput.stage,\n\t\t\tbad,\n\t\t\t{ ipv4: server.ipv4, ...(server.ipv6 ? { ipv6: server.ipv6 } : {}) },\n\t\t\tdnsFix(input, bad),\n\t\t);\n\t}\n\treturn ok;\n}\n\n/** How to fix it: the deploy, or the records to create by hand. */\nfunction dnsFix(input: CheckStackDnsInput, bad: readonly MisdirectedHost[]) {\n\tconst hosts = bad.map((h) => h.host);\n\tlet plan: StackDnsPlan;\n\ttry {\n\t\tplan = planStackDns({\n\t\t\tstage: input.stage,\n\t\t\thosts,\n\t\t\tserver: input.server,\n\t\t\tdns: input.dns,\n\t\t});\n\t} catch {\n\t\tplan = {\n\t\t\tstage: input.stage,\n\t\t\tserver: input.server,\n\t\t\tdomains: [],\n\t\t\tuncovered: hosts,\n\t\t};\n\t}\n\tconst automatic = plan.domains.filter((d) => d.provider !== 'manual');\n\tconst byHand = [\n\t\t...plan.domains\n\t\t\t.filter((d) => d.provider === 'manual')\n\t\t\t.flatMap((d) => d.records.map(describe)),\n\t\t...plan.uncovered.flatMap((host) => [\n\t\t\t`${host.padEnd(32)} A ${input.server.ipv4}`,\n\t\t\t...(input.server.ipv6\n\t\t\t\t? [`${host.padEnd(32)} AAAA ${input.server.ipv6}`]\n\t\t\t\t: []),\n\t\t]),\n\t];\n\tconst lines: string[] = [];\n\tif (automatic.length > 0) {\n\t\tlines.push(\n\t\t\t`Fix: gkm deploy --stage ${input.stage} writes the records for ${automatic.map((d) => d.domain).join(', ')} through their provider (--dry-run shows them first).`,\n\t\t);\n\t}\n\tif (byHand.length > 0) {\n\t\tlines.push(\n\t\t\t`${automatic.length > 0 ? 'And create' : 'Fix: create'} these records at the DNS host:`,\n\t\t\t...byHand.map((line) => ` ${line}`),\n\t\t);\n\t}\n\treturn lines.join('\\n');\n}\n\n// ============================================================================\n// The step: plan, apply, confirm, check\n// ============================================================================\n\n/** The environment variable each token-authenticated provider reads. */\nexport const DNS_TOKEN_KEYS = {\n\tgodaddy: 'GODADDY_API_TOKEN',\n\thostinger: 'HOSTINGER_API_TOKEN',\n} as const;\n\n/** A provider whose credential is a token gkm reads. */\nexport type TokenDnsProvider = keyof typeof DNS_TOKEN_KEYS;\n\n/** The tokens a stage's records are written with. */\nexport interface DnsCredentials {\n\tgodaddy?: GoDaddyCredential;\n\thostinger?: HostingerCredential;\n}\n\n/**\n * A domain whose records the deploy writes, and no token to write them with.\n * Raised before anything is built, started or written.\n */\nexport class DnsCredentialMissing extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly domain: string,\n\t\treadonly provider: TokenDnsProvider,\n\t\t/** The variable the token is read from: `GODADDY_API_TOKEN`. */\n\t\treadonly key: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}''s hosts are under ${domain}, whose DNS is ${provider}'s, and ` +\n\t\t\t\t`the deploy writes their records — but ${key} is not set. In CI, add ` +\n\t\t\t\t`${key} as a secret on the '${stage}' GitHub environment and pass it ` +\n\t\t\t\t`to the step that writes the records: env: { ${key}: \\${{ secrets.${key} }} }. ` +\n\t\t\t\t`On this machine, export ${key} or run gkm login --provider ${provider}. ` +\n\t\t\t\t'With a CDN or proxy in front of the server, pass --skip-dns.',\n\t\t);\n\t\tthis.name = 'DnsCredentialMissing';\n\t}\n}\n\n/** One `dns` domain and its provider's config. */\nexport interface DnsDomain {\n\tdomain: string;\n\tconfig: DnsProviderConfig;\n}\n\n/**\n * The `dns` domain a stage's own domain (`domains.<stage>`) is under — where\n * its hosts' records go — or none.\n */\nexport function stageDnsDomains(\n\tstage: string,\n\tdomains: Readonly<Record<string, string>> | undefined,\n\tdns: WorkspaceDnsConfig | undefined,\n): DnsDomain[] {\n\tconst own = domains?.[stage];\n\tif (!own || !dns || isLocalHost(own)) return [];\n\tconst normalized = normalizeDnsConfig(dns);\n\tconst root = findRootDomain(own.toLowerCase(), normalized);\n\treturn root ? [{ domain: root, config: normalized[root]! }] : [];\n}\n\n/**\n * Each token the domains' providers need, from `credentials` — `manual`,\n * Route53 (the AWS SDK's own chain) and a provider object need none.\n *\n * @throws {DnsCredentialMissing} naming the first domain with no token\n */\nexport async function dnsCredentials(\n\tstage: string,\n\tdomains: readonly DnsDomain[],\n\tcredentials: CredentialProvider,\n): Promise<DnsCredentials> {\n\tconst found: DnsCredentials = {};\n\tfor (const { domain, config } of domains) {\n\t\tconst provider = config.provider;\n\t\tif (provider !== 'godaddy' && provider !== 'hostinger') continue;\n\t\tif (found[provider]) continue;\n\t\tconst token = await credentials.get({ kind: provider });\n\t\tif (!token?.token) {\n\t\t\tthrow new DnsCredentialMissing(\n\t\t\t\tstage,\n\t\t\t\tdomain,\n\t\t\t\tprovider,\n\t\t\t\tDNS_TOKEN_KEYS[provider],\n\t\t\t);\n\t\t}\n\t\tfound[provider] = token;\n\t}\n\treturn found;\n}\n\nexport interface StageDnsInput {\n\tstage: string;\n\t/** The stage's public hosts, read off its stack. */\n\thosts: readonly string[];\n\tserver: ServerAddress;\n\tdns: WorkspaceDnsConfig | undefined;\n\t/** Where the providers' tokens come from. */\n\tcredentials: CredentialProvider;\n\tdryRun?: boolean;\n\tlog?: (line: string) => void;\n\t/** Resolves the hosts no provider confirmed — the system resolver by default. */\n\tlookup?: HostLookup;\n\t/** The provider for a domain — `createDnsProvider` by default. */\n\tproviderFor?: (config: DnsProviderConfig) => Promise<DnsProvider | null>;\n\t/** GoDaddy's API, for tests: its base URL and how a 429 is waited out. */\n\tgodaddy?: Pick<GoDaddyProviderOptions, 'baseUrl' | 'sleep' | 'maxAttempts'>;\n}\n\nexport interface StageDnsResult {\n\tplan: StackDnsPlan;\n\tchanges: DnsChange[];\n\t/** Hosts whose records their provider holds with the server's address. */\n\tconfirmed: string[];\n\t/** Hosts the system resolver found pointing at the server. */\n\tresolved: string[];\n}\n\n/**\n * A stage's DNS, as every deploy runs it: the\n * stage's own hosts planned, written through each domain's provider where\n * they differ, and read back from it. Only A, AAAA and CNAME records of\n * those names are touched; nothing is a wildcard, and no other name is read,\n * written or deleted. What no provider confirms — a `manual` domain, a host\n * under no domain in `dns`, a key that cannot read — is resolved publicly.\n * A dry run prints the plan and writes nothing.\n *\n * @throws {DnsCredentialMissing} before anything is read or written\n * @throws {DnsRecordsNotConfirmed} when a written record does not read back\n * @throws {HostNotPointingAtServer} when a resolved host is not the server\n */\nexport async function stageDns(input: StageDnsInput): Promise<StageDnsResult> {\n\tconst { stage, server } = input;\n\tconst log = input.log ?? ((line: string) => console.log(line));\n\tconst plan = planStackDns({\n\t\tstage,\n\t\thosts: input.hosts,\n\t\tserver,\n\t\tdns: input.dns,\n\t});\n\tconst tokens = await dnsCredentials(\n\t\tstage,\n\t\tplan.domains.map(({ domain, config }) => ({ domain, config })),\n\t\tinput.credentials,\n\t);\n\t// No `dns` at all: nothing to write, and every host is the person's to\n\t// point — checked, without a line per host saying so.\n\tconst applied: StackDnsApplied = !input.dns\n\t\t? { changes: [], confirmed: [], unconfirmed: [...plan.uncovered] }\n\t\t: await settleStackDns(plan, {\n\t\t\t\t...(input.dryRun ? { dryRun: true } : { confirm: true }),\n\t\t\t\tlog,\n\t\t\t\tcredentials: {\n\t\t\t\t\t...(input.godaddy ?? {}),\n\t\t\t\t\t...(tokens.godaddy ? { credential: tokens.godaddy } : {}),\n\t\t\t\t\tlog,\n\t\t\t\t},\n\t\t\t\t...(tokens.hostinger ? { hostinger: tokens.hostinger } : {}),\n\t\t\t\t...(input.providerFor ? { providerFor: input.providerFor } : {}),\n\t\t\t});\n\n\tlet resolved: string[] = [];\n\tif (applied.unconfirmed.length > 0) {\n\t\tresolved = await checkStackDns({\n\t\t\tstage,\n\t\t\thosts: applied.unconfirmed,\n\t\t\tserver,\n\t\t\tdns: input.dns,\n\t\t\t...(input.lookup ? { lookup: input.lookup } : {}),\n\t\t});\n\t}\n\treturn {\n\t\tplan,\n\t\tchanges: applied.changes,\n\t\tconfirmed: applied.confirmed,\n\t\tresolved,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dns-agf2VCdV.mjs","names":["dnsLookup"],"sources":["../src/providers/dns.ts","../src/backups/config.ts","../src/backups/service.ts","../src/compose/dns.ts"],"sourcesContent":["/**\n * What the deploy's DNS step needs to know about a workspace before its\n * stack is run: whether it deploys with compose.\n *\n * The step itself is `stageDns` in `compose/dns.ts`, run by every deploy\n * (`gkm deploy`, and `gkm compose`, the same run) where the deploy runs — a\n * CI runner or a developer's machine, which hold the DNS provider's token.\n * The server never does.\n */\n\nimport type { NormalizedWorkspace } from '../workspace/types.js';\n\n/** Whether the workspace deploys through the compose target. */\nexport function deploysWithCompose(workspace: NormalizedWorkspace): boolean {\n\treturn (\n\t\tworkspace.deploy?.default === 'compose' ||\n\t\tObject.values(workspace.apps).some(\n\t\t\t(app) => app.resolvedDeployTarget === 'compose',\n\t\t)\n\t);\n}\n","/**\n * Which stages take backups, and where they are kept.\n *\n * A deployed compose stage whose stack runs Postgres is backed up by\n * default — every day at 02:00 UTC, kept 30 days — unless\n * `deploy.backups.<stage>` says otherwise. The local stage never is.\n *\n * Every backup is kept in the project bucket (`providers/projectBucket.ts`),\n * beside the stage's deploy state, one folder per run and one file per\n * database:\n *\n * ```\n * gkm/<project>/<stage>/backups/2026-10-10/02-00-00Z/database.sql.gz\n * gkm/<project>/<stage>/backups/2026-10-10/02-00-00Z/auth-database.sql.gz\n * ```\n *\n * The file is named after the database's construct, the way its migrations\n * folder is: `AuthDatabase` is `db/auth-database`, and `auth-database.sql.gz`.\n */\n\nimport { type ConstructManifest, kebabCase } from '@geekmidas/manifest';\nimport { deploysWithCompose } from '../providers/dns.js';\nimport type { PrefixExpiry } from '../providers/projectBucket.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport { checkStageBackups, type StageBackups } from './schedule.js';\n\n/** What a stage's backups come to. */\nexport type StageBackupsChoice =\n\t| {\n\t\t\tmode: 'on';\n\t\t\tbackups: StageBackups;\n\t\t\t/** Whether the stage named it, or took the default. */\n\t\t\tsource: 'config' | 'default';\n\t }\n\t/** `deploy.backups.<stage>: false`. */\n\t| { mode: 'disabled' }\n\t/** Nothing to back up, or nowhere it applies. */\n\t| { mode: 'none'; reason: 'local' | 'not-compose' | 'no-postgres' };\n\n/** The workspace parts {@link stageBackups} reads. */\nexport type BackupsWorkspace = Pick<\n\tNormalizedWorkspace,\n\t'name' | 'apps' | 'deploy' | 'stages' | 'state'\n>;\n\n/** Whether the stack runs Postgres: some construct is a database. */\nexport function runsPostgres(manifest: ConstructManifest): boolean {\n\treturn Object.values(manifest).some((d) => d.kind === 'database');\n}\n\n/**\n * What `stage`'s backups are: on — by its entry or the default — off by\n * `false`, or none, for the local stage, a stage compose does not deploy,\n * and a stack with no database.\n */\nexport function stageBackups(\n\tworkspace: BackupsWorkspace,\n\tmanifest: ConstructManifest,\n\tstage: string,\n): StageBackupsChoice {\n\tif (stage === workspace.stages?.local) {\n\t\treturn { mode: 'none', reason: 'local' };\n\t}\n\tif (!deploysWithCompose(workspace as NormalizedWorkspace)) {\n\t\treturn { mode: 'none', reason: 'not-compose' };\n\t}\n\tif (!runsPostgres(manifest)) return { mode: 'none', reason: 'no-postgres' };\n\tconst value = (\n\t\tworkspace.deploy?.backups as Record<string, unknown> | undefined\n\t)?.[stage];\n\tconst checked = checkStageBackups(stage, value ?? {});\n\tif (checked === false) return { mode: 'disabled' };\n\treturn {\n\t\tmode: 'on',\n\t\tbackups: checked,\n\t\tsource: value === undefined ? 'default' : 'config',\n\t};\n}\n\n/**\n * Where a stage's backups are, in the project bucket — under the same\n * `<prefix>/<project>/<stage>` its deploy state is: `gkm/shop/production/backups`.\n */\nexport function backupsPrefix(\n\tworkspace: Pick<NormalizedWorkspace, 'name' | 'state'>,\n\tstage: string,\n): string {\n\tconst state = workspace.state;\n\tconst prefix =\n\t\tstate?.provider === 's3' && state.prefix !== undefined\n\t\t\t? state.prefix.replace(/\\/+$/, '')\n\t\t\t: 'gkm';\n\treturn `${prefix ? `${prefix}/` : ''}${workspace.name}/${stage}/backups`;\n}\n\n/** One run's folder: `2026-10-10/02-00-00Z`, for the time it started. */\nexport function runFolder(at: Date): string {\n\tconst iso = at.toISOString();\n\treturn `${iso.slice(0, 10)}/${iso.slice(11, 19).replace(/:/g, '-')}Z`;\n}\n\n/** A run folder's time back, or `undefined` for a key that is not one. */\nexport function runTime(folder: string): Date | undefined {\n\tconst match = /^(\\d{4}-\\d{2}-\\d{2})\\/(\\d{2})-(\\d{2})-(\\d{2})Z$/.exec(folder);\n\tif (!match) return undefined;\n\tconst at = new Date(`${match[1]}T${match[2]}:${match[3]}:${match[4]}Z`);\n\treturn Number.isNaN(at.getTime()) ? undefined : at;\n}\n\n/** One database the stack backs up. */\nexport interface BackupDatabase {\n\t/** Its construct: `AuthDatabase`. */\n\tid: string;\n\t/** Its file in a run's folder, less `.sql.gz`: `auth-database`. */\n\tfile: string;\n\t/** Its name in the stack's Postgres: `auth_database_production`. */\n\tname: string;\n}\n\n/** A database's file in a run's folder: `auth-database.sql.gz`. */\nexport function backupFile(database: Pick<BackupDatabase, 'file'>): string {\n\treturn `${database.file}.sql.gz`;\n}\n\n/** Each database the stack runs, by its construct and its name in Postgres. */\nexport function backupDatabases(\n\tresources: readonly { id: string; kind: string; name: string }[],\n): BackupDatabase[] {\n\treturn resources\n\t\t.filter((r) => r.kind === 'database')\n\t\t.map((r) => ({ id: r.id, file: kebabCase(r.id), name: r.name }))\n\t\t.sort((a, b) => a.file.localeCompare(b.file));\n}\n\n/**\n * The expiry of every stage's backups, by each stage's own entry — what the\n * project bucket's lifecycle carries beside its own rule.\n *\n * A put replaces a bucket's whole lifecycle, so this is built from every\n * deployed stage each time, never from the stage being deployed alone: one\n * stage's deploy keeps every other's rule. A stage set to `false` has none,\n * and its backups are kept until someone deletes them.\n */\nexport function backupExpiries(\n\tworkspace: BackupsWorkspace,\n\tmanifest: ConstructManifest,\n): PrefixExpiry[] {\n\tconst expiries: PrefixExpiry[] = [];\n\tfor (const stage of [...(workspace.stages?.deployed ?? [])].sort()) {\n\t\tconst choice = stageBackups(workspace, manifest, stage);\n\t\tif (choice.mode !== 'on') continue;\n\t\texpiries.push({\n\t\t\tid: `gkm-backups-${stage}`,\n\t\t\tprefix: `${backupsPrefix(workspace, stage)}/`,\n\t\t\tdays: choice.backups.keepDays,\n\t\t\tabortIncompleteDays: 1,\n\t\t});\n\t}\n\treturn expiries;\n}\n","/**\n * The stack's `backups` service: what a deployed compose stage with Postgres\n * runs beside its apps, unless `deploy.backups.<stage>` is `false`.\n *\n * - **The image** is built from the stack's own Postgres image —\n * `postgres:<major>-alpine`, already on the server for the stack, and with\n * the `pg_dump` that matches the server — with Node added and gkm's runner\n * (`runner.ts`) copied in. Nothing is pulled that the stack does not have,\n * and nothing is mounted from the host: the build context is two files.\n * - **Its key** is `BACKUPS_URL`, from the stage's secrets, like any app's:\n * allowed to put objects under the stage's backups prefix and nothing else.\n * The server holds no AWS credentials of its own.\n * - **Its login** is a role of its own, `<project>_backups`, granted\n * `pg_read_all_data` — it can read every database and write none. Postgres\n * before 14 has no such role, and there it signs in as the stack's\n * superuser.\n * - **Its health** is the last good run: unhealthy once that is more than\n * twice the schedule's longest gap old, after a startup grace.\n */\n\nimport { createHash } from 'node:crypto';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { DEFAULT_POSTGRES_VERSION } from '@geekmidas/manifest';\nimport { GkmError } from '../errors';\nimport { postgresImage } from '../reconcile/containers.js';\nimport { localRolePassword } from '../reconcile/env.js';\nimport { postgresRoleSlug } from '../reconcile/localCredentials.js';\nimport type { Plan } from '../reconcile/plan.js';\nimport { quoteIdentifier, type Statement } from '../reconcile/provision.js';\nimport {\n\ttype BackupDatabase,\n\ttype BackupsWorkspace,\n\tbackupDatabases,\n\tbackupsPrefix,\n} from './config.js';\nimport type { StageBackups } from './schedule.js';\n\n/** The service's name in the stack. */\nexport const BACKUPS_SERVICE = 'backups';\n\n/** The secret its key is written to. */\nexport const BACKUPS_URL_KEY = 'BACKUPS_URL';\n\n/** Where the runner is, in the image. */\nexport const RUNNER_PATH = '/gkm/backup.mjs';\n\n/** The first Postgres with `pg_read_all_data`. */\nconst READ_ALL_DATA_SINCE = 14;\n\n/** A stage that backs up, and no key to back up with. */\nexport class BackupsUrlMissing extends GkmError {\n\tconstructor(readonly stage: string) {\n\t\tsuper(\n\t\t\t`'${stage}' runs Postgres, so its stack backs it up, and its secrets ` +\n\t\t\t\t`hold no ${BACKUPS_URL_KEY} to back up with. The deploy writes it — ` +\n\t\t\t\t\"the IAM user and key it creates in the stage's AWS account — so \" +\n\t\t\t\t'run the deploy with the ' +\n\t\t\t\t\"account's credentials. To take no backups on this stage, set \" +\n\t\t\t\t`deploy.backups.${stage}: false.`,\n\t\t);\n\t\tthis.name = 'BackupsUrlMissing';\n\t}\n}\n\n/** The backups role: `<project>_backups`, as the superuser is `<project>_admin`. */\nexport function backupsRole(project: string): string {\n\treturn `${postgresRoleSlug(project)}_backups`;\n}\n\n/** The stack's backups, as its compose file and env file have them. */\nexport interface StackBackups {\n\tbackups: StageBackups;\n\tdatabases: BackupDatabase[];\n\t/** `gkm/<project>/<stage>/backups`. */\n\tprefix: string;\n\t/** The Postgres image it is built from. */\n\tfrom: string;\n\t/** `<project>-backups:pg18-<hash>`: built on the server, never pushed. */\n\timage: string;\n\t/** Its build context: the Dockerfile and the runner. */\n\tfiles: { Dockerfile: string; 'backup.mjs': string };\n\t/** What it signs in to Postgres as. */\n\tlogin: { user: string; password: string };\n\t/** Whether that is its own read-only role (else the superuser). */\n\treadOnly: boolean;\n\t/** Its environment — empty in a stack that only builds. */\n\tenv: Record<string, string>;\n}\n\n/**\n * The runner's source, as JavaScript: the built `backups/runner.mjs` beside\n * the CLI's bundle, or — run from source — `runner.ts`, with its types\n * stripped by esbuild.\n */\nexport function runnerSource(): string {\n\tconst here = dirname(fileURLToPath(import.meta.url));\n\tfor (const path of [\n\t\tjoin(here, 'backups', 'runner.mjs'),\n\t\tjoin(here, 'runner.mjs'),\n\t\tjoin(here, '..', 'backups', 'runner.mjs'),\n\t]) {\n\t\tif (existsSync(path)) return readFileSync(path, 'utf-8');\n\t}\n\tconst { transformSync } = createRequire(import.meta.url)(\n\t\t'esbuild',\n\t) as typeof import('esbuild');\n\treturn transformSync(readFileSync(join(here, 'runner.ts'), 'utf-8'), {\n\t\tloader: 'ts',\n\t\tformat: 'esm',\n\t\ttarget: 'node22',\n\t}).code;\n}\n\n/** The image's Dockerfile: the stack's Postgres, Node, and the runner. */\nexport function backupsDockerfile(from: string): string {\n\treturn `# Generated by gkm — the stage's backups: pg_dump from ${from}, and gkm's uploader.\nFROM ${from}\nRUN apk add --no-cache nodejs\nCOPY backup.mjs ${RUNNER_PATH}\nUSER postgres\nENTRYPOINT []\nCMD [\"node\", \"${RUNNER_PATH}\", \"serve\"]\n`;\n}\n\nexport interface StackBackupsInput {\n\tworkspace: BackupsWorkspace;\n\tstage: string;\n\tproject: string;\n\tplan: Plan;\n\tbackups: StageBackups;\n\t/** The stage's secrets, by key. */\n\tcustom: Readonly<Record<string, string>>;\n\tseed: string;\n\t/** The stack's superuser. */\n\tsuperuser: { user: string; password: string };\n\t/** What the stage's telemetry hands a process, when it is on. */\n\ttelemetry?: Readonly<Record<string, string>>;\n\t/** The stack only builds: no environment, no key needed. */\n\tbuildOnly?: boolean;\n}\n\n/**\n * The stack's backups service, for a stage whose backups are on.\n *\n * @throws {BackupsUrlMissing} when the stage's secrets hold no key\n */\nexport function stackBackups(input: StackBackupsInput): StackBackups {\n\tconst { plan, stage } = input;\n\tconst version =\n\t\tplan.resources.find((r) => r.version)?.version ?? DEFAULT_POSTGRES_VERSION;\n\tconst from = postgresImage(version);\n\tconst script = runnerSource();\n\tconst dockerfile = backupsDockerfile(from);\n\tconst hash = createHash('sha256')\n\t\t.update(dockerfile)\n\t\t.update(script)\n\t\t.digest('hex')\n\t\t.slice(0, 12);\n\n\tconst readOnly = version >= READ_ALL_DATA_SINCE;\n\tconst role = backupsRole(input.workspace.name);\n\tconst login = readOnly\n\t\t? {\n\t\t\t\tuser: role,\n\t\t\t\tpassword: localRolePassword(\n\t\t\t\t\tinput.workspace.name,\n\t\t\t\t\tplan,\n\t\t\t\t\trole,\n\t\t\t\t\tinput.seed,\n\t\t\t\t),\n\t\t\t}\n\t\t: input.superuser;\n\n\tconst databases = backupDatabases(plan.resources);\n\tconst prefix = backupsPrefix(input.workspace, stage);\n\tconst { kind, ...schedule } = input.backups.schedule;\n\n\tlet env: Record<string, string> = {};\n\tif (!input.buildOnly) {\n\t\tconst url = input.custom[BACKUPS_URL_KEY];\n\t\tif (!url) throw new BackupsUrlMissing(stage);\n\t\tenv = {\n\t\t\tBACKUPS_URL: url,\n\t\t\tBACKUPS_PREFIX: prefix,\n\t\t\tBACKUPS_DATABASES: JSON.stringify(\n\t\t\t\tdatabases.map(({ file, name }) => ({ file, name })),\n\t\t\t),\n\t\t\tBACKUPS_SCHEDULE: JSON.stringify(\n\t\t\t\tkind === 'cron'\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\tminutes: input.backups.schedule.minutes,\n\t\t\t\t\t\t\thours: input.backups.schedule.hours,\n\t\t\t\t\t\t\tdays: input.backups.schedule.days,\n\t\t\t\t\t\t\tmonths: input.backups.schedule.months,\n\t\t\t\t\t\t\tweekdays: input.backups.schedule.weekdays,\n\t\t\t\t\t\t}\n\t\t\t\t\t: { kind, ...schedule },\n\t\t\t),\n\t\t\tBACKUPS_MAX_GAP_SECONDS: String(input.backups.maxGapSeconds),\n\t\t\tPGHOST: 'postgres',\n\t\t\tPGPORT: '5432',\n\t\t\tPGUSER: login.user,\n\t\t\tPGPASSWORD: login.password,\n\t\t\t...(input.telemetry?.OTEL_EXPORTER_OTLP_ENDPOINT\n\t\t\t\t? {\n\t\t\t\t\t\tOTEL_EXPORTER_OTLP_ENDPOINT:\n\t\t\t\t\t\t\tinput.telemetry.OTEL_EXPORTER_OTLP_ENDPOINT,\n\t\t\t\t\t\t...(input.telemetry.OTEL_EXPORTER_OTLP_HEADERS\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\tOTEL_EXPORTER_OTLP_HEADERS:\n\t\t\t\t\t\t\t\t\t\tinput.telemetry.OTEL_EXPORTER_OTLP_HEADERS,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\tOTEL_SERVICE_NAME: BACKUPS_SERVICE,\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t};\n\t}\n\n\treturn {\n\t\tbackups: input.backups,\n\t\tdatabases,\n\t\tprefix,\n\t\tfrom,\n\t\timage: `${input.project}-backups:pg${version}-${hash}`,\n\t\tfiles: { Dockerfile: dockerfile, 'backup.mjs': script },\n\t\tlogin,\n\t\treadOnly,\n\t\tenv,\n\t};\n}\n\n/** The service in the stack's compose file. */\nexport function backupsService(backups: StackBackups, envFiles: boolean) {\n\treturn {\n\t\timage: backups.image,\n\t\t// Built on the server from two files; there is nothing to pull.\n\t\tbuild: { context: `./${BACKUPS_SERVICE}`, dockerfile: 'Dockerfile' },\n\t\tpull_policy: 'build' as const,\n\t\trestart: 'unless-stopped',\n\t\t...(envFiles\n\t\t\t? {\n\t\t\t\t\tenv_file: [\n\t\t\t\t\t\t{ path: `./${BACKUPS_SERVICE}.env`, format: 'raw' as const },\n\t\t\t\t\t],\n\t\t\t\t}\n\t\t\t: {}),\n\t\tdepends_on: { postgres: { condition: 'service_healthy' as const } },\n\t\thealthcheck: {\n\t\t\ttest: ['CMD', 'node', RUNNER_PATH, 'health'],\n\t\t\tinterval: '1m',\n\t\t\ttimeout: '10s',\n\t\t\tretries: 3,\n\t\t\tstart_period: '30s',\n\t\t},\n\t};\n}\n\n/**\n * The backups role, created and kept: able to sign in and read every\n * database, and nothing else. None when it signs in as the superuser.\n */\nexport function backupsRoleStatements(backups: StackBackups): Statement[] {\n\tif (!backups.readOnly) return [];\n\tconst role = quoteIdentifier(backups.login.user);\n\tconst password = `'${backups.login.password.replace(/'/g, \"''\")}'`;\n\treturn [\n\t\t{\n\t\t\tid: BACKUPS_SERVICE,\n\t\t\tdescribe: `role ${backups.login.user}`,\n\t\t\texists: {\n\t\t\t\tsql: 'SELECT 1 FROM pg_roles WHERE rolname = $1',\n\t\t\t\tvalues: [backups.login.user],\n\t\t\t},\n\t\t\tcreate: `CREATE ROLE ${role} LOGIN PASSWORD ${password}`,\n\t\t},\n\t\t{\n\t\t\tid: BACKUPS_SERVICE,\n\t\t\tdescribe: `role ${backups.login.user}: its password`,\n\t\t\tcreate: `ALTER ROLE ${role} WITH LOGIN PASSWORD ${password}`,\n\t\t},\n\t\t{\n\t\t\tid: BACKUPS_SERVICE,\n\t\t\tdescribe: `role ${backups.login.user}: read every database`,\n\t\t\tcreate: `GRANT pg_read_all_data TO ${role}`,\n\t\t},\n\t];\n}\n","/**\n * A compose stage's DNS: the records its public hosts need, and the check that\n * they resolve to its server before a certificate is asked for.\n *\n * - **The server** (`serverAddress`): the stage's own secrets hold its public\n * addresses — `GKM_SERVER_IPV4`, and `GKM_SERVER_IPV6` where it has one.\n * Reserved for gkm: never handed to an app.\n * - **The plan** (`planStackDns`, pure): every public host the stack serves —\n * its apex, each app, each file server, the public log UI — pointed at the\n * server. An A record per host (and an AAAA with an IPv6 address), or, where\n * the domain's `dns` entry says `records: { mode: 'cname', target }`, one A\n * record for the target and a CNAME to it for every other host but the\n * apex, which DNS forbids a CNAME at.\n * - **Applying it** (`applyStackDns`): through the domain's provider in `dns`,\n * a record set at a time. A record with the right value is left alone; one\n * with another value is replaced and the change printed. A `manual` domain\n * is printed and nothing else. A dry run reads, prints and writes nothing.\n * - **The check** (`checkStackDns`): each host resolved with the system\n * resolver. One that is not the server stops the deploy with\n * {@link HostNotPointingAtServer}, before Caddy or Traefik ask Let's Encrypt\n * for a certificate it cannot get — and use up the rate limit trying.\n * - **The step** (`stageDns`): all three, as every deploy runs them — the plan applied through each provider,\n * what was written read back from the provider, and only the hosts no\n * provider wrote resolved publicly. A brand-new name is never looked up\n * before its record exists, so no resolver caches it as missing.\n */\n\nimport { lookup as dnsLookup } from 'node:dns/promises';\nimport { isIPv4, isIPv6 } from 'node:net';\nimport type {\n\tCredentialProvider,\n\tGoDaddyCredential,\n\tHostingerCredential,\n} from '../deploy/credentials';\nimport {\n\tcreateDnsProvider,\n\ttype DnsProvider,\n\ttype DnsRecord,\n\tDnsRecordsUnreadable,\n\ttype DnsConfig as SchemaDnsConfig,\n} from '../target/dokploy/dns/DnsProvider';\nimport type { GoDaddyProviderOptions } from '../target/dokploy/dns/GoDaddyProvider';\nimport {\n\textractSubdomain,\n\tfindRootDomain,\n\tnormalizeDnsConfig,\n} from '../target/dokploy/dns/index';\nimport type {\n\tDnsProvider as DnsProviderConfig,\n\tWorkspaceDnsConfig,\n} from '../workspace/types';\nimport {\n\tcnameTarget,\n\ttype DnsRecordsMode,\n\tisReservedStageKey,\n\tSERVER_IPV4_KEY,\n\tSERVER_IPV6_KEY,\n} from './dnsConfig';\n\nexport { isReservedStageKey, SERVER_IPV4_KEY, SERVER_IPV6_KEY };\n\nimport { GkmError } from '../errors';\nimport type { ComposeStack } from './stack';\n\n/** A deployed stage's server: what its hosts' records point at. */\nexport interface ServerAddress {\n\tipv4: string;\n\tipv6?: string;\n}\n\n/** The line that says how to set the server's address. */\nexport function serverAddressHint(stage: string): string {\n\treturn `gkm secrets:set ${SERVER_IPV4_KEY} '<ip>' --stage ${stage}`;\n}\n\n/** A server address in the stage's secrets that is not an address. */\nexport class ServerAddressInvalid extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly key: string,\n\t\treadonly value: string,\n\t) {\n\t\tconst ipv4 = key === SERVER_IPV4_KEY;\n\t\tsuper(\n\t\t\t`${key} in the '${stage}' stage's secrets is ${JSON.stringify(value)}, ` +\n\t\t\t\t`which is not an ${ipv4 ? 'IPv4 address (like 203.0.113.10)' : 'IPv6 address (like 2001:db8::10)'}. ` +\n\t\t\t\t`Set the server's public address: gkm secrets:set ${key} '<ip>' --stage ${stage}.`,\n\t\t);\n\t\tthis.name = 'ServerAddressInvalid';\n\t}\n}\n\n/**\n * A compose stage that serves real domains with no server address: its hosts\n * cannot be pointed or checked, and a certificate would be asked for blind.\n */\nexport class ServerAddressMissing extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly domain: string,\n\t) {\n\t\tsuper(\n\t\t\t`The '${stage}' stage serves ${domain} and its secrets hold no ` +\n\t\t\t\t`${SERVER_IPV4_KEY} — the public address of the server its stack runs ` +\n\t\t\t\t\"on, which its hosts' DNS records point at and every deploy checks \" +\n\t\t\t\t`before asking for certificates. Set it: ${serverAddressHint(stage)} ` +\n\t\t\t\t`(and ${SERVER_IPV6_KEY} for an IPv6 address).`,\n\t\t);\n\t\tthis.name = 'ServerAddressMissing';\n\t}\n}\n\n/**\n * The server's address from the stage's secrets, checked — `undefined` when\n * `GKM_SERVER_IPV4` is not set.\n *\n * @throws {ServerAddressInvalid} for a value that is not an address\n */\nexport function serverAddress(\n\tstage: string,\n\tcustom: Readonly<Record<string, string>> | undefined,\n): ServerAddress | undefined {\n\tconst ipv4 = custom?.[SERVER_IPV4_KEY]?.trim();\n\tconst ipv6 = custom?.[SERVER_IPV6_KEY]?.trim();\n\tif (ipv6 && !isIPv6(ipv6)) {\n\t\tthrow new ServerAddressInvalid(stage, SERVER_IPV6_KEY, ipv6);\n\t}\n\tif (!ipv4) return undefined;\n\tif (!isIPv4(ipv4))\n\t\tthrow new ServerAddressInvalid(stage, SERVER_IPV4_KEY, ipv4);\n\treturn { ipv4, ...(ipv6 ? { ipv6 } : {}) };\n}\n\n/**\n * A name that resolves to this machine by definition — `localhost` and every\n * `*.localhost` — and is in nobody's DNS.\n */\nexport function isLocalHost(host: string): boolean {\n\tconst name = host.toLowerCase().replace(/\\.$/, '');\n\treturn name === 'localhost' || name.endsWith('.localhost');\n}\n\n/**\n * The server's address, required where the stage serves real domains — not\n * `*.localhost` ones, which point at this machine already.\n *\n * @throws {ServerAddressMissing} when the stage has a domain and no address\n * @throws {ServerAddressInvalid} for a value that is not an address\n */\nexport function requiredServerAddress(\n\tstage: string,\n\tdomain: string | undefined,\n\tcustom: Readonly<Record<string, string>> | undefined,\n): ServerAddress | undefined {\n\tconst server = serverAddress(stage, custom);\n\tif (!server && domain && !isLocalHost(domain)) {\n\t\tthrow new ServerAddressMissing(stage, domain);\n\t}\n\treturn server;\n}\n\n/** The record types the DNS step writes. */\nexport type StackRecordType = 'A' | 'AAAA' | 'CNAME';\n\nconst MANAGED: readonly StackRecordType[] = ['A', 'AAAA', 'CNAME'];\n\n/** One record a stack's host needs. */\nexport interface PlannedDnsRecord {\n\t/** The full name: `api.example.com`. */\n\thost: string;\n\t/** The root domain in `dns` it is under. */\n\tdomain: string;\n\t/** Relative to the domain, as providers take it: `api`, `@`. */\n\tname: string;\n\ttype: StackRecordType;\n\tvalue: string;\n\tttl: number;\n}\n\n/** One `dns` domain's share of the plan. */\nexport interface DomainDnsPlan {\n\tdomain: string;\n\t/** `godaddy`, `route53`, `manual`, … — `custom` for an object. */\n\tprovider: string;\n\tconfig: DnsProviderConfig;\n\trecords: PlannedDnsRecord[];\n}\n\nexport interface StackDnsPlan {\n\tstage: string;\n\tserver: ServerAddress;\n\tdomains: DomainDnsPlan[];\n\t/** Hosts under no `dns` domain: theirs to point by hand. */\n\tuncovered: string[];\n}\n\n/** Every public host the stack serves, once each, sorted. */\nexport function stackHosts(stack: Pick<ComposeStack, 'routes'>): string[] {\n\treturn [...new Set(stack.routes.map((route) => route.host))].sort();\n}\n\n/** The TTL a domain's records are written with. */\nexport function recordTtl(config: DnsProviderConfig): number {\n\tconst ttl = 'ttl' in config ? config.ttl : undefined;\n\tif (ttl) return ttl;\n\treturn config.provider === 'godaddy' ? 600 : 300;\n}\n\nfunction providerName(config: DnsProviderConfig): string {\n\treturn typeof config.provider === 'string' ? config.provider : 'custom';\n}\n\nexport interface PlanStackDnsInput {\n\tstage: string;\n\thosts: readonly string[];\n\tserver: ServerAddress;\n\tdns: WorkspaceDnsConfig | undefined;\n}\n\n/**\n * A wildcard host: gkm writes one record per public host, so a stage on\n * another server — or a name someone else points elsewhere — is never caught\n * by a record it did not ask for.\n */\nexport class DnsWildcardRefused extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly host: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}' would point ${host} at its server, and gkm never writes a ` +\n\t\t\t\t'wildcard record: each public host gets its own. Give the app a ' +\n\t\t\t\t'subdomain of its own instead.',\n\t\t);\n\t\tthis.name = 'DnsWildcardRefused';\n\t}\n}\n\n/** The records a stage's hosts need — pure. */\nexport function planStackDns(input: PlanStackDnsInput): StackDnsPlan {\n\tconst { stage, server } = input;\n\tconst dns = input.dns ? normalizeDnsConfig(input.dns) : {};\n\tconst records = new Map<string, PlannedDnsRecord>();\n\tconst uncovered: string[] = [];\n\n\tconst add = (\n\t\thost: string,\n\t\tdomain: string,\n\t\ttype: StackRecordType,\n\t\tvalue: string,\n\t) => {\n\t\tconst name = extractSubdomain(host, domain);\n\t\trecords.set(`${domain} ${name} ${type}`, {\n\t\t\thost,\n\t\t\tdomain,\n\t\t\tname,\n\t\t\ttype,\n\t\t\tvalue,\n\t\t\tttl: recordTtl(dns[domain]!),\n\t\t});\n\t};\n\tconst pointAt = (host: string, domain: string) => {\n\t\tadd(host, domain, 'A', server.ipv4);\n\t\tif (server.ipv6) add(host, domain, 'AAAA', server.ipv6);\n\t};\n\n\tfor (const raw of input.hosts) {\n\t\tconst host = raw.toLowerCase();\n\t\tif (host.includes('*')) throw new DnsWildcardRefused(stage, host);\n\t\tconst domain = findRootDomain(host, dns);\n\t\tif (!domain) {\n\t\t\tuncovered.push(host);\n\t\t\tcontinue;\n\t\t}\n\t\t// A target under the domain itself — the schema refuses one that is\n\t\t// not — gets the A record every other host's CNAME points at.\n\t\tconst target = cnameTarget(\n\t\t\t(dns[domain] as { records?: DnsRecordsMode }).records,\n\t\t\tstage,\n\t\t);\n\t\tif (target) pointAt(target, domain);\n\t\t// The apex can hold no CNAME — it has the zone's SOA and NS — so it\n\t\t// is always an A record.\n\t\tif (target && host !== domain && host !== target) {\n\t\t\tadd(host, domain, 'CNAME', target);\n\t\t} else {\n\t\t\tpointAt(host, domain);\n\t\t}\n\t}\n\n\tconst domains = new Map<string, DomainDnsPlan>();\n\tfor (const record of [...records.values()].sort(\n\t\t(a, b) =>\n\t\t\ta.domain.localeCompare(b.domain) ||\n\t\t\ta.name.localeCompare(b.name) ||\n\t\t\ta.type.localeCompare(b.type),\n\t)) {\n\t\tconst config = dns[record.domain]!;\n\t\tconst plan = domains.get(record.domain) ?? {\n\t\t\tdomain: record.domain,\n\t\t\tprovider: providerName(config),\n\t\t\tconfig,\n\t\t\trecords: [],\n\t\t};\n\t\tplan.records.push(record);\n\t\tdomains.set(record.domain, plan);\n\t}\n\n\treturn {\n\t\tstage,\n\t\tserver,\n\t\tdomains: [...domains.values()],\n\t\tuncovered: uncovered.sort(),\n\t};\n}\n\n/** One change applying a plan makes — or, in a dry run, would. */\nexport interface DnsChange {\n\tdomain: string;\n\tname: string;\n\ttype: StackRecordType;\n\t/**\n\t * `write`: the provider's key cannot read records, so the record is\n\t * written without knowing what is there — idempotent, and not diffed.\n\t */\n\taction: 'create' | 'update' | 'delete' | 'unchanged' | 'write' | 'manual';\n\t/** The value it is set to — absent for a delete. */\n\tvalue?: string;\n\t/** What it was, for an update or a delete. */\n\tprevious?: string;\n\tttl?: number;\n}\n\nexport interface ApplyStackDnsOptions {\n\tdryRun?: boolean;\n\tlog?: (line: string) => void;\n\t/** Where the GoDaddy provider reads its key: the environment and the CLI's home. */\n\tcredentials?: GoDaddyProviderOptions;\n\t/** The Hostinger token, when the caller already has it. */\n\thostinger?: HostingerCredential;\n\t/** The provider for a domain — `createDnsProvider` by default. */\n\tproviderFor?: (config: DnsProviderConfig) => Promise<DnsProvider | null>;\n\t/**\n\t * Read every record written back from its provider, and throw\n\t * {@link DnsRecordsNotConfirmed} for one that does not hold its value.\n\t */\n\tconfirm?: boolean;\n}\n\n/** What applying a plan did, and what of it the providers confirmed. */\nexport interface StackDnsApplied {\n\t/** Every change, the unchanged included; a dry run makes none of them. */\n\tchanges: DnsChange[];\n\t/**\n\t * Hosts whose every record the provider holds with its value — read\n\t * before the run (unchanged) or after it wrote them (`confirm`).\n\t */\n\tconfirmed: string[];\n\t/**\n\t * Hosts no provider vouches for: a `manual` domain's, those under no\n\t * domain in `dns`, and those of a key that can write and not read.\n\t */\n\tunconfirmed: string[];\n}\n\n/** One record a provider did not hold with its value after it was written. */\nexport interface UnconfirmedRecord {\n\thost: string;\n\ttype: StackRecordType;\n\texpected: string;\n\t/** What the provider read back — empty when the record is not there. */\n\tfound: string[];\n}\n\n/** Records a provider accepted and then did not read back with their value. */\nexport class DnsRecordsNotConfirmed extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly domain: string,\n\t\treadonly provider: string,\n\t\treadonly records: readonly UnconfirmedRecord[],\n\t) {\n\t\tconst lines = records.map(\n\t\t\t(r) =>\n\t\t\t\t` - ${r.host} ${r.type}: ${r.found.length > 0 ? r.found.join(', ') : 'nothing'}, expected ${r.expected}`,\n\t\t);\n\t\tsuper(\n\t\t\t`${provider} accepted '${stage}''s records for ${domain}, and reading them ` +\n\t\t\t\t`back does not show them:\\n${lines.join('\\n')}\\n` +\n\t\t\t\t\"Deploy again; if it persists, check the records in the provider's \" +\n\t\t\t\t'console — another tool may be writing the same names.',\n\t\t);\n\t\tthis.name = 'DnsRecordsNotConfirmed';\n\t}\n}\n\n/** `api.example.com A 203.0.113.10 (TTL 600)` */\nfunction describe(record: PlannedDnsRecord): string {\n\treturn `${record.host.padEnd(32)} ${record.type.padEnd(5)} ${record.value} (TTL ${record.ttl})`;\n}\n\nfunction printManual(\n\tplan: DomainDnsPlan,\n\tlog: (line: string) => void,\n\twhy: string,\n): DnsChange[] {\n\tlog(` ${plan.domain}: ${why} — create these records at its DNS host:`);\n\tfor (const record of plan.records) log(` ${describe(record)}`);\n\treturn plan.records.map((r) => ({\n\t\tdomain: r.domain,\n\t\tname: r.name,\n\t\ttype: r.type,\n\t\taction: 'manual',\n\t\tvalue: r.value,\n\t\tttl: r.ttl,\n\t}));\n}\n\n/** The existing records of every planned name, for the types gkm manages. */\nasync function existingRecords(\n\tprovider: DnsProvider,\n\tplan: DomainDnsPlan,\n): Promise<DnsRecord[]> {\n\tconst names = [...new Set(plan.records.map((r) => r.name))];\n\tconst wanted = names.flatMap((name) =>\n\t\tMANAGED.map((type) => ({ name, type })),\n\t);\n\tif (provider.readRecords) return provider.readRecords(plan.domain, wanted);\n\tconst all = await provider.getRecords(plan.domain);\n\treturn all.filter(\n\t\t(r) =>\n\t\t\tnames.includes(r.name) && (MANAGED as readonly string[]).includes(r.type),\n\t);\n}\n\nconst same = (a: string, b: string) =>\n\ta.trim().replace(/\\.$/, '').toLowerCase() ===\n\tb.trim().replace(/\\.$/, '').toLowerCase();\n\n/**\n * What each domain's records need, compared with what is there: creates,\n * updates, and the deletes a change of mode needs — a name moving from an A\n * record to a CNAME, or back, cannot hold both. Nothing else is deleted.\n */\nexport function diffDomainDns(\n\tplan: DomainDnsPlan,\n\texisting: readonly DnsRecord[],\n): DnsChange[] {\n\tconst changes: DnsChange[] = [];\n\tconst find = (name: string, type: string) =>\n\t\texisting.find((r) => r.name === name && r.type === type);\n\n\tconst planned = new Map<string, Set<StackRecordType>>();\n\tfor (const record of plan.records) {\n\t\tconst types = planned.get(record.name) ?? new Set();\n\t\ttypes.add(record.type);\n\t\tplanned.set(record.name, types);\n\t}\n\n\t// The deletes first: a CNAME where an A record goes, A and AAAA where a\n\t// CNAME goes.\n\tfor (const [name, types] of planned) {\n\t\tconst conflicting: StackRecordType[] = types.has('CNAME')\n\t\t\t? ['A', 'AAAA']\n\t\t\t: ['CNAME'];\n\t\tfor (const type of conflicting) {\n\t\t\tconst found = find(name, type);\n\t\t\tif (!found || found.values.length === 0) continue;\n\t\t\tchanges.push({\n\t\t\t\tdomain: plan.domain,\n\t\t\t\tname,\n\t\t\t\ttype,\n\t\t\t\taction: 'delete',\n\t\t\t\tprevious: found.values.join(', '),\n\t\t\t});\n\t\t}\n\t}\n\n\tfor (const record of plan.records) {\n\t\tconst found = find(record.name, record.type);\n\t\tconst base = {\n\t\t\tdomain: plan.domain,\n\t\t\tname: record.name,\n\t\t\ttype: record.type,\n\t\t\tvalue: record.value,\n\t\t\tttl: record.ttl,\n\t\t};\n\t\tif (!found || found.values.length === 0) {\n\t\t\tchanges.push({ ...base, action: 'create' });\n\t\t} else if (\n\t\t\tfound.values.length === 1 &&\n\t\t\tsame(found.values[0]!, record.value)\n\t\t) {\n\t\t\tchanges.push({ ...base, action: 'unchanged' });\n\t\t} else {\n\t\t\tchanges.push({\n\t\t\t\t...base,\n\t\t\t\taction: 'update',\n\t\t\t\tprevious: found.values.join(', '),\n\t\t\t});\n\t\t}\n\t}\n\treturn changes;\n}\n\n/** A host's full name from a domain and a relative name. */\nfunction fullName(domain: string, name: string): string {\n\treturn name === '@' ? domain : `${name}.${domain}`;\n}\n\nfunction printChange(\n\tchange: DnsChange,\n\tdryRun: boolean,\n\tlog: (line: string) => void,\n) {\n\tconst host = fullName(change.domain, change.name).padEnd(32);\n\tconst type = change.type.padEnd(5);\n\tconst would = dryRun ? 'would ' : '';\n\tswitch (change.action) {\n\t\tcase 'unchanged':\n\t\t\tlog(\n\t\t\t\t` ✓ ${host} ${type} ${change.value} (TTL ${change.ttl}) — up to date`,\n\t\t\t);\n\t\t\tbreak;\n\t\tcase 'create':\n\t\t\tlog(\n\t\t\t\t` + ${host} ${type} ${change.value} (TTL ${change.ttl}) — ${would}create`,\n\t\t\t);\n\t\t\tbreak;\n\t\tcase 'update':\n\t\t\tlog(\n\t\t\t\t` ~ ${host} ${type} ${change.previous} → ${change.value} (TTL ${change.ttl}) — ${would}update`,\n\t\t\t);\n\t\t\tbreak;\n\t\tcase 'write':\n\t\t\tlog(\n\t\t\t\tdryRun\n\t\t\t\t\t? ` ? ${host} ${type} ${change.value} (TTL ${change.ttl}) — would write; current value unknown (key cannot read DNS)`\n\t\t\t\t\t: ` ! ${host} ${type} ${change.value} (TTL ${change.ttl}) — written (not diffed)`,\n\t\t\t);\n\t\t\tbreak;\n\t\tcase 'delete':\n\t\t\tlog(\n\t\t\t\t` - ${host} ${type} ${change.previous} — ${would}delete (a name cannot hold a CNAME and an A record)`,\n\t\t\t);\n\t\t\tbreak;\n\t}\n}\n\n/**\n * Each domain's records, through its provider. Returns every change, the\n * unchanged included; a dry run makes none of them.\n */\nexport async function applyStackDns(\n\tplan: StackDnsPlan,\n\toptions: ApplyStackDnsOptions = {},\n): Promise<DnsChange[]> {\n\treturn (await settleStackDns(plan, options)).changes;\n}\n\n/**\n * Each domain's records, through its provider — and, with `confirm`, what was\n * written read back from it. A record already holding its value is neither\n * written nor read again.\n *\n * @throws {DnsRecordsNotConfirmed} when a written record does not read back\n */\nexport async function settleStackDns(\n\tplan: StackDnsPlan,\n\toptions: ApplyStackDnsOptions = {},\n): Promise<StackDnsApplied> {\n\tconst dryRun = options.dryRun === true;\n\tconst log = options.log ?? ((line: string) => console.log(line));\n\tconst providerFor =\n\t\toptions.providerFor ??\n\t\t((config: DnsProviderConfig) =>\n\t\t\tcreateDnsProvider({\n\t\t\t\tconfig: config as SchemaDnsConfig,\n\t\t\t\t...(options.credentials ? { godaddy: options.credentials } : {}),\n\t\t\t\t...(options.hostinger ? { hostinger: options.hostinger } : {}),\n\t\t\t}));\n\n\tconst changes: DnsChange[] = [];\n\tconst confirmed = new Set<string>();\n\tconst unconfirmed = new Set<string>();\n\tconst hostsOf = (domain: DomainDnsPlan) =>\n\t\tnew Set(domain.records.map((r) => r.host));\n\tfor (const host of plan.uncovered) {\n\t\tlog(\n\t\t\t` ⚠ ${host} is under no domain in dns — point it at ${plan.server.ipv4} yourself`,\n\t\t);\n\t}\n\n\tfor (const domain of plan.domains) {\n\t\tif (domain.provider === 'manual') {\n\t\t\tchanges.push(...printManual(domain, log, 'manual'));\n\t\t\tfor (const host of hostsOf(domain)) unconfirmed.add(host);\n\t\t\tcontinue;\n\t\t}\n\t\tconst provider = await providerFor(domain.config);\n\t\tif (!provider) {\n\t\t\tchanges.push(...printManual(domain, log, 'manual'));\n\t\t\tfor (const host of hostsOf(domain)) unconfirmed.add(host);\n\t\t\tcontinue;\n\t\t}\n\n\t\tlog(` ${domain.domain} (${provider.name})${dryRun ? ' — dry run' : ''}`);\n\t\tlet existing: DnsRecord[];\n\t\tlet diff: DnsChange[];\n\t\tlet unreadable = false;\n\t\ttry {\n\t\t\texisting = await existingRecords(provider, domain);\n\t\t\tdiff = diffDomainDns(domain, existing);\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof DnsRecordsUnreadable)) throw error;\n\t\t\tlog(\n\t\t\t\t` ℹ The ${provider.name} key cannot read ${domain.domain}'s DNS records, so changes cannot be diffed: every record is written (idempotent).`,\n\t\t\t);\n\t\t\tunreadable = true;\n\t\t\texisting = [];\n\t\t\tdiff = domain.records.map((r) => ({\n\t\t\t\tdomain: r.domain,\n\t\t\t\tname: r.name,\n\t\t\t\ttype: r.type,\n\t\t\t\taction: 'write' as const,\n\t\t\t\tvalue: r.value,\n\t\t\t\tttl: r.ttl,\n\t\t\t}));\n\t\t}\n\t\tfor (const change of diff) printChange(change, dryRun, log);\n\n\t\t// An AAAA nobody planned, at a name that points here: IPv6 clients —\n\t\t// Let's Encrypt among them — would go somewhere else.\n\t\tif (!plan.server.ipv6) {\n\t\t\tfor (const name of new Set(domain.records.map((r) => r.name))) {\n\t\t\t\tconst aaaa = existing.find((r) => r.name === name && r.type === 'AAAA');\n\t\t\t\tconst planned = domain.records.find(\n\t\t\t\t\t(r) => r.name === name && r.type === 'A',\n\t\t\t\t);\n\t\t\t\tif (aaaa && planned && aaaa.values.length > 0) {\n\t\t\t\t\tlog(\n\t\t\t\t\t\t` ⚠ ${fullName(domain.domain, name)} also has AAAA ${aaaa.values.join(', ')}: ` +\n\t\t\t\t\t\t\t`set ${SERVER_IPV6_KEY} in the '${plan.stage}' stage's secrets to the server's IPv6 address, ` +\n\t\t\t\t\t\t\t'or remove that record, or IPv6 clients go elsewhere',\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tchanges.push(...diff);\n\n\t\t// A host every one of whose records was already right is confirmed by\n\t\t// the read that found it so; one with a write is confirmed by reading\n\t\t// it back — or, with a key that cannot read, by nobody.\n\t\tconst written = new Set(\n\t\t\tdiff\n\t\t\t\t.filter((c) => c.action !== 'unchanged')\n\t\t\t\t.map((c) => fullName(c.domain, c.name)),\n\t\t);\n\t\tfor (const host of hostsOf(domain)) {\n\t\t\t// A dry run writes nothing, so nothing it would write is resolved:\n\t\t\t// a name looked up before its record exists is cached as missing.\n\t\t\tif (unreadable) {\n\t\t\t\tif (!dryRun) unconfirmed.add(host);\n\t\t\t} else if (!written.has(host)) confirmed.add(host);\n\t\t}\n\t\tif (dryRun) continue;\n\n\t\tconst deletes = diff.filter((c) => c.action === 'delete');\n\t\tif (deletes.length > 0) {\n\t\t\tawait provider.deleteRecords(\n\t\t\t\tdomain.domain,\n\t\t\t\tdeletes.map((c) => ({ name: c.name, type: c.type })),\n\t\t\t);\n\t\t}\n\t\tconst writes = diff.filter(\n\t\t\t(c) =>\n\t\t\t\tc.action === 'create' || c.action === 'update' || c.action === 'write',\n\t\t);\n\t\tif (writes.length > 0) {\n\t\t\tawait provider.upsertRecords(\n\t\t\t\tdomain.domain,\n\t\t\t\twrites.map((c) => ({\n\t\t\t\t\tname: c.name,\n\t\t\t\t\ttype: c.type,\n\t\t\t\t\tvalue: c.value!,\n\t\t\t\t\tttl: c.ttl!,\n\t\t\t\t})),\n\t\t\t);\n\t\t}\n\n\t\tif (!options.confirm || unreadable || written.size === 0) continue;\n\t\tconst rewritten = {\n\t\t\t...domain,\n\t\t\trecords: domain.records.filter((r) => written.has(r.host)),\n\t\t};\n\t\tconst after = await existingRecords(provider, rewritten);\n\t\tconst missing: UnconfirmedRecord[] = [];\n\t\tfor (const record of rewritten.records) {\n\t\t\tconst found = after.find(\n\t\t\t\t(r) => r.name === record.name && r.type === record.type,\n\t\t\t);\n\t\t\tconst values = found?.values ?? [];\n\t\t\tif (values.length !== 1 || !same(values[0]!, record.value)) {\n\t\t\t\tmissing.push({\n\t\t\t\t\thost: record.host,\n\t\t\t\t\ttype: record.type,\n\t\t\t\t\texpected: record.value,\n\t\t\t\t\tfound: values,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (missing.length > 0) {\n\t\t\tthrow new DnsRecordsNotConfirmed(\n\t\t\t\tplan.stage,\n\t\t\t\tdomain.domain,\n\t\t\t\tprovider.name,\n\t\t\t\tmissing,\n\t\t\t);\n\t\t}\n\t\tfor (const host of written) confirmed.add(host);\n\t\tlog(\n\t\t\t` ✓ ${written.size} host${written.size === 1 ? '' : 's'} read back from ${provider.name}`,\n\t\t);\n\t}\n\tfor (const host of plan.uncovered) unconfirmed.add(host);\n\treturn {\n\t\tchanges,\n\t\tconfirmed: [...confirmed].sort(),\n\t\tunconfirmed: [...unconfirmed].filter((h) => !confirmed.has(h)).sort(),\n\t};\n}\n\n// ============================================================================\n// The check\n// ============================================================================\n\n/** Resolves a host to every address it has — `dns.promises.lookup`, `all`. */\nexport type HostLookup = (host: string) => Promise<string[]>;\n\n/** The system resolver: what the server's own clients would be told. */\nexport const systemLookup: HostLookup = async (host) => {\n\tconst found = await dnsLookup(host, { all: true });\n\treturn found.map((a) => a.address);\n};\n\n/** One host that does not resolve to the server. */\nexport interface MisdirectedHost {\n\thost: string;\n\t/** What it resolves to — empty when it does not resolve. */\n\tresolved: string[];\n\t/** Why it did not resolve, when it did not. */\n\terror?: string;\n}\n\n/** Hosts that do not resolve to the stage's server. */\nexport class HostNotPointingAtServer extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly hosts: readonly MisdirectedHost[],\n\t\treadonly expected: { ipv4: string; ipv6?: string },\n\t\t/** The records to create, where `dns` covers none of them. */\n\t\treadonly fix: string,\n\t) {\n\t\tconst want = expected.ipv6\n\t\t\t? `${expected.ipv4} (and ${expected.ipv6})`\n\t\t\t: expected.ipv4;\n\t\tconst lines = hosts.map(\n\t\t\t(h) =>\n\t\t\t\t` - ${h.host} → ${h.resolved.length > 0 ? h.resolved.join(', ') : `nothing${h.error ? ` (${h.error})` : ''}`}, expected ${want}`,\n\t\t);\n\t\tsuper(\n\t\t\t`${hosts.length === 1 ? 'A host' : `${hosts.length} hosts`} of '${stage}' ` +\n\t\t\t\t`do${hosts.length === 1 ? 'es' : ''} not resolve to its server, so a ` +\n\t\t\t\t`certificate for ${hosts.length === 1 ? 'it' : 'them'} cannot be issued:\\n` +\n\t\t\t\t`${lines.join('\\n')}\\n${fix}\\n` +\n\t\t\t\t'Records can take a few minutes to propagate. With a CDN or proxy in ' +\n\t\t\t\t'front of the server, pass --skip-dns.',\n\t\t);\n\t\tthis.name = 'HostNotPointingAtServer';\n\t}\n}\n\nexport interface CheckStackDnsInput {\n\tstage: string;\n\thosts: readonly string[];\n\tserver: ServerAddress;\n\tdns: WorkspaceDnsConfig | undefined;\n\tlookup?: HostLookup;\n}\n\n/**\n * Every host resolved: each must resolve to the server's IPv4 address and to\n * nothing but the server's addresses.\n *\n * @throws {HostNotPointingAtServer} naming every host that does not\n */\nexport async function checkStackDns(\n\tinput: CheckStackDnsInput,\n): Promise<string[]> {\n\tconst lookup = input.lookup ?? systemLookup;\n\tconst { server } = input;\n\tconst ours = new Set(\n\t\t[server.ipv4, server.ipv6].filter(Boolean).map((a) => a!.toLowerCase()),\n\t);\n\tconst bad: MisdirectedHost[] = [];\n\tconst ok: string[] = [];\n\tfor (const host of input.hosts) {\n\t\tlet resolved: string[];\n\t\ttry {\n\t\t\tresolved = [...new Set(await lookup(host))];\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\terror && typeof error === 'object' && 'code' in error\n\t\t\t\t\t? String((error as { code: unknown }).code)\n\t\t\t\t\t: error instanceof Error\n\t\t\t\t\t\t? error.message\n\t\t\t\t\t\t: String(error);\n\t\t\tbad.push({ host, resolved: [], error: code });\n\t\t\tcontinue;\n\t\t}\n\t\tconst pointsHere =\n\t\t\tresolved.some((a) => a.toLowerCase() === server.ipv4.toLowerCase()) &&\n\t\t\tresolved.every((a) => ours.has(a.toLowerCase()));\n\t\tif (pointsHere) ok.push(host);\n\t\telse bad.push({ host, resolved });\n\t}\n\n\tif (bad.length > 0) {\n\t\tthrow new HostNotPointingAtServer(\n\t\t\tinput.stage,\n\t\t\tbad,\n\t\t\t{ ipv4: server.ipv4, ...(server.ipv6 ? { ipv6: server.ipv6 } : {}) },\n\t\t\tdnsFix(input, bad),\n\t\t);\n\t}\n\treturn ok;\n}\n\n/** How to fix it: the deploy, or the records to create by hand. */\nfunction dnsFix(input: CheckStackDnsInput, bad: readonly MisdirectedHost[]) {\n\tconst hosts = bad.map((h) => h.host);\n\tlet plan: StackDnsPlan;\n\ttry {\n\t\tplan = planStackDns({\n\t\t\tstage: input.stage,\n\t\t\thosts,\n\t\t\tserver: input.server,\n\t\t\tdns: input.dns,\n\t\t});\n\t} catch {\n\t\tplan = {\n\t\t\tstage: input.stage,\n\t\t\tserver: input.server,\n\t\t\tdomains: [],\n\t\t\tuncovered: hosts,\n\t\t};\n\t}\n\tconst automatic = plan.domains.filter((d) => d.provider !== 'manual');\n\tconst byHand = [\n\t\t...plan.domains\n\t\t\t.filter((d) => d.provider === 'manual')\n\t\t\t.flatMap((d) => d.records.map(describe)),\n\t\t...plan.uncovered.flatMap((host) => [\n\t\t\t`${host.padEnd(32)} A ${input.server.ipv4}`,\n\t\t\t...(input.server.ipv6\n\t\t\t\t? [`${host.padEnd(32)} AAAA ${input.server.ipv6}`]\n\t\t\t\t: []),\n\t\t]),\n\t];\n\tconst lines: string[] = [];\n\tif (automatic.length > 0) {\n\t\tlines.push(\n\t\t\t`Fix: gkm deploy --stage ${input.stage} writes the records for ${automatic.map((d) => d.domain).join(', ')} through their provider (--dry-run shows them first).`,\n\t\t);\n\t}\n\tif (byHand.length > 0) {\n\t\tlines.push(\n\t\t\t`${automatic.length > 0 ? 'And create' : 'Fix: create'} these records at the DNS host:`,\n\t\t\t...byHand.map((line) => ` ${line}`),\n\t\t);\n\t}\n\treturn lines.join('\\n');\n}\n\n// ============================================================================\n// The step: plan, apply, confirm, check\n// ============================================================================\n\n/** The environment variable each token-authenticated provider reads. */\nexport const DNS_TOKEN_KEYS = {\n\tgodaddy: 'GODADDY_API_TOKEN',\n\thostinger: 'HOSTINGER_API_TOKEN',\n} as const;\n\n/** A provider whose credential is a token gkm reads. */\nexport type TokenDnsProvider = keyof typeof DNS_TOKEN_KEYS;\n\n/** The tokens a stage's records are written with. */\nexport interface DnsCredentials {\n\tgodaddy?: GoDaddyCredential;\n\thostinger?: HostingerCredential;\n}\n\n/**\n * A domain whose records the deploy writes, and no token to write them with.\n * Raised before anything is built, started or written.\n */\nexport class DnsCredentialMissing extends GkmError {\n\tconstructor(\n\t\treadonly stage: string,\n\t\treadonly domain: string,\n\t\treadonly provider: TokenDnsProvider,\n\t\t/** The variable the token is read from: `GODADDY_API_TOKEN`. */\n\t\treadonly key: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${stage}''s hosts are under ${domain}, whose DNS is ${provider}'s, and ` +\n\t\t\t\t`the deploy writes their records — but ${key} is not set. In CI, add ` +\n\t\t\t\t`${key} as a secret on the '${stage}' GitHub environment and pass it ` +\n\t\t\t\t`to the step that writes the records: env: { ${key}: \\${{ secrets.${key} }} }. ` +\n\t\t\t\t`On this machine, export ${key} or run gkm login --provider ${provider}. ` +\n\t\t\t\t'With a CDN or proxy in front of the server, pass --skip-dns.',\n\t\t);\n\t\tthis.name = 'DnsCredentialMissing';\n\t}\n}\n\n/** One `dns` domain and its provider's config. */\nexport interface DnsDomain {\n\tdomain: string;\n\tconfig: DnsProviderConfig;\n}\n\n/**\n * The `dns` domain a stage's own domain (`domains.<stage>`) is under — where\n * its hosts' records go — or none.\n */\nexport function stageDnsDomains(\n\tstage: string,\n\tdomains: Readonly<Record<string, string>> | undefined,\n\tdns: WorkspaceDnsConfig | undefined,\n): DnsDomain[] {\n\tconst own = domains?.[stage];\n\tif (!own || !dns || isLocalHost(own)) return [];\n\tconst normalized = normalizeDnsConfig(dns);\n\tconst root = findRootDomain(own.toLowerCase(), normalized);\n\treturn root ? [{ domain: root, config: normalized[root]! }] : [];\n}\n\n/**\n * Each token the domains' providers need, from `credentials` — `manual`,\n * Route53 (the AWS SDK's own chain) and a provider object need none.\n *\n * @throws {DnsCredentialMissing} naming the first domain with no token\n */\nexport async function dnsCredentials(\n\tstage: string,\n\tdomains: readonly DnsDomain[],\n\tcredentials: CredentialProvider,\n): Promise<DnsCredentials> {\n\tconst found: DnsCredentials = {};\n\tfor (const { domain, config } of domains) {\n\t\tconst provider = config.provider;\n\t\tif (provider !== 'godaddy' && provider !== 'hostinger') continue;\n\t\tif (found[provider]) continue;\n\t\tconst token = await credentials.get({ kind: provider });\n\t\tif (!token?.token) {\n\t\t\tthrow new DnsCredentialMissing(\n\t\t\t\tstage,\n\t\t\t\tdomain,\n\t\t\t\tprovider,\n\t\t\t\tDNS_TOKEN_KEYS[provider],\n\t\t\t);\n\t\t}\n\t\tfound[provider] = token;\n\t}\n\treturn found;\n}\n\nexport interface StageDnsInput {\n\tstage: string;\n\t/** The stage's public hosts, read off its stack. */\n\thosts: readonly string[];\n\tserver: ServerAddress;\n\tdns: WorkspaceDnsConfig | undefined;\n\t/** Where the providers' tokens come from. */\n\tcredentials: CredentialProvider;\n\tdryRun?: boolean;\n\tlog?: (line: string) => void;\n\t/** Resolves the hosts no provider confirmed — the system resolver by default. */\n\tlookup?: HostLookup;\n\t/** The provider for a domain — `createDnsProvider` by default. */\n\tproviderFor?: (config: DnsProviderConfig) => Promise<DnsProvider | null>;\n\t/** GoDaddy's API, for tests: its base URL and how a 429 is waited out. */\n\tgodaddy?: Pick<GoDaddyProviderOptions, 'baseUrl' | 'sleep' | 'maxAttempts'>;\n}\n\nexport interface StageDnsResult {\n\tplan: StackDnsPlan;\n\tchanges: DnsChange[];\n\t/** Hosts whose records their provider holds with the server's address. */\n\tconfirmed: string[];\n\t/** Hosts the system resolver found pointing at the server. */\n\tresolved: string[];\n}\n\n/**\n * A stage's DNS, as every deploy runs it: the\n * stage's own hosts planned, written through each domain's provider where\n * they differ, and read back from it. Only A, AAAA and CNAME records of\n * those names are touched; nothing is a wildcard, and no other name is read,\n * written or deleted. What no provider confirms — a `manual` domain, a host\n * under no domain in `dns`, a key that cannot read — is resolved publicly.\n * A dry run prints the plan and writes nothing.\n *\n * @throws {DnsCredentialMissing} before anything is read or written\n * @throws {DnsRecordsNotConfirmed} when a written record does not read back\n * @throws {HostNotPointingAtServer} when a resolved host is not the server\n */\nexport async function stageDns(input: StageDnsInput): Promise<StageDnsResult> {\n\tconst { stage, server } = input;\n\tconst log = input.log ?? ((line: string) => console.log(line));\n\tconst plan = planStackDns({\n\t\tstage,\n\t\thosts: input.hosts,\n\t\tserver,\n\t\tdns: input.dns,\n\t});\n\tconst tokens = await dnsCredentials(\n\t\tstage,\n\t\tplan.domains.map(({ domain, config }) => ({ domain, config })),\n\t\tinput.credentials,\n\t);\n\t// No `dns` at all: nothing to write, and every host is the person's to\n\t// point — checked, without a line per host saying so.\n\tconst applied: StackDnsApplied = !input.dns\n\t\t? { changes: [], confirmed: [], unconfirmed: [...plan.uncovered] }\n\t\t: await settleStackDns(plan, {\n\t\t\t\t...(input.dryRun ? { dryRun: true } : { confirm: true }),\n\t\t\t\tlog,\n\t\t\t\tcredentials: {\n\t\t\t\t\t...(input.godaddy ?? {}),\n\t\t\t\t\t...(tokens.godaddy ? { credential: tokens.godaddy } : {}),\n\t\t\t\t\tlog,\n\t\t\t\t},\n\t\t\t\t...(tokens.hostinger ? { hostinger: tokens.hostinger } : {}),\n\t\t\t\t...(input.providerFor ? { providerFor: input.providerFor } : {}),\n\t\t\t});\n\n\tlet resolved: string[] = [];\n\tif (applied.unconfirmed.length > 0) {\n\t\tresolved = await checkStackDns({\n\t\t\tstage,\n\t\t\thosts: applied.unconfirmed,\n\t\t\tserver,\n\t\t\tdns: input.dns,\n\t\t\t...(input.lookup ? { lookup: input.lookup } : {}),\n\t\t});\n\t}\n\treturn {\n\t\tplan,\n\t\tchanges: applied.changes,\n\t\tconfirmed: applied.confirmed,\n\t\tresolved,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"serverSetup-CKs4K9gD.cjs","names":["GkmError","spawn","join","homedir","ghCli","prompt","resolve","loadWorkspaceConfig","composeServer","secretsStoreFor","createStateStore","existsSync","describeActor","mkdir","dirname","readFile","writeFile","rm","DeployJournal","createComposeState"],"sources":["../src/compose/serverScript.ts","../src/compose/serverSetup.ts"],"sourcesContent":["/**\n * What `gkm compose:server` runs on the server, as root (through the admin\n * user's sudo). Plain bash, so `--dry-run` can print exactly what will run,\n * and safe to run again: every step changes only what is missing or wrong.\n *\n * Arguments, in order: the admin user (the one running sudo), the deploy\n * user, the SSH port, the deploy key's public line, and its comment.\n *\n * The last line it prints is `HOSTKEY=<fingerprint>` — the server's own\n * ed25519 host key, which the command checks the network's against before\n * pinning it for CI.\n */\nexport const SERVER_SCRIPT = String.raw`set -euo pipefail\nexport DEBIAN_FRONTEND=noninteractive\nADMIN=\"$1\"; DEPLOY=\"$2\"; SSH_PORT=\"$3\"; PUB=\"$4\"; COMMENT=\"$5\"\nsay() { echo \" $*\"; }\nfail() { echo \" ✗ $*\" >&2; exit 1; }\n\n. /etc/os-release\ncase \"$ID\" in\n\tubuntu | debian) ;;\n\t*) fail \"gkm sets up Ubuntu and Debian servers; this one is $ID.\" ;;\nesac\n\n# ── SSH: keys only, no root ────────────────────────────────────────────────\n# Before password logins are turned off, the admin must have a key to come\n# back with — otherwise this would lock them out.\nADMIN_HOME=\"$(getent passwd \"$ADMIN\" | cut -d: -f6)\"\n[ -s \"$ADMIN_HOME/.ssh/authorized_keys\" ] ||\n\tfail \"$ADMIN has no key in $ADMIN_HOME/.ssh/authorized_keys; turning off password logins would lock them out.\"\n# First match wins in sshd_config, and 00- sorts before what a provider ships\n# (cloud-init's 50-cloud-init.conf turns password logins back on).\ncat > /etc/ssh/sshd_config.d/00-gkm-hardening.conf <<'EOF'\n# Written by gkm compose:server.\nPermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nEOF\n# The deploy migrates through an SSH tunnel to the server's loopback.\ncat > /etc/ssh/sshd_config.d/60-gkm-deploy.conf <<EOF\n# Written by gkm compose:server.\nMatch User $DEPLOY\n AllowTcpForwarding local\nEOF\nsshd -t\nsystemctl reload ssh 2>/dev/null || systemctl reload sshd\n# What sshd actually does now, whatever else is in its config.\nEFFECTIVE=\"$(sshd -T)\"\nfor setting in 'permitrootlogin no' 'passwordauthentication no' 'kbdinteractiveauthentication no'; do\n\tgrep -qx \"$setting\" <<<\"$EFFECTIVE\" || fail \"sshd still does not say '$setting'; another file in /etc/ssh overrides it.\"\ndone\nFORWARD=\"$(sshd -T -C \"user=$DEPLOY,host=localhost,addr=127.0.0.1\" | sed -n 's/^allowtcpforwarding //p')\"\ncase \"$FORWARD\" in\n\tyes | all | local) ;;\n\t*) fail \"sshd does not let $DEPLOY forward ports (allowtcpforwarding $FORWARD).\" ;;\nesac\nsay \"sshd: keys only, no root login; $DEPLOY may open local tunnels\"\n# Root is reached through the admin's sudo, which this script is running\n# under — so its password, the one a provider mails out, can go.\npasswd -l root >/dev/null\nsay \"root's password locked; $ADMIN's sudo is the way to root\"\n\n# ── Firewall: SSH, HTTP, HTTPS ─────────────────────────────────────────────\ncommand -v ufw >/dev/null || { apt-get update -qq && apt-get install -y -qq ufw >/dev/null; }\nufw default deny incoming >/dev/null\nufw default allow outgoing >/dev/null\nufw allow \"$SSH_PORT/tcp\" >/dev/null\nufw allow 80/tcp >/dev/null\nufw allow 443/tcp >/dev/null\nufw --force enable >/dev/null\nsay \"firewall: $SSH_PORT, 80, 443 in; everything out\"\nEXTRA=\"$(ufw status | awk 'NR > 4 && $1 != \"\" { print $1 }' | grep -vxE \"(($SSH_PORT|80|443)(/tcp)?|OpenSSH)\" | sort -u || true)\"\nif [ -n \"$EXTRA\" ]; then\n\tsay \"⚠ also open: $(echo $EXTRA). gkm uses none of them; close each with: sudo ufw delete allow <port>\"\nfi\n\n# ── Updates and bans ───────────────────────────────────────────────────────\napt-get update -qq\napt-get install -y -qq unattended-upgrades fail2ban >/dev/null\ncat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'\nAPT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Unattended-Upgrade \"1\";\nEOF\ncat > /etc/fail2ban/jail.d/gkm.local <<EOF\n# Written by gkm compose:server.\n[sshd]\nenabled = true\nport = $SSH_PORT\nEOF\nsystemctl enable --now unattended-upgrades fail2ban >/dev/null 2>&1\nsystemctl restart fail2ban\nsay \"security updates install daily; fail2ban watches sshd\"\n\n# ── Docker, from Docker's own repository ───────────────────────────────────\nif ! command -v docker >/dev/null || ! docker compose version >/dev/null 2>&1; then\n\tsay \"installing Docker…\"\n\tapt-get install -y -qq ca-certificates curl >/dev/null\n\tinstall -m 0755 -d /etc/apt/keyrings\n\tcurl -fsSL \"https://download.docker.com/linux/$ID/gpg\" -o /etc/apt/keyrings/docker.asc\n\tchmod a+r /etc/apt/keyrings/docker.asc\n\techo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$ID $VERSION_CODENAME stable\" \\\n\t\t> /etc/apt/sources.list.d/docker.list\n\tapt-get update -qq\n\tapt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null\nfi\nsystemctl enable --now docker >/dev/null 2>&1\nsay \"docker $(docker version --format '{{.Server.Version}}')\"\n\n# ── The deploy user: no password, no sudo, in the docker group ─────────────\nid \"$DEPLOY\" >/dev/null 2>&1 || adduser --disabled-password --gecos \"\" \"$DEPLOY\" >/dev/null\nusermod -aG docker \"$DEPLOY\"\nDEPLOY_HOME=\"$(getent passwd \"$DEPLOY\" | cut -d: -f6)\"\ninstall -d -m 700 -o \"$DEPLOY\" -g \"$DEPLOY\" \"$DEPLOY_HOME/.ssh\"\nAUTH=\"$DEPLOY_HOME/.ssh/authorized_keys\"\ntouch \"$AUTH\"\n# One managed line: this key (or an earlier one by the same name) replaced,\n# with nothing allowed but the commands and the tunnel the deploy uses.\nKEYBODY=\"$(awk '{ print $2 }' <<<\"$PUB\")\"\ngrep -v -F -e \"$KEYBODY\" -e \" $COMMENT\" \"$AUTH\" > \"$AUTH.new\" || true\necho \"restrict,port-forwarding $PUB\" >> \"$AUTH.new\"\nmv \"$AUTH.new\" \"$AUTH\"\nchown \"$DEPLOY:$DEPLOY\" \"$AUTH\"\nchmod 600 \"$AUTH\"\nsay \"user $DEPLOY (docker group), deploy key authorized with restrict,port-forwarding\"\nOTHERS=\"$(grep -v -F -e \"$KEYBODY\" \"$AUTH\" | grep -c . || true)\"\nif [ \"$OTHERS\" -gt 0 ]; then\n\tsay \"⚠ $AUTH holds $OTHERS other key(s). Review: sudo cat $AUTH\"\nfi\n\necho \"HOSTKEY=$(ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub | awk '{ print $2 }')\"\n`;\n\n/**\n * What runs as root to give a fresh server its admin — the guide's first\n * step, for a server only root can log in to yet: a sudo user, with root's\n * keys so the same key logs in. `adduser` asks for its password, which sudo\n * will ask for.\n */\nexport const ADMIN_SCRIPT = `set -euo pipefail\nADMIN=\"$1\"\nif ! id \"$ADMIN\" >/dev/null 2>&1; then\n\techo \" creating $ADMIN — choose the password sudo will ask for:\"\n\tadduser --gecos \"\" \"$ADMIN\"\nfi\nusermod -aG sudo \"$ADMIN\"\nHOME_DIR=\"$(getent passwd \"$ADMIN\" | cut -d: -f6)\"\ninstall -d -m 700 -o \"$ADMIN\" -g \"$ADMIN\" \"$HOME_DIR/.ssh\"\nAUTH=\"$HOME_DIR/.ssh/authorized_keys\"\ntouch \"$AUTH\"\n# The keys alone: a provider's options on root's lines (a command=\"…\" that\n# says \"log in as ubuntu\") would stop the admin's login too. A server root\n# logs in to with a password has none; ssh-copy-id brings the admin's.\n{ grep -oE '(ssh|ecdsa|sk)-[^ ]+ [A-Za-z0-9+/=]+( .*)?$' /root/.ssh/authorized_keys 2>/dev/null || true; } |\n\twhile IFS= read -r key; do\n\t\tgrep -qxF \"$key\" \"$AUTH\" || echo \"$key\" >> \"$AUTH\"\n\tdone\nchown \"$ADMIN:$ADMIN\" \"$AUTH\"\nchmod 600 \"$AUTH\"\necho \" $ADMIN is a sudo user, with root's SSH keys (if root had any)\"\n`;\n","/**\n * `gkm compose:server` — make a VPS ready for a stage's compose deploys, from\n * this machine, in one go.\n *\n * It logs in as the server's admin — a sudo user, never root — and, through\n * sudo, sets the server up the way a production VPS should be: SSH by key\n * only with root's login off, a firewall open on SSH, 80 and 443 alone,\n * security updates installed daily, fail2ban on sshd, and Docker from\n * Docker's own repository. It then creates the stage's deploy user (in the\n * docker group, no sudo, no password) and authorizes a deploy key on it that\n * may run commands and open the tunnel migrations use, nothing more.\n *\n * Off the server: it makes the deploy key pair here, pins the server's host\n * key — checked against the fingerprint the server reports of itself — and\n * gives both to the stage's GitHub environment as `DEPLOY_SSH_KEY` and\n * `DEPLOY_KNOWN_HOSTS`, which the generated deploy workflow reads. Last, it\n * logs in exactly as CI will, and checks Docker and the tunnel answer.\n *\n * A fresh server only root can log in to gets its admin first: as root —\n * with root's key, or its password on a brand-new server — it creates the\n * user, and `ssh-copy-id` puts your key on it when root had none to give.\n * Root's password is locked once the admin's sudo works.\n *\n * Once it has, the stage's state records that it is done (`server:compose`,\n * stamped with who and when) — nothing personal: the admin is whoever runs\n * it, with their own key.\n * Safe to run again: each step changes only what is missing or wrong.\n */\n\nimport { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { dirname, join, resolve } from 'node:path';\nimport { loadWorkspaceConfig } from '../config';\nimport { describeActor } from '../deploy/actor';\nimport { type Gh, ghCli } from '../deploy/github';\nimport { DeployJournal } from '../deploy/journal';\nimport { createStateStore, type StateStore } from '../deploy/StateStore';\nimport { createComposeState } from '../deploy/state';\nimport { GkmError } from '../errors';\nimport { prompt } from '../prompt';\nimport { secretsStoreFor } from '../secrets/store.js';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport { type ComposeServer, composeServer } from './server';\nimport { ADMIN_SCRIPT, SERVER_SCRIPT } from './serverScript';\n\nexport interface ComposeServerSetupOptions {\n\t/** The deployed stage whose server this is. */\n\tstage: string;\n\t/**\n\t * The server's sudo user. Absent, the user `ssh` logs in to the server as\n\t * by default (`~/.ssh/config`, else this machine's) — asked for only when\n\t * that is root.\n\t */\n\tadmin?: string;\n\t/** The key the admin logs in with. Absent, `ssh`'s own choice. */\n\tidentity?: string;\n\t/** The deploy key pair. Default `~/.ssh/gkm-<project>-<stage>`. */\n\tkeyFile?: string;\n\t/** `owner/name`; defaults to the repository `gh` sees here. */\n\trepo?: string;\n\t/** `--no-github`: leave the GitHub environment alone. */\n\tgithub?: boolean;\n\t/** Print the plan and the script; connect to nothing. */\n\tdryRun?: boolean;\n\t/** Apply without asking first. */\n\tyes?: boolean;\n\tcwd?: string;\n}\n\n/** What a finished command ran: output kept, its exit code. */\nexport interface Ran {\n\tcode: number;\n\tstdout: string;\n\tstderr: string;\n}\n\n/**\n * Runs a program. `interactive` gives it this terminal's stdin and stderr —\n * for a password sudo or adduser asks — while stdout is still kept, and\n * echoed as it comes.\n */\nexport type Runner = (\n\tcommand: string,\n\targs: readonly string[],\n\toptions?: { input?: string; interactive?: boolean },\n) => Promise<Ran>;\n\nexport interface ComposeServerSetupDeps {\n\trun: Runner;\n\tgh: Gh;\n\t/** Ask the person at the terminal. */\n\task: (question: string) => Promise<string>;\n\t/** The stage's state; the workspace's own store when absent. */\n\tstate: StateStore;\n}\n\n/** The stage's record of its server, as `compose:server` last set it up. */\nexport const SERVER_RECORD = 'server:compose';\n\n/**\n * What `server:compose` holds: which server was set up. Who set it up, and\n * when, are the record's own `updatedBy` and `updatedAt`.\n */\nexport interface ServerRecordData {\n\thost: string;\n}\n\nexport interface ComposeServerSetupResult {\n\tserver: ComposeServer;\n\tadmin: string;\n\tkeyFile: string;\n\t/** The pinned host key line, as `known_hosts` holds it. Absent on a dry run. */\n\tknownHosts?: string;\n\t/** The repository whose environment was given the key. */\n\trepo?: string;\n}\n\n/** The admin named is root, whose login this command turns off. */\nexport class ComposeServerAdminIsRoot extends GkmError {\n\tconstructor() {\n\t\tsuper(\n\t\t\t\"The admin can't be root: setting up the server turns root's SSH login \" +\n\t\t\t\t'off. Name the sudo user you log in as (--admin <user>). A fresh ' +\n\t\t\t\t'server with only root gets one: run this again with the name it ' +\n\t\t\t\t\"should have, and gkm offers to create it with root's key.\",\n\t\t);\n\t\tthis.name = 'ComposeServerAdminIsRoot';\n\t}\n}\n\n/** The admin named is the deploy user, which has no sudo. */\nexport class ComposeServerAdminIsDeployUser extends GkmError {\n\tconstructor(readonly user: string) {\n\t\tsuper(\n\t\t\t`'${user}' is the stage's deploy user (deploy.compose.server), which ` +\n\t\t\t\t'gkm creates without sudo. Name the sudo user you log in as with --admin.',\n\t\t);\n\t\tthis.name = 'ComposeServerAdminIsDeployUser';\n\t}\n}\n\n/** No key logs the admin in, and nothing was done to make one. */\nexport class ComposeServerAdminUnreachable extends GkmError {\n\tconstructor(\n\t\treadonly target: string,\n\t\treadonly stderr: string,\n\t) {\n\t\tsuper(\n\t\t\t`Could not log in as ${target} with a key (ssh said: ${stderr.trim() || 'nothing'}). ` +\n\t\t\t\t`Check \\`ssh ${target}\\` works without a password — copy your key with ` +\n\t\t\t\t`\\`ssh-copy-id ${target}\\` — then run this again.`,\n\t\t);\n\t\tthis.name = 'ComposeServerAdminUnreachable';\n\t}\n}\n\n/**\n * The server was set up already, and this person cannot log in to it as\n * the admin they named. Root's login and passwords are off there, so only\n * an admin can let them in.\n */\nexport class ComposeServerAccessNeeded extends GkmError {\n\tconstructor(\n\t\treadonly target: string,\n\t\treadonly setUpBy: string,\n\t\treadonly setUpOn: string,\n\t) {\n\t\tsuper(\n\t\t\t`${target} can't log in with your key, and the server was set up by ${setUpBy} on ${setUpOn} — ` +\n\t\t\t\t'root and password logins are off there. Ask them, or another admin, to add your ' +\n\t\t\t\t`public key to ${target.split('@')[0]}'s ~/.ssh/authorized_keys (or create you a sudo ` +\n\t\t\t\t'user with it), then run this again.',\n\t\t);\n\t\tthis.name = 'ComposeServerAccessNeeded';\n\t}\n}\n\n/** `ssh-keygen` could not make the deploy key pair. */\nexport class DeployKeyNotMade extends GkmError {\n\tconstructor(\n\t\treadonly keyFile: string,\n\t\treadonly stderr: string,\n\t) {\n\t\tsuper(\n\t\t\t`Could not make the deploy key ${keyFile} (ssh-keygen said: ${stderr.trim() || 'nothing'}). ` +\n\t\t\t\t'Pass another path with --key-file.',\n\t\t);\n\t\tthis.name = 'DeployKeyNotMade';\n\t}\n}\n\n/** The script on the server stopped before it finished. */\nexport class ComposeServerSetupFailed extends GkmError {\n\tconstructor(\n\t\treadonly target: string,\n\t\treadonly code: number,\n\t) {\n\t\tsuper(\n\t\t\t`Setting up ${target} stopped (exit ${code}); its output is above. ` +\n\t\t\t\t'Fix what it names and run this again — every step it already ' +\n\t\t\t\t'finished is left as it is.',\n\t\t);\n\t\tthis.name = 'ComposeServerSetupFailed';\n\t}\n}\n\n/** The host key the network shows is not the one the server holds. */\nexport class ComposeServerHostKeyMismatch extends GkmError {\n\tconstructor(\n\t\treadonly host: string,\n\t\treadonly server: string,\n\t\treadonly scanned: string,\n\t) {\n\t\tsuper(\n\t\t\t`${host}'s host key is ${server} on the server, but ${scanned || 'nothing'} ` +\n\t\t\t\t'over the network. Something between here and the server answered for ' +\n\t\t\t\t'it, or the server is not the one you logged in to. Nothing was given ' +\n\t\t\t\t'to GitHub; find out why before running this again.',\n\t\t);\n\t\tthis.name = 'ComposeServerHostKeyMismatch';\n\t}\n}\n\n/** The deploy key logged in, but cannot do what a deploy does. */\nexport class ComposeServerCheckFailed extends GkmError {\n\tconstructor(\n\t\treadonly target: string,\n\t\treadonly check: 'docker' | 'tunnel',\n\t\treadonly output: string,\n\t) {\n\t\tsuper(\n\t\t\tcheck === 'docker'\n\t\t\t\t? `The deploy key can't run docker as ${target} (ssh said: ${output.trim() || 'nothing'}).`\n\t\t\t\t: `The deploy key can't open a tunnel as ${target} (got: ${output.trim() || 'nothing'}). ` +\n\t\t\t\t\t\t\"Check its line in the deploy user's authorized_keys says \" +\n\t\t\t\t\t\t'restrict,port-forwarding, and that sshd allows it forwarding.',\n\t\t);\n\t\tthis.name = 'ComposeServerCheckFailed';\n\t}\n}\n\n/** `'…'`, for a value in a command line the remote shell reads. */\nfunction shellQuote(value: string): string {\n\treturn `'${value.replaceAll(\"'\", `'\\\\''`)}'`;\n}\n\n/**\n * The remote command that runs `script` with `args`: written to a temporary\n * file (so the script keeps the terminal for any password it asks), run,\n * and removed. `sudo` for everything but root.\n */\nexport function remoteCommand(\n\tscript: string,\n\targs: readonly string[],\n\tsudo: boolean,\n): string {\n\tconst encoded = Buffer.from(script).toString('base64');\n\treturn [\n\t\t`f=\"$(mktemp)\"`,\n\t\t`echo ${encoded} | base64 -d > \"$f\"`,\n\t\t`${sudo ? 'sudo ' : ''}bash \"$f\" ${args.map(shellQuote).join(' ')}; s=$?`,\n\t\t`rm -f \"$f\"`,\n\t\t'exit $s',\n\t].join('; ');\n}\n\nfunction sshOptions(server: ComposeServer): string[] {\n\treturn ['-o', 'ConnectTimeout=20', '-p', String(server.port)];\n}\n\n/** How the admin (or root) logs in: the server's options and their key. */\nfunction loginOptions(server: ComposeServer, identity?: string): string[] {\n\treturn [\n\t\t...sshOptions(server),\n\t\t...(identity ? ['-i', identity, '-o', 'IdentitiesOnly=yes'] : []),\n\t];\n}\n\n/** `user@host`, as `ssh` takes it. */\nfunction at(user: string, host: string): string {\n\treturn `${user}@${host}`;\n}\n\n/** The real runner: the system's programs. */\nexport const systemRunner: Runner = (command, args, options = {}) =>\n\tnew Promise((resolve, reject) => {\n\t\tconst child = spawn(command, args, {\n\t\t\tstdio: [\n\t\t\t\toptions.interactive ? 'inherit' : 'pipe',\n\t\t\t\t'pipe',\n\t\t\t\toptions.interactive ? 'inherit' : 'pipe',\n\t\t\t],\n\t\t\ttimeout: options.interactive ? undefined : 60_000,\n\t\t});\n\t\tlet stdout = '';\n\t\tlet stderr = '';\n\t\tchild.stdout?.on('data', (chunk: Buffer) => {\n\t\t\tstdout += chunk.toString();\n\t\t\tif (options.interactive) process.stdout.write(chunk);\n\t\t});\n\t\tchild.stderr?.on('data', (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString();\n\t\t});\n\t\tchild.on('error', reject);\n\t\tchild.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));\n\t\tif (child.stdin) child.stdin.end(options.input ?? '');\n\t});\n\n/** Whether `answer` is a yes, `fallback` when nothing was typed. */\nfunction isYes(answer: string, fallback: boolean): boolean {\n\tconst value = answer.trim().toLowerCase();\n\treturn value === '' ? fallback : value === 'y' || value === 'yes';\n}\n\n/** The SHA256 fingerprint of each key in `keys`, by `ssh-keygen -lf -`. */\nasync function fingerprints(run: Runner, keys: string): Promise<string[]> {\n\tconst { stdout } = await run('ssh-keygen', ['-lf', '-'], { input: keys });\n\treturn stdout\n\t\t.split('\\n')\n\t\t.map((line) => line.split(' ')[1])\n\t\t.filter((value): value is string => Boolean(value));\n}\n\n/** The default deploy key pair for a stage. */\nexport function defaultKeyFile(project: string, stage: string): string {\n\treturn join(homedir(), '.ssh', `gkm-${project}-${stage}`);\n}\n\n/** `gkm compose:server`. */\nexport async function composeServerCommand(\n\toptions: ComposeServerSetupOptions,\n\tdeps: Partial<ComposeServerSetupDeps> = {},\n): Promise<ComposeServerSetupResult> {\n\tconst run = deps.run ?? systemRunner;\n\tconst gh = deps.gh ?? ghCli;\n\tconst ask =\n\t\tdeps.ask ??\n\t\t((question: string) =>\n\t\t\tprompt(question, {\n\t\t\t\tinstead: 'Name the server user with --admin and pass --yes.',\n\t\t\t}));\n\tconst github = options.github !== false;\n\tconst { stage } = options;\n\n\tconst cwd = resolve(options.cwd ?? process.cwd());\n\tconst { workspace } = await loadWorkspaceConfig(cwd);\n\tassertDeployedStage(workspace.stages, stage);\n\tconst server = composeServer({\n\t\tstage,\n\t\tlocal: false,\n\t\tconfig: workspace.deploy?.compose?.server,\n\t\tcustom: (await (await secretsStoreFor(workspace, stage)).read(stage))\n\t\t\t?.custom,\n\t})!;\n\n\t// Whether this server was set up before, by whom and when.\n\tconst state =\n\t\tdeps.state ??\n\t\t(await createStateStore({\n\t\t\tconfig: workspace.state,\n\t\t\tworkspaceRoot: workspace.root,\n\t\t\tworkspaceName: workspace.name,\n\t\t\tnamespace: workspace.deploy?.namespace,\n\t\t}));\n\tconst record = (await state.read(stage))?.resources[SERVER_RECORD];\n\tconst done =\n\t\t(record?.data as ServerRecordData | undefined)?.host === server.host\n\t\t\t? record\n\t\t\t: undefined;\n\n\t// The admin's login: `--admin`/`--identity`, or what `ssh` itself would\n\t// use for this host — read with `ssh -G`, which connects to nothing.\n\tconst identity = options.identity ? resolve(options.identity) : undefined;\n\tconst asAdmin = loginOptions(server, identity);\n\tconst sshUser =\n\t\t(await run('ssh', ['-G', ...asAdmin, server.host])).stdout.match(\n\t\t\t/^user (\\S+)$/m,\n\t\t)?.[1] ?? '';\n\tconst admin = (\n\t\toptions.admin ??\n\t\t(sshUser && sshUser !== 'root'\n\t\t\t? sshUser\n\t\t\t: await ask(`The sudo user you log in to ${server.host} as (not root): `))\n\t).trim();\n\tif (!admin || admin === 'root') throw new ComposeServerAdminIsRoot();\n\tif (admin === server.user) throw new ComposeServerAdminIsDeployUser(admin);\n\n\tconst keyFile = resolve(\n\t\toptions.keyFile ?? defaultKeyFile(workspace.name, stage),\n\t);\n\tconst comment = `gkm-${workspace.name}-${stage}-deploy`;\n\tconst repo = github\n\t\t? (options.repo ??\n\t\t\tgh(['repo', 'view', '--json', 'nameWithOwner', '-q', '.nameWithOwner']))\n\t\t: undefined;\n\tconst adminTarget = at(admin, server.host);\n\tconst deployTarget = at(server.user, server.host);\n\n\tconsole.log(`\\n🖥 Server for stage \"${stage}\"\\n`);\n\tconsole.log(` Server: ${server.host} (ssh port ${server.port})`);\n\tconsole.log(\n\t\t` Admin: ${admin} — logs in with ${identity ?? 'your SSH key'}, runs sudo${options.admin ? '' : ' (your SSH login)'}`,\n\t);\n\tconsole.log(\n\t\t` Deploy user: ${server.user} — docker group, no sudo, no password`,\n\t);\n\tconsole.log(\n\t\t` Deploy key: ${keyFile}${existsSync(keyFile) ? '' : ' (new, made here)'}`,\n\t);\n\tconsole.log(\n\t\t` GitHub: ${repo ? `environment \"${stage}\" of ${repo}: DEPLOY_SSH_KEY, DEPLOY_KNOWN_HOSTS` : 'left alone (--no-github)'}`,\n\t);\n\tif (done) {\n\t\tconsole.log(\n\t\t\t`\\n Already set up by ${describeActor(done.updatedBy)} on ${done.updatedAt.slice(0, 10)}; running again changes only what is missing or wrong.`,\n\t\t);\n\t}\n\tconsole.log('\\n On the server, through sudo:');\n\tfor (const step of [\n\t\t\"SSH: keys only, root's login off (and nothing else may turn passwords back on)\",\n\t\t`root's password locked — ${admin}'s sudo is the way to root`,\n\t\t`firewall: ${server.port}, 80 and 443 in, everything out`,\n\t\t'security updates installed daily (unattended-upgrades); fail2ban on sshd',\n\t\t\"Docker and its compose plugin, from Docker's repository\",\n\t\t`user ${server.user}, the deploy key authorized with restrict,port-forwarding`,\n\t]) {\n\t\tconsole.log(` · ${step}`);\n\t}\n\n\tif (options.dryRun) {\n\t\tconsole.log(\n\t\t\t`\\n The script, run as: sudo bash setup.sh ${[admin, server.user, String(server.port), '<deploy key>', comment].map(shellQuote).join(' ')}\\n`,\n\t\t);\n\t\tconsole.log(SERVER_SCRIPT.replace(/^/gm, ' '));\n\t\tconsole.log(' --dry-run: nothing changed.\\n');\n\t\treturn { server, admin, keyFile, ...(repo ? { repo } : {}) };\n\t}\n\n\tif (\n\t\t!options.yes &&\n\t\t!isYes(await ask(`\\n Set up ${server.host}? [y/N] `), false)\n\t) {\n\t\tconsole.log(' Nothing changed.\\n');\n\t\treturn { server, admin, keyFile, ...(repo ? { repo } : {}) };\n\t}\n\n\t// ── The deploy key pair, here ──────────────────────────────────────────\n\tif (!existsSync(keyFile)) {\n\t\tawait mkdir(dirname(keyFile), { recursive: true, mode: 0o700 });\n\t\tconst made = await run('ssh-keygen', [\n\t\t\t'-t',\n\t\t\t'ed25519',\n\t\t\t'-N',\n\t\t\t'',\n\t\t\t'-C',\n\t\t\tcomment,\n\t\t\t'-f',\n\t\t\tkeyFile,\n\t\t]);\n\t\tif (made.code !== 0) throw new DeployKeyNotMade(keyFile, made.stderr);\n\t\tconsole.log(`\\n ✓ Made the deploy key ${keyFile}`);\n\t}\n\tconst publicKey = (await readFile(`${keyFile}.pub`, 'utf-8')).trim();\n\n\t// ── The admin's login ──────────────────────────────────────────────────\n\t// Key only, and the host key accepted on first sight — as the first\n\t// `ssh` to a new server does. CI's is pinned and checked below.\n\tconst probe = (user: string) =>\n\t\trun('ssh', [\n\t\t\t'-o',\n\t\t\t'BatchMode=yes',\n\t\t\t'-o',\n\t\t\t'StrictHostKeyChecking=accept-new',\n\t\t\t...asAdmin,\n\t\t\tat(user, server.host),\n\t\t\t'true',\n\t\t]);\n\tlet login = await probe(admin);\n\t// A server already set up has no root or password login left to make\n\t// one with: an admin there has to let this person in.\n\tif (login.code !== 0 && done) {\n\t\tthrow new ComposeServerAccessNeeded(\n\t\t\tadminTarget,\n\t\t\tdescribeActor(done.updatedBy),\n\t\t\tdone.updatedAt.slice(0, 10),\n\t\t);\n\t}\n\tif (login.code !== 0) {\n\t\t// A server only root can log in to yet: root creates the admin — with\n\t\t// its key, or on a brand-new server with the password the provider\n\t\t// gave it, which this is the last time anything asks for.\n\t\tconst rootHasKey = (await probe('root')).code === 0;\n\t\tconst createAsRoot = isYes(\n\t\t\tawait ask(\n\t\t\t\trootHasKey\n\t\t\t\t\t? `\\n ${admin} can't log in with a key, but root can. Create ${admin} as a sudo user with root's keys? [Y/n] `\n\t\t\t\t\t: `\\n ${admin} can't log in with a key. Is this a new server, where ${admin} still has to be created? Log in as root with its password to create it [y/N] `,\n\t\t\t),\n\t\t\trootHasKey,\n\t\t);\n\t\tif (createAsRoot) {\n\t\t\tconst created = await run(\n\t\t\t\t'ssh',\n\t\t\t\t[\n\t\t\t\t\t'-t',\n\t\t\t\t\t...asAdmin,\n\t\t\t\t\tat('root', server.host),\n\t\t\t\t\tremoteCommand(ADMIN_SCRIPT, [admin], false),\n\t\t\t\t],\n\t\t\t\t{ interactive: true },\n\t\t\t);\n\t\t\tif (created.code !== 0) {\n\t\t\t\tthrow new ComposeServerSetupFailed(\n\t\t\t\t\tat('root', server.host),\n\t\t\t\t\tcreated.code,\n\t\t\t\t);\n\t\t\t}\n\t\t\tlogin = await probe(admin);\n\t\t}\n\t\t// Root had no key to hand on — a server set up with a password — so\n\t\t// yours goes on with the admin's.\n\t\tif (\n\t\t\tlogin.code !== 0 &&\n\t\t\tisYes(\n\t\t\t\tawait ask(\n\t\t\t\t\t`\\n ${admin} can't log in with a key. Copy yours there with ssh-copy-id (asks ${admin}'s password)? [Y/n] `,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t)\n\t\t) {\n\t\t\tconst copied = await run(\n\t\t\t\t'ssh-copy-id',\n\t\t\t\t[\n\t\t\t\t\t...(identity ? ['-i', identity] : []),\n\t\t\t\t\t'-p',\n\t\t\t\t\tString(server.port),\n\t\t\t\t\tadminTarget,\n\t\t\t\t],\n\t\t\t\t{ interactive: true },\n\t\t\t);\n\t\t\tif (copied.code !== 0) {\n\t\t\t\tthrow new ComposeServerAdminUnreachable(adminTarget, copied.stderr);\n\t\t\t}\n\t\t}\n\t\tlogin = await probe(admin);\n\t\tif (login.code !== 0) {\n\t\t\tthrow new ComposeServerAdminUnreachable(adminTarget, login.stderr);\n\t\t}\n\t}\n\tconsole.log(` ✓ ${adminTarget} logs in with a key\\n`);\n\n\t// ── The server ─────────────────────────────────────────────────────────\n\tconst setup = await run(\n\t\t'ssh',\n\t\t[\n\t\t\t'-t',\n\t\t\t...asAdmin,\n\t\t\tadminTarget,\n\t\t\tremoteCommand(\n\t\t\t\tSERVER_SCRIPT,\n\t\t\t\t[admin, server.user, String(server.port), publicKey, comment],\n\t\t\t\ttrue,\n\t\t\t),\n\t\t],\n\t\t{ interactive: true },\n\t);\n\tif (setup.code !== 0) {\n\t\tthrow new ComposeServerSetupFailed(adminTarget, setup.code);\n\t}\n\tconst serverFingerprint = setup.stdout\n\t\t.replaceAll('\\r', '')\n\t\t.match(/^HOSTKEY=(\\S+)$/m)?.[1];\n\tif (!serverFingerprint) {\n\t\tthrow new ComposeServerSetupFailed(adminTarget, setup.code);\n\t}\n\n\t// ── The host key, pinned ───────────────────────────────────────────────\n\tconst scan = await run('ssh-keyscan', [\n\t\t'-t',\n\t\t'ed25519',\n\t\t'-p',\n\t\tString(server.port),\n\t\tserver.host,\n\t]);\n\tconst knownHosts = scan.stdout\n\t\t.split('\\n')\n\t\t.filter((line) => line.trim() && !line.startsWith('#'))\n\t\t.join('\\n');\n\tconst [scanned = ''] = await fingerprints(run, knownHosts);\n\tif (scanned !== serverFingerprint) {\n\t\tthrow new ComposeServerHostKeyMismatch(\n\t\t\tserver.host,\n\t\t\tserverFingerprint,\n\t\t\tscanned,\n\t\t);\n\t}\n\tconsole.log(`\\n ✓ Host key ${scanned}, as the server reports it`);\n\n\tif (repo) {\n\t\tgh(['api', '--method', 'PUT', `repos/${repo}/environments/${stage}`]);\n\t\tgh([\n\t\t\t'variable',\n\t\t\t'set',\n\t\t\t'DEPLOY_KNOWN_HOSTS',\n\t\t\t'--env',\n\t\t\tstage,\n\t\t\t'--repo',\n\t\t\trepo,\n\t\t\t'--body',\n\t\t\tknownHosts,\n\t\t]);\n\t\t// Through stdin, so the key is never an argument a process list shows.\n\t\tgh(\n\t\t\t['secret', 'set', 'DEPLOY_SSH_KEY', '--env', stage, '--repo', repo],\n\t\t\tawait readFile(keyFile, 'utf-8'),\n\t\t);\n\t\tconsole.log(\n\t\t\t` ✓ GitHub: environment \"${stage}\", DEPLOY_SSH_KEY and DEPLOY_KNOWN_HOSTS`,\n\t\t);\n\t}\n\n\t// ── Logged in as CI will be ────────────────────────────────────────────\n\tconst known = join(dirname(keyFile), `.gkm-known-${workspace.name}-${stage}`);\n\tawait writeFile(known, `${knownHosts}\\n`);\n\ttry {\n\t\tconst asCi = [\n\t\t\t'-i',\n\t\t\tkeyFile,\n\t\t\t'-o',\n\t\t\t'IdentitiesOnly=yes',\n\t\t\t'-o',\n\t\t\t'BatchMode=yes',\n\t\t\t'-o',\n\t\t\t`UserKnownHostsFile=${known}`,\n\t\t\t'-o',\n\t\t\t'StrictHostKeyChecking=yes',\n\t\t\t...sshOptions(server),\n\t\t];\n\t\tconst docker = await run('ssh', [\n\t\t\t...asCi,\n\t\t\tdeployTarget,\n\t\t\t'docker',\n\t\t\t'version',\n\t\t\t'--format',\n\t\t\t'{{.Server.Version}}',\n\t\t]);\n\t\tif (docker.code !== 0) {\n\t\t\tthrow new ComposeServerCheckFailed(deployTarget, 'docker', docker.stderr);\n\t\t}\n\t\tconsole.log(\n\t\t\t` ✓ ${deployTarget} runs docker (engine ${docker.stdout.trim()})`,\n\t\t);\n\t\t// The server's own sshd, through a tunnel: what a migration does to\n\t\t// reach Postgres on the server's loopback.\n\t\tconst tunnel = await run('ssh', [\n\t\t\t...asCi,\n\t\t\t'-W',\n\t\t\t`127.0.0.1:${server.port}`,\n\t\t\tdeployTarget,\n\t\t]);\n\t\tif (!tunnel.stdout.startsWith('SSH-2.0')) {\n\t\t\tthrow new ComposeServerCheckFailed(\n\t\t\t\tdeployTarget,\n\t\t\t\t'tunnel',\n\t\t\t\ttunnel.stdout.slice(0, 80) || tunnel.stderr,\n\t\t\t);\n\t\t}\n\t\tconsole.log(` ✓ ${deployTarget} opens tunnels (migrations)`);\n\t} finally {\n\t\tawait rm(known, { force: true });\n\t}\n\n\t// Done: the stage's state says so, and who did it.\n\tconst journal = await DeployJournal.open(state, stage, () =>\n\t\tcreateComposeState(stage),\n\t);\n\tawait journal.ready(\n\t\t{\n\t\t\tkey: SERVER_RECORD,\n\t\t\ttype: 'server',\n\t\t\tdata: { host: server.host } satisfies ServerRecordData,\n\t\t},\n\t\tserver.host,\n\t);\n\tconsole.log(` ✓ Recorded in the \"${stage}\" state`);\n\n\tconsole.log(\n\t\t`\\n✅ ${server.host} is ready for \"${stage}\".${repo ? ' CI deploys it with the key it was given.' : ''} ` +\n\t\t\t`To deploy from here, load the key first: ssh-add ${keyFile}\\n`,\n\t);\n\treturn {\n\t\tserver,\n\t\tadmin,\n\t\tkeyFile,\n\t\tknownHosts,\n\t\t...(repo ? { repo } : {}),\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"serverSetup-P68H5Lal.mjs","names":[],"sources":["../src/compose/serverScript.ts","../src/compose/serverSetup.ts"],"sourcesContent":["/**\n * What `gkm compose:server` runs on the server, as root (through the admin\n * user's sudo). Plain bash, so `--dry-run` can print exactly what will run,\n * and safe to run again: every step changes only what is missing or wrong.\n *\n * Arguments, in order: the admin user (the one running sudo), the deploy\n * user, the SSH port, the deploy key's public line, and its comment.\n *\n * The last line it prints is `HOSTKEY=<fingerprint>` — the server's own\n * ed25519 host key, which the command checks the network's against before\n * pinning it for CI.\n */\nexport const SERVER_SCRIPT = String.raw`set -euo pipefail\nexport DEBIAN_FRONTEND=noninteractive\nADMIN=\"$1\"; DEPLOY=\"$2\"; SSH_PORT=\"$3\"; PUB=\"$4\"; COMMENT=\"$5\"\nsay() { echo \" $*\"; }\nfail() { echo \" ✗ $*\" >&2; exit 1; }\n\n. /etc/os-release\ncase \"$ID\" in\n\tubuntu | debian) ;;\n\t*) fail \"gkm sets up Ubuntu and Debian servers; this one is $ID.\" ;;\nesac\n\n# ── SSH: keys only, no root ────────────────────────────────────────────────\n# Before password logins are turned off, the admin must have a key to come\n# back with — otherwise this would lock them out.\nADMIN_HOME=\"$(getent passwd \"$ADMIN\" | cut -d: -f6)\"\n[ -s \"$ADMIN_HOME/.ssh/authorized_keys\" ] ||\n\tfail \"$ADMIN has no key in $ADMIN_HOME/.ssh/authorized_keys; turning off password logins would lock them out.\"\n# First match wins in sshd_config, and 00- sorts before what a provider ships\n# (cloud-init's 50-cloud-init.conf turns password logins back on).\ncat > /etc/ssh/sshd_config.d/00-gkm-hardening.conf <<'EOF'\n# Written by gkm compose:server.\nPermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nEOF\n# The deploy migrates through an SSH tunnel to the server's loopback.\ncat > /etc/ssh/sshd_config.d/60-gkm-deploy.conf <<EOF\n# Written by gkm compose:server.\nMatch User $DEPLOY\n AllowTcpForwarding local\nEOF\nsshd -t\nsystemctl reload ssh 2>/dev/null || systemctl reload sshd\n# What sshd actually does now, whatever else is in its config.\nEFFECTIVE=\"$(sshd -T)\"\nfor setting in 'permitrootlogin no' 'passwordauthentication no' 'kbdinteractiveauthentication no'; do\n\tgrep -qx \"$setting\" <<<\"$EFFECTIVE\" || fail \"sshd still does not say '$setting'; another file in /etc/ssh overrides it.\"\ndone\nFORWARD=\"$(sshd -T -C \"user=$DEPLOY,host=localhost,addr=127.0.0.1\" | sed -n 's/^allowtcpforwarding //p')\"\ncase \"$FORWARD\" in\n\tyes | all | local) ;;\n\t*) fail \"sshd does not let $DEPLOY forward ports (allowtcpforwarding $FORWARD).\" ;;\nesac\nsay \"sshd: keys only, no root login; $DEPLOY may open local tunnels\"\n# Root is reached through the admin's sudo, which this script is running\n# under — so its password, the one a provider mails out, can go.\npasswd -l root >/dev/null\nsay \"root's password locked; $ADMIN's sudo is the way to root\"\n\n# ── Firewall: SSH, HTTP, HTTPS ─────────────────────────────────────────────\ncommand -v ufw >/dev/null || { apt-get update -qq && apt-get install -y -qq ufw >/dev/null; }\nufw default deny incoming >/dev/null\nufw default allow outgoing >/dev/null\nufw allow \"$SSH_PORT/tcp\" >/dev/null\nufw allow 80/tcp >/dev/null\nufw allow 443/tcp >/dev/null\nufw --force enable >/dev/null\nsay \"firewall: $SSH_PORT, 80, 443 in; everything out\"\nEXTRA=\"$(ufw status | awk 'NR > 4 && $1 != \"\" { print $1 }' | grep -vxE \"(($SSH_PORT|80|443)(/tcp)?|OpenSSH)\" | sort -u || true)\"\nif [ -n \"$EXTRA\" ]; then\n\tsay \"⚠ also open: $(echo $EXTRA). gkm uses none of them; close each with: sudo ufw delete allow <port>\"\nfi\n\n# ── Updates and bans ───────────────────────────────────────────────────────\napt-get update -qq\napt-get install -y -qq unattended-upgrades fail2ban >/dev/null\ncat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'\nAPT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Unattended-Upgrade \"1\";\nEOF\ncat > /etc/fail2ban/jail.d/gkm.local <<EOF\n# Written by gkm compose:server.\n[sshd]\nenabled = true\nport = $SSH_PORT\nEOF\nsystemctl enable --now unattended-upgrades fail2ban >/dev/null 2>&1\nsystemctl restart fail2ban\nsay \"security updates install daily; fail2ban watches sshd\"\n\n# ── Docker, from Docker's own repository ───────────────────────────────────\nif ! command -v docker >/dev/null || ! docker compose version >/dev/null 2>&1; then\n\tsay \"installing Docker…\"\n\tapt-get install -y -qq ca-certificates curl >/dev/null\n\tinstall -m 0755 -d /etc/apt/keyrings\n\tcurl -fsSL \"https://download.docker.com/linux/$ID/gpg\" -o /etc/apt/keyrings/docker.asc\n\tchmod a+r /etc/apt/keyrings/docker.asc\n\techo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$ID $VERSION_CODENAME stable\" \\\n\t\t> /etc/apt/sources.list.d/docker.list\n\tapt-get update -qq\n\tapt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null\nfi\nsystemctl enable --now docker >/dev/null 2>&1\nsay \"docker $(docker version --format '{{.Server.Version}}')\"\n\n# ── The deploy user: no password, no sudo, in the docker group ─────────────\nid \"$DEPLOY\" >/dev/null 2>&1 || adduser --disabled-password --gecos \"\" \"$DEPLOY\" >/dev/null\nusermod -aG docker \"$DEPLOY\"\nDEPLOY_HOME=\"$(getent passwd \"$DEPLOY\" | cut -d: -f6)\"\ninstall -d -m 700 -o \"$DEPLOY\" -g \"$DEPLOY\" \"$DEPLOY_HOME/.ssh\"\nAUTH=\"$DEPLOY_HOME/.ssh/authorized_keys\"\ntouch \"$AUTH\"\n# One managed line: this key (or an earlier one by the same name) replaced,\n# with nothing allowed but the commands and the tunnel the deploy uses.\nKEYBODY=\"$(awk '{ print $2 }' <<<\"$PUB\")\"\ngrep -v -F -e \"$KEYBODY\" -e \" $COMMENT\" \"$AUTH\" > \"$AUTH.new\" || true\necho \"restrict,port-forwarding $PUB\" >> \"$AUTH.new\"\nmv \"$AUTH.new\" \"$AUTH\"\nchown \"$DEPLOY:$DEPLOY\" \"$AUTH\"\nchmod 600 \"$AUTH\"\nsay \"user $DEPLOY (docker group), deploy key authorized with restrict,port-forwarding\"\nOTHERS=\"$(grep -v -F -e \"$KEYBODY\" \"$AUTH\" | grep -c . || true)\"\nif [ \"$OTHERS\" -gt 0 ]; then\n\tsay \"⚠ $AUTH holds $OTHERS other key(s). Review: sudo cat $AUTH\"\nfi\n\necho \"HOSTKEY=$(ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub | awk '{ print $2 }')\"\n`;\n\n/**\n * What runs as root to give a fresh server its admin — the guide's first\n * step, for a server only root can log in to yet: a sudo user, with root's\n * keys so the same key logs in. `adduser` asks for its password, which sudo\n * will ask for.\n */\nexport const ADMIN_SCRIPT = `set -euo pipefail\nADMIN=\"$1\"\nif ! id \"$ADMIN\" >/dev/null 2>&1; then\n\techo \" creating $ADMIN — choose the password sudo will ask for:\"\n\tadduser --gecos \"\" \"$ADMIN\"\nfi\nusermod -aG sudo \"$ADMIN\"\nHOME_DIR=\"$(getent passwd \"$ADMIN\" | cut -d: -f6)\"\ninstall -d -m 700 -o \"$ADMIN\" -g \"$ADMIN\" \"$HOME_DIR/.ssh\"\nAUTH=\"$HOME_DIR/.ssh/authorized_keys\"\ntouch \"$AUTH\"\n# The keys alone: a provider's options on root's lines (a command=\"…\" that\n# says \"log in as ubuntu\") would stop the admin's login too. A server root\n# logs in to with a password has none; ssh-copy-id brings the admin's.\n{ grep -oE '(ssh|ecdsa|sk)-[^ ]+ [A-Za-z0-9+/=]+( .*)?$' /root/.ssh/authorized_keys 2>/dev/null || true; } |\n\twhile IFS= read -r key; do\n\t\tgrep -qxF \"$key\" \"$AUTH\" || echo \"$key\" >> \"$AUTH\"\n\tdone\nchown \"$ADMIN:$ADMIN\" \"$AUTH\"\nchmod 600 \"$AUTH\"\necho \" $ADMIN is a sudo user, with root's SSH keys (if root had any)\"\n`;\n","/**\n * `gkm compose:server` — make a VPS ready for a stage's compose deploys, from\n * this machine, in one go.\n *\n * It logs in as the server's admin — a sudo user, never root — and, through\n * sudo, sets the server up the way a production VPS should be: SSH by key\n * only with root's login off, a firewall open on SSH, 80 and 443 alone,\n * security updates installed daily, fail2ban on sshd, and Docker from\n * Docker's own repository. It then creates the stage's deploy user (in the\n * docker group, no sudo, no password) and authorizes a deploy key on it that\n * may run commands and open the tunnel migrations use, nothing more.\n *\n * Off the server: it makes the deploy key pair here, pins the server's host\n * key — checked against the fingerprint the server reports of itself — and\n * gives both to the stage's GitHub environment as `DEPLOY_SSH_KEY` and\n * `DEPLOY_KNOWN_HOSTS`, which the generated deploy workflow reads. Last, it\n * logs in exactly as CI will, and checks Docker and the tunnel answer.\n *\n * A fresh server only root can log in to gets its admin first: as root —\n * with root's key, or its password on a brand-new server — it creates the\n * user, and `ssh-copy-id` puts your key on it when root had none to give.\n * Root's password is locked once the admin's sudo works.\n *\n * Once it has, the stage's state records that it is done (`server:compose`,\n * stamped with who and when) — nothing personal: the admin is whoever runs\n * it, with their own key.\n * Safe to run again: each step changes only what is missing or wrong.\n */\n\nimport { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { dirname, join, resolve } from 'node:path';\nimport { loadWorkspaceConfig } from '../config';\nimport { describeActor } from '../deploy/actor';\nimport { type Gh, ghCli } from '../deploy/github';\nimport { DeployJournal } from '../deploy/journal';\nimport { createStateStore, type StateStore } from '../deploy/StateStore';\nimport { createComposeState } from '../deploy/state';\nimport { GkmError } from '../errors';\nimport { prompt } from '../prompt';\nimport { secretsStoreFor } from '../secrets/store.js';\nimport { assertDeployedStage } from '../workspace/stages.js';\nimport { type ComposeServer, composeServer } from './server';\nimport { ADMIN_SCRIPT, SERVER_SCRIPT } from './serverScript';\n\nexport interface ComposeServerSetupOptions {\n\t/** The deployed stage whose server this is. */\n\tstage: string;\n\t/**\n\t * The server's sudo user. Absent, the user `ssh` logs in to the server as\n\t * by default (`~/.ssh/config`, else this machine's) — asked for only when\n\t * that is root.\n\t */\n\tadmin?: string;\n\t/** The key the admin logs in with. Absent, `ssh`'s own choice. */\n\tidentity?: string;\n\t/** The deploy key pair. Default `~/.ssh/gkm-<project>-<stage>`. */\n\tkeyFile?: string;\n\t/** `owner/name`; defaults to the repository `gh` sees here. */\n\trepo?: string;\n\t/** `--no-github`: leave the GitHub environment alone. */\n\tgithub?: boolean;\n\t/** Print the plan and the script; connect to nothing. */\n\tdryRun?: boolean;\n\t/** Apply without asking first. */\n\tyes?: boolean;\n\tcwd?: string;\n}\n\n/** What a finished command ran: output kept, its exit code. */\nexport interface Ran {\n\tcode: number;\n\tstdout: string;\n\tstderr: string;\n}\n\n/**\n * Runs a program. `interactive` gives it this terminal's stdin and stderr —\n * for a password sudo or adduser asks — while stdout is still kept, and\n * echoed as it comes.\n */\nexport type Runner = (\n\tcommand: string,\n\targs: readonly string[],\n\toptions?: { input?: string; interactive?: boolean },\n) => Promise<Ran>;\n\nexport interface ComposeServerSetupDeps {\n\trun: Runner;\n\tgh: Gh;\n\t/** Ask the person at the terminal. */\n\task: (question: string) => Promise<string>;\n\t/** The stage's state; the workspace's own store when absent. */\n\tstate: StateStore;\n}\n\n/** The stage's record of its server, as `compose:server` last set it up. */\nexport const SERVER_RECORD = 'server:compose';\n\n/**\n * What `server:compose` holds: which server was set up. Who set it up, and\n * when, are the record's own `updatedBy` and `updatedAt`.\n */\nexport interface ServerRecordData {\n\thost: string;\n}\n\nexport interface ComposeServerSetupResult {\n\tserver: ComposeServer;\n\tadmin: string;\n\tkeyFile: string;\n\t/** The pinned host key line, as `known_hosts` holds it. Absent on a dry run. */\n\tknownHosts?: string;\n\t/** The repository whose environment was given the key. */\n\trepo?: string;\n}\n\n/** The admin named is root, whose login this command turns off. */\nexport class ComposeServerAdminIsRoot extends GkmError {\n\tconstructor() {\n\t\tsuper(\n\t\t\t\"The admin can't be root: setting up the server turns root's SSH login \" +\n\t\t\t\t'off. Name the sudo user you log in as (--admin <user>). A fresh ' +\n\t\t\t\t'server with only root gets one: run this again with the name it ' +\n\t\t\t\t\"should have, and gkm offers to create it with root's key.\",\n\t\t);\n\t\tthis.name = 'ComposeServerAdminIsRoot';\n\t}\n}\n\n/** The admin named is the deploy user, which has no sudo. */\nexport class ComposeServerAdminIsDeployUser extends GkmError {\n\tconstructor(readonly user: string) {\n\t\tsuper(\n\t\t\t`'${user}' is the stage's deploy user (deploy.compose.server), which ` +\n\t\t\t\t'gkm creates without sudo. Name the sudo user you log in as with --admin.',\n\t\t);\n\t\tthis.name = 'ComposeServerAdminIsDeployUser';\n\t}\n}\n\n/** No key logs the admin in, and nothing was done to make one. */\nexport class ComposeServerAdminUnreachable extends GkmError {\n\tconstructor(\n\t\treadonly target: string,\n\t\treadonly stderr: string,\n\t) {\n\t\tsuper(\n\t\t\t`Could not log in as ${target} with a key (ssh said: ${stderr.trim() || 'nothing'}). ` +\n\t\t\t\t`Check \\`ssh ${target}\\` works without a password — copy your key with ` +\n\t\t\t\t`\\`ssh-copy-id ${target}\\` — then run this again.`,\n\t\t);\n\t\tthis.name = 'ComposeServerAdminUnreachable';\n\t}\n}\n\n/**\n * The server was set up already, and this person cannot log in to it as\n * the admin they named. Root's login and passwords are off there, so only\n * an admin can let them in.\n */\nexport class ComposeServerAccessNeeded extends GkmError {\n\tconstructor(\n\t\treadonly target: string,\n\t\treadonly setUpBy: string,\n\t\treadonly setUpOn: string,\n\t) {\n\t\tsuper(\n\t\t\t`${target} can't log in with your key, and the server was set up by ${setUpBy} on ${setUpOn} — ` +\n\t\t\t\t'root and password logins are off there. Ask them, or another admin, to add your ' +\n\t\t\t\t`public key to ${target.split('@')[0]}'s ~/.ssh/authorized_keys (or create you a sudo ` +\n\t\t\t\t'user with it), then run this again.',\n\t\t);\n\t\tthis.name = 'ComposeServerAccessNeeded';\n\t}\n}\n\n/** `ssh-keygen` could not make the deploy key pair. */\nexport class DeployKeyNotMade extends GkmError {\n\tconstructor(\n\t\treadonly keyFile: string,\n\t\treadonly stderr: string,\n\t) {\n\t\tsuper(\n\t\t\t`Could not make the deploy key ${keyFile} (ssh-keygen said: ${stderr.trim() || 'nothing'}). ` +\n\t\t\t\t'Pass another path with --key-file.',\n\t\t);\n\t\tthis.name = 'DeployKeyNotMade';\n\t}\n}\n\n/** The script on the server stopped before it finished. */\nexport class ComposeServerSetupFailed extends GkmError {\n\tconstructor(\n\t\treadonly target: string,\n\t\treadonly code: number,\n\t) {\n\t\tsuper(\n\t\t\t`Setting up ${target} stopped (exit ${code}); its output is above. ` +\n\t\t\t\t'Fix what it names and run this again — every step it already ' +\n\t\t\t\t'finished is left as it is.',\n\t\t);\n\t\tthis.name = 'ComposeServerSetupFailed';\n\t}\n}\n\n/** The host key the network shows is not the one the server holds. */\nexport class ComposeServerHostKeyMismatch extends GkmError {\n\tconstructor(\n\t\treadonly host: string,\n\t\treadonly server: string,\n\t\treadonly scanned: string,\n\t) {\n\t\tsuper(\n\t\t\t`${host}'s host key is ${server} on the server, but ${scanned || 'nothing'} ` +\n\t\t\t\t'over the network. Something between here and the server answered for ' +\n\t\t\t\t'it, or the server is not the one you logged in to. Nothing was given ' +\n\t\t\t\t'to GitHub; find out why before running this again.',\n\t\t);\n\t\tthis.name = 'ComposeServerHostKeyMismatch';\n\t}\n}\n\n/** The deploy key logged in, but cannot do what a deploy does. */\nexport class ComposeServerCheckFailed extends GkmError {\n\tconstructor(\n\t\treadonly target: string,\n\t\treadonly check: 'docker' | 'tunnel',\n\t\treadonly output: string,\n\t) {\n\t\tsuper(\n\t\t\tcheck === 'docker'\n\t\t\t\t? `The deploy key can't run docker as ${target} (ssh said: ${output.trim() || 'nothing'}).`\n\t\t\t\t: `The deploy key can't open a tunnel as ${target} (got: ${output.trim() || 'nothing'}). ` +\n\t\t\t\t\t\t\"Check its line in the deploy user's authorized_keys says \" +\n\t\t\t\t\t\t'restrict,port-forwarding, and that sshd allows it forwarding.',\n\t\t);\n\t\tthis.name = 'ComposeServerCheckFailed';\n\t}\n}\n\n/** `'…'`, for a value in a command line the remote shell reads. */\nfunction shellQuote(value: string): string {\n\treturn `'${value.replaceAll(\"'\", `'\\\\''`)}'`;\n}\n\n/**\n * The remote command that runs `script` with `args`: written to a temporary\n * file (so the script keeps the terminal for any password it asks), run,\n * and removed. `sudo` for everything but root.\n */\nexport function remoteCommand(\n\tscript: string,\n\targs: readonly string[],\n\tsudo: boolean,\n): string {\n\tconst encoded = Buffer.from(script).toString('base64');\n\treturn [\n\t\t`f=\"$(mktemp)\"`,\n\t\t`echo ${encoded} | base64 -d > \"$f\"`,\n\t\t`${sudo ? 'sudo ' : ''}bash \"$f\" ${args.map(shellQuote).join(' ')}; s=$?`,\n\t\t`rm -f \"$f\"`,\n\t\t'exit $s',\n\t].join('; ');\n}\n\nfunction sshOptions(server: ComposeServer): string[] {\n\treturn ['-o', 'ConnectTimeout=20', '-p', String(server.port)];\n}\n\n/** How the admin (or root) logs in: the server's options and their key. */\nfunction loginOptions(server: ComposeServer, identity?: string): string[] {\n\treturn [\n\t\t...sshOptions(server),\n\t\t...(identity ? ['-i', identity, '-o', 'IdentitiesOnly=yes'] : []),\n\t];\n}\n\n/** `user@host`, as `ssh` takes it. */\nfunction at(user: string, host: string): string {\n\treturn `${user}@${host}`;\n}\n\n/** The real runner: the system's programs. */\nexport const systemRunner: Runner = (command, args, options = {}) =>\n\tnew Promise((resolve, reject) => {\n\t\tconst child = spawn(command, args, {\n\t\t\tstdio: [\n\t\t\t\toptions.interactive ? 'inherit' : 'pipe',\n\t\t\t\t'pipe',\n\t\t\t\toptions.interactive ? 'inherit' : 'pipe',\n\t\t\t],\n\t\t\ttimeout: options.interactive ? undefined : 60_000,\n\t\t});\n\t\tlet stdout = '';\n\t\tlet stderr = '';\n\t\tchild.stdout?.on('data', (chunk: Buffer) => {\n\t\t\tstdout += chunk.toString();\n\t\t\tif (options.interactive) process.stdout.write(chunk);\n\t\t});\n\t\tchild.stderr?.on('data', (chunk: Buffer) => {\n\t\t\tstderr += chunk.toString();\n\t\t});\n\t\tchild.on('error', reject);\n\t\tchild.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));\n\t\tif (child.stdin) child.stdin.end(options.input ?? '');\n\t});\n\n/** Whether `answer` is a yes, `fallback` when nothing was typed. */\nfunction isYes(answer: string, fallback: boolean): boolean {\n\tconst value = answer.trim().toLowerCase();\n\treturn value === '' ? fallback : value === 'y' || value === 'yes';\n}\n\n/** The SHA256 fingerprint of each key in `keys`, by `ssh-keygen -lf -`. */\nasync function fingerprints(run: Runner, keys: string): Promise<string[]> {\n\tconst { stdout } = await run('ssh-keygen', ['-lf', '-'], { input: keys });\n\treturn stdout\n\t\t.split('\\n')\n\t\t.map((line) => line.split(' ')[1])\n\t\t.filter((value): value is string => Boolean(value));\n}\n\n/** The default deploy key pair for a stage. */\nexport function defaultKeyFile(project: string, stage: string): string {\n\treturn join(homedir(), '.ssh', `gkm-${project}-${stage}`);\n}\n\n/** `gkm compose:server`. */\nexport async function composeServerCommand(\n\toptions: ComposeServerSetupOptions,\n\tdeps: Partial<ComposeServerSetupDeps> = {},\n): Promise<ComposeServerSetupResult> {\n\tconst run = deps.run ?? systemRunner;\n\tconst gh = deps.gh ?? ghCli;\n\tconst ask =\n\t\tdeps.ask ??\n\t\t((question: string) =>\n\t\t\tprompt(question, {\n\t\t\t\tinstead: 'Name the server user with --admin and pass --yes.',\n\t\t\t}));\n\tconst github = options.github !== false;\n\tconst { stage } = options;\n\n\tconst cwd = resolve(options.cwd ?? process.cwd());\n\tconst { workspace } = await loadWorkspaceConfig(cwd);\n\tassertDeployedStage(workspace.stages, stage);\n\tconst server = composeServer({\n\t\tstage,\n\t\tlocal: false,\n\t\tconfig: workspace.deploy?.compose?.server,\n\t\tcustom: (await (await secretsStoreFor(workspace, stage)).read(stage))\n\t\t\t?.custom,\n\t})!;\n\n\t// Whether this server was set up before, by whom and when.\n\tconst state =\n\t\tdeps.state ??\n\t\t(await createStateStore({\n\t\t\tconfig: workspace.state,\n\t\t\tworkspaceRoot: workspace.root,\n\t\t\tworkspaceName: workspace.name,\n\t\t\tnamespace: workspace.deploy?.namespace,\n\t\t}));\n\tconst record = (await state.read(stage))?.resources[SERVER_RECORD];\n\tconst done =\n\t\t(record?.data as ServerRecordData | undefined)?.host === server.host\n\t\t\t? record\n\t\t\t: undefined;\n\n\t// The admin's login: `--admin`/`--identity`, or what `ssh` itself would\n\t// use for this host — read with `ssh -G`, which connects to nothing.\n\tconst identity = options.identity ? resolve(options.identity) : undefined;\n\tconst asAdmin = loginOptions(server, identity);\n\tconst sshUser =\n\t\t(await run('ssh', ['-G', ...asAdmin, server.host])).stdout.match(\n\t\t\t/^user (\\S+)$/m,\n\t\t)?.[1] ?? '';\n\tconst admin = (\n\t\toptions.admin ??\n\t\t(sshUser && sshUser !== 'root'\n\t\t\t? sshUser\n\t\t\t: await ask(`The sudo user you log in to ${server.host} as (not root): `))\n\t).trim();\n\tif (!admin || admin === 'root') throw new ComposeServerAdminIsRoot();\n\tif (admin === server.user) throw new ComposeServerAdminIsDeployUser(admin);\n\n\tconst keyFile = resolve(\n\t\toptions.keyFile ?? defaultKeyFile(workspace.name, stage),\n\t);\n\tconst comment = `gkm-${workspace.name}-${stage}-deploy`;\n\tconst repo = github\n\t\t? (options.repo ??\n\t\t\tgh(['repo', 'view', '--json', 'nameWithOwner', '-q', '.nameWithOwner']))\n\t\t: undefined;\n\tconst adminTarget = at(admin, server.host);\n\tconst deployTarget = at(server.user, server.host);\n\n\tconsole.log(`\\n🖥 Server for stage \"${stage}\"\\n`);\n\tconsole.log(` Server: ${server.host} (ssh port ${server.port})`);\n\tconsole.log(\n\t\t` Admin: ${admin} — logs in with ${identity ?? 'your SSH key'}, runs sudo${options.admin ? '' : ' (your SSH login)'}`,\n\t);\n\tconsole.log(\n\t\t` Deploy user: ${server.user} — docker group, no sudo, no password`,\n\t);\n\tconsole.log(\n\t\t` Deploy key: ${keyFile}${existsSync(keyFile) ? '' : ' (new, made here)'}`,\n\t);\n\tconsole.log(\n\t\t` GitHub: ${repo ? `environment \"${stage}\" of ${repo}: DEPLOY_SSH_KEY, DEPLOY_KNOWN_HOSTS` : 'left alone (--no-github)'}`,\n\t);\n\tif (done) {\n\t\tconsole.log(\n\t\t\t`\\n Already set up by ${describeActor(done.updatedBy)} on ${done.updatedAt.slice(0, 10)}; running again changes only what is missing or wrong.`,\n\t\t);\n\t}\n\tconsole.log('\\n On the server, through sudo:');\n\tfor (const step of [\n\t\t\"SSH: keys only, root's login off (and nothing else may turn passwords back on)\",\n\t\t`root's password locked — ${admin}'s sudo is the way to root`,\n\t\t`firewall: ${server.port}, 80 and 443 in, everything out`,\n\t\t'security updates installed daily (unattended-upgrades); fail2ban on sshd',\n\t\t\"Docker and its compose plugin, from Docker's repository\",\n\t\t`user ${server.user}, the deploy key authorized with restrict,port-forwarding`,\n\t]) {\n\t\tconsole.log(` · ${step}`);\n\t}\n\n\tif (options.dryRun) {\n\t\tconsole.log(\n\t\t\t`\\n The script, run as: sudo bash setup.sh ${[admin, server.user, String(server.port), '<deploy key>', comment].map(shellQuote).join(' ')}\\n`,\n\t\t);\n\t\tconsole.log(SERVER_SCRIPT.replace(/^/gm, ' '));\n\t\tconsole.log(' --dry-run: nothing changed.\\n');\n\t\treturn { server, admin, keyFile, ...(repo ? { repo } : {}) };\n\t}\n\n\tif (\n\t\t!options.yes &&\n\t\t!isYes(await ask(`\\n Set up ${server.host}? [y/N] `), false)\n\t) {\n\t\tconsole.log(' Nothing changed.\\n');\n\t\treturn { server, admin, keyFile, ...(repo ? { repo } : {}) };\n\t}\n\n\t// ── The deploy key pair, here ──────────────────────────────────────────\n\tif (!existsSync(keyFile)) {\n\t\tawait mkdir(dirname(keyFile), { recursive: true, mode: 0o700 });\n\t\tconst made = await run('ssh-keygen', [\n\t\t\t'-t',\n\t\t\t'ed25519',\n\t\t\t'-N',\n\t\t\t'',\n\t\t\t'-C',\n\t\t\tcomment,\n\t\t\t'-f',\n\t\t\tkeyFile,\n\t\t]);\n\t\tif (made.code !== 0) throw new DeployKeyNotMade(keyFile, made.stderr);\n\t\tconsole.log(`\\n ✓ Made the deploy key ${keyFile}`);\n\t}\n\tconst publicKey = (await readFile(`${keyFile}.pub`, 'utf-8')).trim();\n\n\t// ── The admin's login ──────────────────────────────────────────────────\n\t// Key only, and the host key accepted on first sight — as the first\n\t// `ssh` to a new server does. CI's is pinned and checked below.\n\tconst probe = (user: string) =>\n\t\trun('ssh', [\n\t\t\t'-o',\n\t\t\t'BatchMode=yes',\n\t\t\t'-o',\n\t\t\t'StrictHostKeyChecking=accept-new',\n\t\t\t...asAdmin,\n\t\t\tat(user, server.host),\n\t\t\t'true',\n\t\t]);\n\tlet login = await probe(admin);\n\t// A server already set up has no root or password login left to make\n\t// one with: an admin there has to let this person in.\n\tif (login.code !== 0 && done) {\n\t\tthrow new ComposeServerAccessNeeded(\n\t\t\tadminTarget,\n\t\t\tdescribeActor(done.updatedBy),\n\t\t\tdone.updatedAt.slice(0, 10),\n\t\t);\n\t}\n\tif (login.code !== 0) {\n\t\t// A server only root can log in to yet: root creates the admin — with\n\t\t// its key, or on a brand-new server with the password the provider\n\t\t// gave it, which this is the last time anything asks for.\n\t\tconst rootHasKey = (await probe('root')).code === 0;\n\t\tconst createAsRoot = isYes(\n\t\t\tawait ask(\n\t\t\t\trootHasKey\n\t\t\t\t\t? `\\n ${admin} can't log in with a key, but root can. Create ${admin} as a sudo user with root's keys? [Y/n] `\n\t\t\t\t\t: `\\n ${admin} can't log in with a key. Is this a new server, where ${admin} still has to be created? Log in as root with its password to create it [y/N] `,\n\t\t\t),\n\t\t\trootHasKey,\n\t\t);\n\t\tif (createAsRoot) {\n\t\t\tconst created = await run(\n\t\t\t\t'ssh',\n\t\t\t\t[\n\t\t\t\t\t'-t',\n\t\t\t\t\t...asAdmin,\n\t\t\t\t\tat('root', server.host),\n\t\t\t\t\tremoteCommand(ADMIN_SCRIPT, [admin], false),\n\t\t\t\t],\n\t\t\t\t{ interactive: true },\n\t\t\t);\n\t\t\tif (created.code !== 0) {\n\t\t\t\tthrow new ComposeServerSetupFailed(\n\t\t\t\t\tat('root', server.host),\n\t\t\t\t\tcreated.code,\n\t\t\t\t);\n\t\t\t}\n\t\t\tlogin = await probe(admin);\n\t\t}\n\t\t// Root had no key to hand on — a server set up with a password — so\n\t\t// yours goes on with the admin's.\n\t\tif (\n\t\t\tlogin.code !== 0 &&\n\t\t\tisYes(\n\t\t\t\tawait ask(\n\t\t\t\t\t`\\n ${admin} can't log in with a key. Copy yours there with ssh-copy-id (asks ${admin}'s password)? [Y/n] `,\n\t\t\t\t),\n\t\t\t\ttrue,\n\t\t\t)\n\t\t) {\n\t\t\tconst copied = await run(\n\t\t\t\t'ssh-copy-id',\n\t\t\t\t[\n\t\t\t\t\t...(identity ? ['-i', identity] : []),\n\t\t\t\t\t'-p',\n\t\t\t\t\tString(server.port),\n\t\t\t\t\tadminTarget,\n\t\t\t\t],\n\t\t\t\t{ interactive: true },\n\t\t\t);\n\t\t\tif (copied.code !== 0) {\n\t\t\t\tthrow new ComposeServerAdminUnreachable(adminTarget, copied.stderr);\n\t\t\t}\n\t\t}\n\t\tlogin = await probe(admin);\n\t\tif (login.code !== 0) {\n\t\t\tthrow new ComposeServerAdminUnreachable(adminTarget, login.stderr);\n\t\t}\n\t}\n\tconsole.log(` ✓ ${adminTarget} logs in with a key\\n`);\n\n\t// ── The server ─────────────────────────────────────────────────────────\n\tconst setup = await run(\n\t\t'ssh',\n\t\t[\n\t\t\t'-t',\n\t\t\t...asAdmin,\n\t\t\tadminTarget,\n\t\t\tremoteCommand(\n\t\t\t\tSERVER_SCRIPT,\n\t\t\t\t[admin, server.user, String(server.port), publicKey, comment],\n\t\t\t\ttrue,\n\t\t\t),\n\t\t],\n\t\t{ interactive: true },\n\t);\n\tif (setup.code !== 0) {\n\t\tthrow new ComposeServerSetupFailed(adminTarget, setup.code);\n\t}\n\tconst serverFingerprint = setup.stdout\n\t\t.replaceAll('\\r', '')\n\t\t.match(/^HOSTKEY=(\\S+)$/m)?.[1];\n\tif (!serverFingerprint) {\n\t\tthrow new ComposeServerSetupFailed(adminTarget, setup.code);\n\t}\n\n\t// ── The host key, pinned ───────────────────────────────────────────────\n\tconst scan = await run('ssh-keyscan', [\n\t\t'-t',\n\t\t'ed25519',\n\t\t'-p',\n\t\tString(server.port),\n\t\tserver.host,\n\t]);\n\tconst knownHosts = scan.stdout\n\t\t.split('\\n')\n\t\t.filter((line) => line.trim() && !line.startsWith('#'))\n\t\t.join('\\n');\n\tconst [scanned = ''] = await fingerprints(run, knownHosts);\n\tif (scanned !== serverFingerprint) {\n\t\tthrow new ComposeServerHostKeyMismatch(\n\t\t\tserver.host,\n\t\t\tserverFingerprint,\n\t\t\tscanned,\n\t\t);\n\t}\n\tconsole.log(`\\n ✓ Host key ${scanned}, as the server reports it`);\n\n\tif (repo) {\n\t\tgh(['api', '--method', 'PUT', `repos/${repo}/environments/${stage}`]);\n\t\tgh([\n\t\t\t'variable',\n\t\t\t'set',\n\t\t\t'DEPLOY_KNOWN_HOSTS',\n\t\t\t'--env',\n\t\t\tstage,\n\t\t\t'--repo',\n\t\t\trepo,\n\t\t\t'--body',\n\t\t\tknownHosts,\n\t\t]);\n\t\t// Through stdin, so the key is never an argument a process list shows.\n\t\tgh(\n\t\t\t['secret', 'set', 'DEPLOY_SSH_KEY', '--env', stage, '--repo', repo],\n\t\t\tawait readFile(keyFile, 'utf-8'),\n\t\t);\n\t\tconsole.log(\n\t\t\t` ✓ GitHub: environment \"${stage}\", DEPLOY_SSH_KEY and DEPLOY_KNOWN_HOSTS`,\n\t\t);\n\t}\n\n\t// ── Logged in as CI will be ────────────────────────────────────────────\n\tconst known = join(dirname(keyFile), `.gkm-known-${workspace.name}-${stage}`);\n\tawait writeFile(known, `${knownHosts}\\n`);\n\ttry {\n\t\tconst asCi = [\n\t\t\t'-i',\n\t\t\tkeyFile,\n\t\t\t'-o',\n\t\t\t'IdentitiesOnly=yes',\n\t\t\t'-o',\n\t\t\t'BatchMode=yes',\n\t\t\t'-o',\n\t\t\t`UserKnownHostsFile=${known}`,\n\t\t\t'-o',\n\t\t\t'StrictHostKeyChecking=yes',\n\t\t\t...sshOptions(server),\n\t\t];\n\t\tconst docker = await run('ssh', [\n\t\t\t...asCi,\n\t\t\tdeployTarget,\n\t\t\t'docker',\n\t\t\t'version',\n\t\t\t'--format',\n\t\t\t'{{.Server.Version}}',\n\t\t]);\n\t\tif (docker.code !== 0) {\n\t\t\tthrow new ComposeServerCheckFailed(deployTarget, 'docker', docker.stderr);\n\t\t}\n\t\tconsole.log(\n\t\t\t` ✓ ${deployTarget} runs docker (engine ${docker.stdout.trim()})`,\n\t\t);\n\t\t// The server's own sshd, through a tunnel: what a migration does to\n\t\t// reach Postgres on the server's loopback.\n\t\tconst tunnel = await run('ssh', [\n\t\t\t...asCi,\n\t\t\t'-W',\n\t\t\t`127.0.0.1:${server.port}`,\n\t\t\tdeployTarget,\n\t\t]);\n\t\tif (!tunnel.stdout.startsWith('SSH-2.0')) {\n\t\t\tthrow new ComposeServerCheckFailed(\n\t\t\t\tdeployTarget,\n\t\t\t\t'tunnel',\n\t\t\t\ttunnel.stdout.slice(0, 80) || tunnel.stderr,\n\t\t\t);\n\t\t}\n\t\tconsole.log(` ✓ ${deployTarget} opens tunnels (migrations)`);\n\t} finally {\n\t\tawait rm(known, { force: true });\n\t}\n\n\t// Done: the stage's state says so, and who did it.\n\tconst journal = await DeployJournal.open(state, stage, () =>\n\t\tcreateComposeState(stage),\n\t);\n\tawait journal.ready(\n\t\t{\n\t\t\tkey: SERVER_RECORD,\n\t\t\ttype: 'server',\n\t\t\tdata: { host: server.host } satisfies ServerRecordData,\n\t\t},\n\t\tserver.host,\n\t);\n\tconsole.log(` ✓ Recorded in the \"${stage}\" state`);\n\n\tconsole.log(\n\t\t`\\n✅ ${server.host} is ready for \"${stage}\".${repo ? ' CI deploys it with the key it was given.' : ''} ` +\n\t\t\t`To deploy from here, load the key first: ssh-add ${keyFile}\\n`,\n\t);\n\treturn {\n\t\tserver,\n\t\tadmin,\n\t\tkeyFile,\n\t\tknownHosts,\n\t\t...(repo ? { repo } : {}),\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAYA,MAAa,gBAAgB,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8HvC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACvC5B,MAAa,gBAAgB;;AAqB7B,IAAa,2BAAb,cAA8C,SAAS;CACtD,cAAc;EACb,MACC,iQAID;EACA,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,iCAAb,cAAoD,SAAS;CACvC;CAArB,YAAY,MAAuB;EAClC,MACC,IAAI,KAAK,qIAEV;EAJoB,KAAA,OAAA;EAKpB,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,gCAAb,cAAmD,SAAS;CAEjD;CACA;CAFV,YACC,QACA,QACC;EACD,MACC,uBAAuB,OAAO,yBAAyB,OAAO,KAAK,KAAK,UAAU,iBAClE,OAAO,iEACL,OAAO,0BAC1B;EAPS,KAAA,SAAA;EACA,KAAA,SAAA;EAOT,KAAK,OAAO;CACb;AACD;;;;;;AAOA,IAAa,4BAAb,cAA+C,SAAS;CAE7C;CACA;CACA;CAHV,YACC,QACA,SACA,SACC;EACD,MACC,GAAG,OAAO,4DAA4D,QAAQ,MAAM,QAAQ,mGAE1E,OAAO,MAAM,GAAG,CAAC,CAAC,GAAG,oFAExC;EATS,KAAA,SAAA;EACA,KAAA,UAAA;EACA,KAAA,UAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,mBAAb,cAAsC,SAAS;CAEpC;CACA;CAFV,YACC,SACA,QACC;EACD,MACC,iCAAiC,QAAQ,qBAAqB,OAAO,KAAK,KAAK,UAAU,sCAE1F;EANS,KAAA,UAAA;EACA,KAAA,SAAA;EAMT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,2BAAb,cAA8C,SAAS;CAE5C;CACA;CAFV,YACC,QACA,MACC;EACD,MACC,cAAc,OAAO,iBAAiB,KAAK,gHAG5C;EAPS,KAAA,SAAA;EACA,KAAA,OAAA;EAOT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,+BAAb,cAAkD,SAAS;CAEhD;CACA;CACA;CAHV,YACC,MACA,QACA,SACC;EACD,MACC,GAAG,KAAK,iBAAiB,OAAO,sBAAsB,WAAW,UAAU,8LAI5E;EATS,KAAA,OAAA;EACA,KAAA,SAAA;EACA,KAAA,UAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,2BAAb,cAA8C,SAAS;CAE5C;CACA;CACA;CAHV,YACC,QACA,OACA,QACC;EACD,MACC,UAAU,WACP,sCAAsC,OAAO,cAAc,OAAO,KAAK,KAAK,UAAU,MACtF,yCAAyC,OAAO,SAAS,OAAO,KAAK,KAAK,UAAU,0HAGxF;EAVS,KAAA,SAAA;EACA,KAAA,QAAA;EACA,KAAA,SAAA;EAST,KAAK,OAAO;CACb;AACD;;AAGA,SAAS,WAAW,OAAuB;CAC1C,OAAO,IAAI,MAAM,WAAW,KAAK,OAAO,EAAE;AAC3C;;;;;;AAOA,SAAgB,cACf,QACA,MACA,MACS;CAET,OAAO;EACN;EACA,QAHe,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,QAG9B,EAAE;EAChB,GAAG,OAAO,UAAU,GAAG,YAAY,KAAK,IAAI,UAAU,CAAC,CAAC,KAAK,GAAG,EAAE;EAClE;EACA;CACD,CAAC,CAAC,KAAK,IAAI;AACZ;AAEA,SAAS,WAAW,QAAiC;CACpD,OAAO;EAAC;EAAM;EAAqB;EAAM,OAAO,OAAO,IAAI;CAAC;AAC7D;;AAGA,SAAS,aAAa,QAAuB,UAA6B;CACzE,OAAO,CACN,GAAG,WAAW,MAAM,GACpB,GAAI,WAAW;EAAC;EAAM;EAAU;EAAM;CAAoB,IAAI,CAAC,CAChE;AACD;;AAGA,SAAS,GAAG,MAAc,MAAsB;CAC/C,OAAO,GAAG,KAAK,GAAG;AACnB;;AAGA,MAAa,gBAAwB,SAAS,MAAM,UAAU,CAAC,MAC9D,IAAI,SAAS,SAAS,WAAW;CAChC,MAAM,QAAQ,MAAM,SAAS,MAAM;EAClC,OAAO;GACN,QAAQ,cAAc,YAAY;GAClC;GACA,QAAQ,cAAc,YAAY;EACnC;EACA,SAAS,QAAQ,cAAc,KAAA,IAAY;CAC5C,CAAC;CACD,IAAI,SAAS;CACb,IAAI,SAAS;CACb,MAAM,QAAQ,GAAG,SAAS,UAAkB;EAC3C,UAAU,MAAM,SAAS;EACzB,IAAI,QAAQ,aAAa,QAAQ,OAAO,MAAM,KAAK;CACpD,CAAC;CACD,MAAM,QAAQ,GAAG,SAAS,UAAkB;EAC3C,UAAU,MAAM,SAAS;CAC1B,CAAC;CACD,MAAM,GAAG,SAAS,MAAM;CACxB,MAAM,GAAG,UAAU,SAAS,QAAQ;EAAE,MAAM,QAAQ;EAAG;EAAQ;CAAO,CAAC,CAAC;CACxE,IAAI,MAAM,OAAO,MAAM,MAAM,IAAI,QAAQ,SAAS,EAAE;AACrD,CAAC;;AAGF,SAAS,MAAM,QAAgB,UAA4B;CAC1D,MAAM,QAAQ,OAAO,KAAK,CAAC,CAAC,YAAY;CACxC,OAAO,UAAU,KAAK,WAAW,UAAU,OAAO,UAAU;AAC7D;;AAGA,eAAe,aAAa,KAAa,MAAiC;CACzE,MAAM,EAAE,WAAW,MAAM,IAAI,cAAc,CAAC,OAAO,GAAG,GAAG,EAAE,OAAO,KAAK,CAAC;CACxE,OAAO,OACL,MAAM,IAAI,CAAC,CACX,KAAK,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CACjC,QAAQ,UAA2B,QAAQ,KAAK,CAAC;AACpD;;AAGA,SAAgB,eAAe,SAAiB,OAAuB;CACtE,OAAO,KAAK,QAAQ,GAAG,QAAQ,OAAO,QAAQ,GAAG,OAAO;AACzD;;AAGA,eAAsB,qBACrB,SACA,OAAwC,CAAC,GACL;CACpC,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,KAAK,KAAK,MAAM;CACtB,MAAM,MACL,KAAK,SACH,aACD,OAAO,UAAU,EAChB,SAAS,oDACV,CAAC;CACH,MAAM,SAAS,QAAQ,WAAW;CAClC,MAAM,EAAE,UAAU;CAElB,MAAM,MAAM,QAAQ,QAAQ,OAAO,QAAQ,IAAI,CAAC;CAChD,MAAM,EAAE,cAAc,MAAM,oBAAoB,GAAG;CACnD,oBAAoB,UAAU,QAAQ,KAAK;CAC3C,MAAM,SAAS,cAAc;EAC5B;EACA,OAAO;EACP,QAAQ,UAAU,QAAQ,SAAS;EACnC,SAAS,OAAO,MAAM,gBAAgB,WAAW,KAAK,EAAA,CAAG,KAAK,KAAK,EAAA,EAChE;CACJ,CAAC;CAGD,MAAM,QACL,KAAK,SACJ,MAAM,iBAAiB;EACvB,QAAQ,UAAU;EAClB,eAAe,UAAU;EACzB,eAAe,UAAU;EACzB,WAAW,UAAU,QAAQ;CAC9B,CAAC;CACF,MAAM,UAAU,MAAM,MAAM,KAAK,KAAK,EAAA,EAAI,UAAU;CACpD,MAAM,QACJ,QAAQ,KAAA,EAAuC,SAAS,OAAO,OAC7D,SACA,KAAA;CAIJ,MAAM,WAAW,QAAQ,WAAW,QAAQ,QAAQ,QAAQ,IAAI,KAAA;CAChE,MAAM,UAAU,aAAa,QAAQ,QAAQ;CAC7C,MAAM,WACJ,MAAM,IAAI,OAAO;EAAC;EAAM,GAAG;EAAS,OAAO;CAAI,CAAC,EAAA,CAAG,OAAO,MAC1D,eACD,CAAC,GAAG,MAAM;CACX,MAAM,SACL,QAAQ,UACP,WAAW,YAAY,SACrB,UACA,MAAM,IAAI,+BAA+B,OAAO,KAAK,iBAAiB,GAAA,CACxE,KAAK;CACP,IAAI,CAAC,SAAS,UAAU,QAAQ,MAAM,IAAI,yBAAyB;CACnE,IAAI,UAAU,OAAO,MAAM,MAAM,IAAI,+BAA+B,KAAK;CAEzE,MAAM,UAAU,QACf,QAAQ,WAAW,eAAe,UAAU,MAAM,KAAK,CACxD;CACA,MAAM,UAAU,OAAO,UAAU,KAAK,GAAG,MAAM;CAC/C,MAAM,OAAO,SACT,QAAQ,QACV,GAAG;EAAC;EAAQ;EAAQ;EAAU;EAAiB;EAAM;CAAgB,CAAC,IACrE,KAAA;CACH,MAAM,cAAc,GAAG,OAAO,OAAO,IAAI;CACzC,MAAM,eAAe,GAAG,OAAO,MAAM,OAAO,IAAI;CAEhD,QAAQ,IAAI,2BAA2B,MAAM,IAAI;CACjD,QAAQ,IAAI,kBAAkB,OAAO,KAAK,aAAa,OAAO,KAAK,EAAE;CACrE,QAAQ,IACP,kBAAkB,MAAM,kBAAkB,YAAY,eAAe,aAAa,QAAQ,QAAQ,KAAK,qBACxG;CACA,QAAQ,IACP,kBAAkB,OAAO,KAAK,sCAC/B;CACA,QAAQ,IACP,kBAAkB,UAAU,WAAW,OAAO,IAAI,KAAK,qBACxD;CACA,QAAQ,IACP,kBAAkB,OAAO,gBAAgB,MAAM,OAAO,KAAK,wCAAwC,4BACpG;CACA,IAAI,MACH,QAAQ,IACP,yBAAyB,cAAc,KAAK,SAAS,EAAE,MAAM,KAAK,UAAU,MAAM,GAAG,EAAE,EAAE,uDAC1F;CAED,QAAQ,IAAI,kCAAkC;CAC9C,KAAK,MAAM,QAAQ;EAClB;EACA,4BAA4B,MAAM;EAClC,aAAa,OAAO,KAAK;EACzB;EACA;EACA,QAAQ,OAAO,KAAK;CACrB,GACC,QAAQ,IAAI,SAAS,MAAM;CAG5B,IAAI,QAAQ,QAAQ;EACnB,QAAQ,IACP,8CAA8C;GAAC;GAAO,OAAO;GAAM,OAAO,OAAO,IAAI;GAAG;GAAgB;EAAO,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,KAAK,GAAG,EAAE,GAC5I;EACA,QAAQ,IAAI,cAAc,QAAQ,OAAO,MAAM,CAAC;EAChD,QAAQ,IAAI,iCAAiC;EAC7C,OAAO;GAAE;GAAQ;GAAO;GAAS,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;EAAG;CAC5D;CAEA,IACC,CAAC,QAAQ,OACT,CAAC,MAAM,MAAM,IAAI,cAAc,OAAO,KAAK,SAAS,GAAG,KAAK,GAC3D;EACD,QAAQ,IAAI,sBAAsB;EAClC,OAAO;GAAE;GAAQ;GAAO;GAAS,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;EAAG;CAC5D;CAGA,IAAI,CAAC,WAAW,OAAO,GAAG;EACzB,MAAM,MAAM,QAAQ,OAAO,GAAG;GAAE,WAAW;GAAM,MAAM;EAAM,CAAC;EAC9D,MAAM,OAAO,MAAM,IAAI,cAAc;GACpC;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACD,CAAC;EACD,IAAI,KAAK,SAAS,GAAG,MAAM,IAAI,iBAAiB,SAAS,KAAK,MAAM;EACpE,QAAQ,IAAI,6BAA6B,SAAS;CACnD;CACA,MAAM,aAAa,MAAM,SAAS,GAAG,QAAQ,OAAO,OAAO,EAAA,CAAG,KAAK;CAKnE,MAAM,SAAS,SACd,IAAI,OAAO;EACV;EACA;EACA;EACA;EACA,GAAG;EACH,GAAG,MAAM,OAAO,IAAI;EACpB;CACD,CAAC;CACF,IAAI,QAAQ,MAAM,MAAM,KAAK;CAG7B,IAAI,MAAM,SAAS,KAAK,MACvB,MAAM,IAAI,0BACT,aACA,cAAc,KAAK,SAAS,GAC5B,KAAK,UAAU,MAAM,GAAG,EAAE,CAC3B;CAED,IAAI,MAAM,SAAS,GAAG;EAIrB,MAAM,cAAc,MAAM,MAAM,MAAM,EAAA,CAAG,SAAS;EASlD,IARqB,MACpB,MAAM,IACL,aACG,OAAO,MAAM,iDAAiD,MAAM,4CACpE,OAAO,MAAM,wDAAwD,MAAM,+EAC/E,GACA,UAEc,GAAG;GACjB,MAAM,UAAU,MAAM,IACrB,OACA;IACC;IACA,GAAG;IACH,GAAG,QAAQ,OAAO,IAAI;IACtB,cAAc,cAAc,CAAC,KAAK,GAAG,KAAK;GAC3C,GACA,EAAE,aAAa,KAAK,CACrB;GACA,IAAI,QAAQ,SAAS,GACpB,MAAM,IAAI,yBACT,GAAG,QAAQ,OAAO,IAAI,GACtB,QAAQ,IACT;GAED,QAAQ,MAAM,MAAM,KAAK;EAC1B;EAGA,IACC,MAAM,SAAS,KACf,MACC,MAAM,IACL,OAAO,MAAM,oEAAoE,MAAM,qBACxF,GACA,IACD,GACC;GACD,MAAM,SAAS,MAAM,IACpB,eACA;IACC,GAAI,WAAW,CAAC,MAAM,QAAQ,IAAI,CAAC;IACnC;IACA,OAAO,OAAO,IAAI;IAClB;GACD,GACA,EAAE,aAAa,KAAK,CACrB;GACA,IAAI,OAAO,SAAS,GACnB,MAAM,IAAI,8BAA8B,aAAa,OAAO,MAAM;EAEpE;EACA,QAAQ,MAAM,MAAM,KAAK;EACzB,IAAI,MAAM,SAAS,GAClB,MAAM,IAAI,8BAA8B,aAAa,MAAM,MAAM;CAEnE;CACA,QAAQ,IAAI,OAAO,YAAY,sBAAsB;CAGrD,MAAM,QAAQ,MAAM,IACnB,OACA;EACC;EACA,GAAG;EACH;EACA,cACC,eACA;GAAC;GAAO,OAAO;GAAM,OAAO,OAAO,IAAI;GAAG;GAAW;EAAO,GAC5D,IACD;CACD,GACA,EAAE,aAAa,KAAK,CACrB;CACA,IAAI,MAAM,SAAS,GAClB,MAAM,IAAI,yBAAyB,aAAa,MAAM,IAAI;CAE3D,MAAM,oBAAoB,MAAM,OAC9B,WAAW,MAAM,EAAE,CAAC,CACpB,MAAM,kBAAkB,CAAC,GAAG;CAC9B,IAAI,CAAC,mBACJ,MAAM,IAAI,yBAAyB,aAAa,MAAM,IAAI;CAW3D,MAAM,cAAa,MAPA,IAAI,eAAe;EACrC;EACA;EACA;EACA,OAAO,OAAO,IAAI;EAClB,OAAO;CACR,CAAC,EAAA,CACuB,OACtB,MAAM,IAAI,CAAC,CACX,QAAQ,SAAS,KAAK,KAAK,KAAK,CAAC,KAAK,WAAW,GAAG,CAAC,CAAC,CACtD,KAAK,IAAI;CACX,MAAM,CAAC,UAAU,MAAM,MAAM,aAAa,KAAK,UAAU;CACzD,IAAI,YAAY,mBACf,MAAM,IAAI,6BACT,OAAO,MACP,mBACA,OACD;CAED,QAAQ,IAAI,kBAAkB,QAAQ,2BAA2B;CAEjE,IAAI,MAAM;EACT,GAAG;GAAC;GAAO;GAAY;GAAO,SAAS,KAAK,gBAAgB;EAAO,CAAC;EACpE,GAAG;GACF;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACD,CAAC;EAED,GACC;GAAC;GAAU;GAAO;GAAkB;GAAS;GAAO;GAAU;EAAI,GAClE,MAAM,SAAS,SAAS,OAAO,CAChC;EACA,QAAQ,IACP,4BAA4B,MAAM,yCACnC;CACD;CAGA,MAAM,QAAQ,KAAK,QAAQ,OAAO,GAAG,cAAc,UAAU,KAAK,GAAG,OAAO;CAC5E,MAAM,UAAU,OAAO,GAAG,WAAW,GAAG;CACxC,IAAI;EACH,MAAM,OAAO;GACZ;GACA;GACA;GACA;GACA;GACA;GACA;GACA,sBAAsB;GACtB;GACA;GACA,GAAG,WAAW,MAAM;EACrB;EACA,MAAM,SAAS,MAAM,IAAI,OAAO;GAC/B,GAAG;GACH;GACA;GACA;GACA;GACA;EACD,CAAC;EACD,IAAI,OAAO,SAAS,GACnB,MAAM,IAAI,yBAAyB,cAAc,UAAU,OAAO,MAAM;EAEzE,QAAQ,IACP,OAAO,aAAa,uBAAuB,OAAO,OAAO,KAAK,EAAE,EACjE;EAGA,MAAM,SAAS,MAAM,IAAI,OAAO;GAC/B,GAAG;GACH;GACA,aAAa,OAAO;GACpB;EACD,CAAC;EACD,IAAI,CAAC,OAAO,OAAO,WAAW,SAAS,GACtC,MAAM,IAAI,yBACT,cACA,UACA,OAAO,OAAO,MAAM,GAAG,EAAE,KAAK,OAAO,MACtC;EAED,QAAQ,IAAI,OAAO,aAAa,4BAA4B;CAC7D,UAAU;EACT,MAAM,GAAG,OAAO,EAAE,OAAO,KAAK,CAAC;CAChC;CAMA,OAAM,MAHgB,cAAc,KAAK,OAAO,aAC/C,mBAAmB,KAAK,CACzB,EAAA,CACc,MACb;EACC,KAAK;EACL,MAAM;EACN,MAAM,EAAE,MAAM,OAAO,KAAK;CAC3B,GACA,OAAO,IACR;CACA,QAAQ,IAAI,wBAAwB,MAAM,QAAQ;CAElD,QAAQ,IACP,OAAO,OAAO,KAAK,iBAAiB,MAAM,IAAI,OAAO,8CAA8C,GAAG,oDACjD,QAAQ,GAC9D;CACA,OAAO;EACN;EACA;EACA;EACA;EACA,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;CACxB;AACD"}
|