@absolutejs/deploy 0.20.0 → 0.21.0
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/cloudTarget.d.ts +4 -4
- package/dist/cloudflare.d.ts +2 -2
- package/dist/cloudflare.js.map +2 -2
- package/dist/deployer.d.ts +9 -9
- package/dist/digitalocean.d.ts +5 -5
- package/dist/digitalocean.js +2 -8
- package/dist/digitalocean.js.map +5 -5
- package/dist/digitaloceanDns.d.ts +2 -2
- package/dist/digitaloceanDns.js +2 -8
- package/dist/digitaloceanDns.js.map +6 -6
- package/dist/digitaloceanEphemeralInfrastructure.d.ts +20 -0
- package/dist/digitaloceanEphemeralInfrastructure.js +610 -0
- package/dist/digitaloceanEphemeralInfrastructure.js.map +13 -0
- package/dist/digitaloceanInfrastructure.js +2 -8
- package/dist/digitaloceanInfrastructure.js.map +6 -6
- package/dist/digitaloceanIngress.js +2 -8
- package/dist/digitaloceanIngress.js.map +5 -5
- package/dist/dns.d.ts +1 -1
- package/dist/dns.js.map +2 -2
- package/dist/env.d.ts +1 -1
- package/dist/env.js.map +2 -2
- package/dist/ephemeralInfrastructure.d.ts +37 -0
- package/dist/ephemeralInfrastructure.js +4 -0
- package/dist/ephemeralInfrastructure.js.map +9 -0
- package/dist/gcp.js +2 -6
- package/dist/gcp.js.map +3 -3
- package/dist/hetzner.d.ts +3 -3
- package/dist/hetzner.js +2 -8
- package/dist/hetzner.js.map +5 -5
- package/dist/hetznerDns.d.ts +2 -2
- package/dist/hetznerDns.js.map +2 -2
- package/dist/hetznerInfrastructure.js +2 -8
- package/dist/hetznerInfrastructure.js.map +7 -7
- package/dist/index.d.ts +10 -10
- package/dist/index.js +16 -64
- package/dist/index.js.map +5 -5
- package/dist/linode.d.ts +3 -3
- package/dist/linode.js +2 -8
- package/dist/linode.js.map +5 -5
- package/dist/linodeInfrastructure.js +3 -13
- package/dist/linodeInfrastructure.js.map +7 -7
- package/dist/preview.d.ts +3 -3
- package/dist/preview.js +2 -5
- package/dist/preview.js.map +3 -3
- package/dist/processManagers.d.ts +4 -4
- package/dist/route53.d.ts +2 -2
- package/dist/route53.js.map +2 -2
- package/dist/targets.d.ts +1 -1
- package/dist/tls.d.ts +5 -5
- package/dist/tls.js +2 -5
- package/dist/tls.js.map +3 -3
- package/dist/vultr.d.ts +4 -4
- package/dist/vultr.js +2 -8
- package/dist/vultr.js.map +5 -5
- package/dist/vultrInfrastructure.js +2 -8
- package/dist/vultrInfrastructure.js.map +7 -7
- package/package.json +12 -5
package/dist/preview.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/preview.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Preview-deploys fleet for `@absolutejs/deploy`.\n *\n * A `PreviewFleet` is a tenant-aware orchestrator that creates,\n * lists, and tears down ephemeral environments — one per PR /\n * branch / commit. It composes the existing `Deployer` (release\n * dirs + atomic symlink swap + rollback) with a `DnsProvider` and a\n * persistent registry so a deploy bot can:\n *\n * const url = await fleet.create({ previewId: 'pr-42', ... });\n *\n * await fleet.teardown('pr-42');\n *\n * await fleet.gc({ olderThanMs: 7 * 24 * 60 * 60 * 1000 });\n *\n * Substrate responsibilities:\n *\n * - allocate a free port from a configurable pool\n * - build the hostname `<previewId>.<baseDomain>`\n * - upsert an A record for that hostname (DNS provider injected)\n * - call a caller-supplied `makeDeployer({ previewId, port,\n * hostname })` factory, then run `deployer.deploy()` so the\n * preview lands as a normal release on disk\n * - persist the preview registry so we can list / GC later\n * - on teardown, run a caller-supplied stop callback, remove the\n * DNS record, drop the registry entry\n *\n * Everything tenant-specific (secrets snapshotting, db seeding,\n * stopping the process) is a caller-supplied hook. The fleet owns\n * fleet bookkeeping, not application logic.\n */\n\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { Deployer, DeployResult, ReleaseAnnotations } from \"./deployer\";\nimport type { DnsProvider, DnsRecord, DnsRecordSpec } from \"./dns\";\n\n// =============================================================================\n// Registry shape\n// =============================================================================\n\nexport type PreviewRecord = {\n previewId: string;\n hostname: string;\n url: string;\n port: number;\n dnsRecordId?: string;\n releaseId: string;\n commitSha?: string;\n createdAt: number;\n annotations?: ReleaseAnnotations;\n};\n\nexport type PreviewStore = {\n list: () => Promise<PreviewRecord[]>;\n get: (previewId: string) => Promise<PreviewRecord | null>;\n put: (record: PreviewRecord) => Promise<void>;\n remove: (previewId: string) => Promise<void>;\n};\n\n// =============================================================================\n// Hooks + options\n// =============================================================================\n\nexport type PreviewDeployerContext = {\n previewId: string;\n port: number;\n hostname: string;\n url: string;\n};\n\nexport type PreviewFleetOptions = {\n /**\n * Apex used to build preview hostnames: `<previewId>.<baseDomain>`.\n * `previewId` is slugified before use (alphanumeric + `-`).\n */\n baseDomain: string;\n /**\n * Optional URL scheme. Defaults to `'https'`. Set `'http'` for\n * local-loop previews behind a localhost proxy.\n */\n scheme?: \"http\" | \"https\";\n /**\n * DNS provider — `cloudflareProvider`, `route53Provider`, etc.\n * Optional: when absent, `fleet.create` skips DNS work and the\n * caller is responsible for routing requests at the hostname.\n */\n dns?: DnsProvider;\n /**\n * Public IPv4 the A record should point at. Required when `dns`\n * is set. The fleet calls `dns.upsert({ name, type: 'A',\n * content: ipv4 })`.\n */\n ipv4?: string;\n /**\n * TTL applied to created A records. Default 60s — previews come\n * and go and we want low cache lifetime.\n */\n dnsTtl?: number;\n /**\n * Proxied flag (Cloudflare orange-cloud). Default `false` — most\n * previews want a real IP, not a Cloudflare proxy.\n */\n dnsProxied?: boolean;\n /**\n * Port range that the fleet allocates from. Default `[3100, 3899]`\n * (3000 squat is documented in memory).\n */\n portRange?: { start: number; end: number };\n /** Override port allocation entirely (e.g. talk to a port-leaser). */\n allocatePort?: (record: {\n previewId: string;\n hostname: string;\n }) => Promise<number>;\n /**\n * REQUIRED. Build a `Deployer` for an individual preview. The\n * factory receives the resolved id, port, hostname, and URL —\n * use them to set the right env, processManager, and target.\n */\n makeDeployer: (ctx: PreviewDeployerContext) => Deployer | Promise<Deployer>;\n /**\n * Stop callback. Called by `teardown` BEFORE DNS removal so the\n * preview process stops accepting connections before the record\n * disappears. Use it to call `processManager.stop()`, kill the\n * port, etc.\n */\n stop?: (record: PreviewRecord) => Promise<void> | void;\n /**\n * After-teardown callback. Called after DNS + registry removal —\n * good place to delete the release directory, drop a tenant\n * schema, clear secrets, etc.\n */\n afterTeardown?: (record: PreviewRecord) => Promise<void> | void;\n /** Registry. Default = filesystem store at `<root>`. */\n store?: PreviewStore;\n /** Directory the file-based default store writes into. */\n registryRoot?: string;\n /** Override `Date.now()` for tests. */\n clock?: () => number;\n};\n\n// =============================================================================\n// File-based PreviewStore (default)\n// =============================================================================\n\n/**\n * Single-file JSON registry. Reads + writes atomically (temp-file\n * + rename). Adequate for a deploy bot on one host — swap in a\n * Postgres-backed store for distributed deploy bots.\n */\nexport const createFilePreviewStore = (root: string): PreviewStore => {\n const path = join(root, \"previews.json\");\n\n const readAll = (): PreviewRecord[] => {\n if (!existsSync(path)) return [];\n try {\n const raw = readFileSync(path, \"utf8\");\n if (raw.trim() === \"\") return [];\n const parsed = JSON.parse(raw) as { previews?: PreviewRecord[] };\n return Array.isArray(parsed.previews) ? parsed.previews : [];\n } catch {\n return [];\n }\n };\n\n const writeAll = (records: PreviewRecord[]): void => {\n mkdirSync(dirname(path), { recursive: true });\n const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;\n writeFileSync(tmp, JSON.stringify({ previews: records }, null, 2));\n renameSync(tmp, path);\n };\n\n return {\n get: async (previewId) =>\n readAll().find((r) => r.previewId === previewId) ?? null,\n list: async () => readAll(),\n put: async (record) => {\n const records = readAll().filter((r) => r.previewId !== record.previewId);\n records.push(record);\n writeAll(records);\n },\n remove: async (previewId) => {\n const records = readAll().filter((r) => r.previewId !== previewId);\n writeAll(records);\n },\n };\n};\n\n// =============================================================================\n// In-memory store (for tests + ephemeral fleets)\n// =============================================================================\n\nexport const createMemoryPreviewStore = (): PreviewStore => {\n const map = new Map<string, PreviewRecord>();\n return {\n get: async (previewId) => map.get(previewId) ?? null,\n list: async () => Array.from(map.values()),\n put: async (record) => {\n map.set(record.previewId, record);\n },\n remove: async (previewId) => {\n map.delete(previewId);\n },\n };\n};\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nconst slugify = (id: string): string =>\n id\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 50);\n\nconst defaultAllocatePort = (\n used: Set<number>,\n range: { start: number; end: number },\n): number => {\n for (let port = range.start; port <= range.end; port += 1) {\n if (!used.has(port)) return port;\n }\n throw new Error(\n `preview-fleet: no free ports in [${range.start}, ${range.end}] (${used.size} in use)`,\n );\n};\n\n// =============================================================================\n// Fleet\n// =============================================================================\n\nexport type CreatePreviewInput = {\n previewId: string;\n commitSha?: string;\n annotations?: ReleaseAnnotations;\n /**\n * Override hostname. Default = `<slug(previewId)>.<baseDomain>`.\n * Use this for previews with a vanity name that shouldn't echo\n * the PR id directly.\n */\n hostname?: string;\n};\n\nexport type CreatePreviewResult = {\n record: PreviewRecord;\n deploy: DeployResult;\n};\n\nexport type PreviewFleet = {\n /**\n * Spin up a preview. Idempotent on `previewId` — if a record\n * already exists, the fleet re-uses its port + DNS and runs a\n * fresh `deployer.deploy()` (so PRs that push new commits roll\n * forward).\n */\n create: (input: CreatePreviewInput) => Promise<CreatePreviewResult>;\n /** Tear down a single preview (stop → DNS remove → registry remove → afterTeardown). */\n teardown: (previewId: string) => Promise<void>;\n /** List active previews. */\n list: () => Promise<PreviewRecord[]>;\n /** Get one preview by id. */\n get: (previewId: string) => Promise<PreviewRecord | null>;\n /**\n * Tear down all previews older than `olderThanMs`. Returns the\n * list of preview ids torn down. Failures on individual previews\n * are swallowed and reported on `errors`.\n */\n gc: (options: { olderThanMs: number }) => Promise<{\n removed: string[];\n errors: { previewId: string; error: Error }[];\n }>;\n};\n\nexport const createPreviewFleet = (\n options: PreviewFleetOptions,\n): PreviewFleet => {\n const scheme = options.scheme ?? \"https\";\n const portRange = options.portRange ?? { end: 3899, start: 3100 };\n const dnsTtl = options.dnsTtl ?? 60;\n const dnsProxied = options.dnsProxied ?? false;\n const clock = options.clock ?? Date.now;\n const store =\n options.store ??\n createFilePreviewStore(\n options.registryRoot ?? join(process.cwd(), \".preview-fleet\"),\n );\n\n if (options.dns !== undefined && options.ipv4 === undefined) {\n throw new Error(\n \"preview-fleet: `ipv4` is required when `dns` is configured\",\n );\n }\n\n const ensureDns = async (\n hostname: string,\n ): Promise<DnsRecord | undefined> => {\n if (options.dns === undefined) return undefined;\n const spec: DnsRecordSpec = {\n content: options.ipv4!,\n name: hostname,\n proxied: dnsProxied,\n ttl: dnsTtl,\n type: \"A\",\n };\n return await options.dns.upsert(spec);\n };\n\n const removeDns = async (record: PreviewRecord): Promise<void> => {\n if (options.dns === undefined || record.dnsRecordId === undefined) {\n return;\n }\n try {\n await options.dns.delete(record.dnsRecordId);\n } catch {\n // Idempotent teardown — the record may already be gone. Don't\n // block the rest of teardown on a stale id.\n }\n };\n\n const buildHostname = (previewId: string, override?: string): string => {\n if (override !== undefined) return override;\n const slug = slugify(previewId);\n if (slug === \"\") {\n throw new Error(\n `preview-fleet: previewId ${JSON.stringify(previewId)} slugifies to empty`,\n );\n }\n return `${slug}.${options.baseDomain}`;\n };\n\n const create = async (\n input: CreatePreviewInput,\n ): Promise<CreatePreviewResult> => {\n const existing = await store.get(input.previewId);\n const hostname = buildHostname(input.previewId, input.hostname);\n const url = `${scheme}://${hostname}`;\n\n // Allocate a port — reuse existing if we're re-deploying.\n let port: number;\n if (existing !== null) {\n port = existing.port;\n } else if (options.allocatePort !== undefined) {\n port = await options.allocatePort({\n hostname,\n previewId: input.previewId,\n });\n } else {\n const used = new Set((await store.list()).map((record) => record.port));\n port = defaultAllocatePort(used, portRange);\n }\n\n const deployer = await options.makeDeployer({\n hostname,\n port,\n previewId: input.previewId,\n url,\n });\n\n const deployOptions =\n input.annotations !== undefined\n ? { annotations: input.annotations }\n : input.commitSha !== undefined\n ? { annotations: { commitSha: input.commitSha } }\n : undefined;\n const deployResult = await deployer.deploy(deployOptions);\n\n // DNS only after the deploy succeeded — no point flipping the\n // record at a half-baked release.\n const dnsRecord = await ensureDns(hostname);\n\n const record: PreviewRecord = {\n createdAt: existing?.createdAt ?? clock(),\n hostname,\n port,\n previewId: input.previewId,\n releaseId: deployResult.releaseId,\n url,\n ...(dnsRecord !== undefined ? { dnsRecordId: dnsRecord.id } : {}),\n ...(input.commitSha !== undefined ? { commitSha: input.commitSha } : {}),\n ...(input.annotations !== undefined\n ? { annotations: input.annotations }\n : {}),\n };\n\n await store.put(record);\n return { deploy: deployResult, record };\n };\n\n const teardown = async (previewId: string): Promise<void> => {\n const record = await store.get(previewId);\n if (record === null) return;\n\n if (options.stop !== undefined) {\n try {\n await options.stop(record);\n } catch {\n // Don't block DNS removal on a stop failure. The caller\n // can see the error via afterTeardown by re-querying.\n }\n }\n\n await removeDns(record);\n await store.remove(previewId);\n\n if (options.afterTeardown !== undefined) {\n try {\n await options.afterTeardown(record);\n } catch {\n // Caller's own teardown errors are their problem; the\n // substrate has already removed the record + DNS.\n }\n }\n };\n\n const gc = async ({\n olderThanMs,\n }: {\n olderThanMs: number;\n }): Promise<{\n removed: string[];\n errors: { previewId: string; error: Error }[];\n }> => {\n const cutoff = clock() - olderThanMs;\n const all = await store.list();\n const expired = all.filter((record) => record.createdAt < cutoff);\n const removed: string[] = [];\n const errors: { previewId: string; error: Error }[] = [];\n for (const record of expired) {\n try {\n await teardown(record.previewId);\n removed.push(record.previewId);\n } catch (e) {\n errors.push({\n error: e instanceof Error ? e : new Error(String(e)),\n previewId: record.previewId,\n });\n }\n }\n return { errors, removed };\n };\n\n return {\n create,\n gc,\n get: (previewId) => store.get(previewId),\n list: () => store.list(),\n teardown,\n };\n};\n"
|
|
5
|
+
"/**\n * Preview-deploys fleet for `@absolutejs/deploy`.\n *\n * A `PreviewFleet` is a tenant-aware orchestrator that creates,\n * lists, and tears down ephemeral environments — one per PR /\n * branch / commit. It composes the existing `Deployer` (release\n * dirs + atomic symlink swap + rollback) with a `DnsProvider` and a\n * persistent registry so a deploy bot can:\n *\n * const url = await fleet.create({ previewId: 'pr-42', ... });\n *\n * await fleet.teardown('pr-42');\n *\n * await fleet.gc({ olderThanMs: 7 * 24 * 60 * 60 * 1000 });\n *\n * Substrate responsibilities:\n *\n * - allocate a free port from a configurable pool\n * - build the hostname `<previewId>.<baseDomain>`\n * - upsert an A record for that hostname (DNS provider injected)\n * - call a caller-supplied `makeDeployer({ previewId, port,\n * hostname })` factory, then run `deployer.deploy()` so the\n * preview lands as a normal release on disk\n * - persist the preview registry so we can list / GC later\n * - on teardown, run a caller-supplied stop callback, remove the\n * DNS record, drop the registry entry\n *\n * Everything tenant-specific (secrets snapshotting, db seeding,\n * stopping the process) is a caller-supplied hook. The fleet owns\n * fleet bookkeeping, not application logic.\n */\n\nimport {\n\texistsSync,\n\tmkdirSync,\n\treadFileSync,\n\trenameSync,\n\twriteFileSync\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport type {\n\tDeployer,\n\tDeployResult,\n\tReleaseAnnotations\n} from './deployer';\nimport type { DnsProvider, DnsRecord, DnsRecordSpec } from './dns';\n\n// =============================================================================\n// Registry shape\n// =============================================================================\n\nexport type PreviewRecord = {\n\tpreviewId: string;\n\thostname: string;\n\turl: string;\n\tport: number;\n\tdnsRecordId?: string;\n\treleaseId: string;\n\tcommitSha?: string;\n\tcreatedAt: number;\n\tannotations?: ReleaseAnnotations;\n};\n\nexport type PreviewStore = {\n\tlist: () => Promise<PreviewRecord[]>;\n\tget: (previewId: string) => Promise<PreviewRecord | null>;\n\tput: (record: PreviewRecord) => Promise<void>;\n\tremove: (previewId: string) => Promise<void>;\n};\n\n// =============================================================================\n// Hooks + options\n// =============================================================================\n\nexport type PreviewDeployerContext = {\n\tpreviewId: string;\n\tport: number;\n\thostname: string;\n\turl: string;\n};\n\nexport type PreviewFleetOptions = {\n\t/**\n\t * Apex used to build preview hostnames: `<previewId>.<baseDomain>`.\n\t * `previewId` is slugified before use (alphanumeric + `-`).\n\t */\n\tbaseDomain: string;\n\t/**\n\t * Optional URL scheme. Defaults to `'https'`. Set `'http'` for\n\t * local-loop previews behind a localhost proxy.\n\t */\n\tscheme?: 'http' | 'https';\n\t/**\n\t * DNS provider — `cloudflareProvider`, `route53Provider`, etc.\n\t * Optional: when absent, `fleet.create` skips DNS work and the\n\t * caller is responsible for routing requests at the hostname.\n\t */\n\tdns?: DnsProvider;\n\t/**\n\t * Public IPv4 the A record should point at. Required when `dns`\n\t * is set. The fleet calls `dns.upsert({ name, type: 'A',\n\t * content: ipv4 })`.\n\t */\n\tipv4?: string;\n\t/**\n\t * TTL applied to created A records. Default 60s — previews come\n\t * and go and we want low cache lifetime.\n\t */\n\tdnsTtl?: number;\n\t/**\n\t * Proxied flag (Cloudflare orange-cloud). Default `false` — most\n\t * previews want a real IP, not a Cloudflare proxy.\n\t */\n\tdnsProxied?: boolean;\n\t/**\n\t * Port range that the fleet allocates from. Default `[3100, 3899]`\n\t * (3000 squat is documented in memory).\n\t */\n\tportRange?: { start: number; end: number };\n\t/** Override port allocation entirely (e.g. talk to a port-leaser). */\n\tallocatePort?: (record: {\n\t\tpreviewId: string;\n\t\thostname: string;\n\t}) => Promise<number>;\n\t/**\n\t * REQUIRED. Build a `Deployer` for an individual preview. The\n\t * factory receives the resolved id, port, hostname, and URL —\n\t * use them to set the right env, processManager, and target.\n\t */\n\tmakeDeployer: (\n\t\tctx: PreviewDeployerContext\n\t) => Deployer | Promise<Deployer>;\n\t/**\n\t * Stop callback. Called by `teardown` BEFORE DNS removal so the\n\t * preview process stops accepting connections before the record\n\t * disappears. Use it to call `processManager.stop()`, kill the\n\t * port, etc.\n\t */\n\tstop?: (record: PreviewRecord) => Promise<void> | void;\n\t/**\n\t * After-teardown callback. Called after DNS + registry removal —\n\t * good place to delete the release directory, drop a tenant\n\t * schema, clear secrets, etc.\n\t */\n\tafterTeardown?: (record: PreviewRecord) => Promise<void> | void;\n\t/** Registry. Default = filesystem store at `<root>`. */\n\tstore?: PreviewStore;\n\t/** Directory the file-based default store writes into. */\n\tregistryRoot?: string;\n\t/** Override `Date.now()` for tests. */\n\tclock?: () => number;\n};\n\n// =============================================================================\n// File-based PreviewStore (default)\n// =============================================================================\n\n/**\n * Single-file JSON registry. Reads + writes atomically (temp-file\n * + rename). Adequate for a deploy bot on one host — swap in a\n * Postgres-backed store for distributed deploy bots.\n */\nexport const createFilePreviewStore = (root: string): PreviewStore => {\n\tconst path = join(root, 'previews.json');\n\n\tconst readAll = (): PreviewRecord[] => {\n\t\tif (!existsSync(path)) return [];\n\t\ttry {\n\t\t\tconst raw = readFileSync(path, 'utf8');\n\t\t\tif (raw.trim() === '') return [];\n\t\t\tconst parsed = JSON.parse(raw) as { previews?: PreviewRecord[] };\n\t\t\treturn Array.isArray(parsed.previews) ? parsed.previews : [];\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t};\n\n\tconst writeAll = (records: PreviewRecord[]): void => {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\tconst tmp = `${path}.tmp-${process.pid}-${Date.now()}`;\n\t\twriteFileSync(tmp, JSON.stringify({ previews: records }, null, 2));\n\t\trenameSync(tmp, path);\n\t};\n\n\treturn {\n\t\tget: async (previewId) =>\n\t\t\treadAll().find((r) => r.previewId === previewId) ?? null,\n\t\tlist: async () => readAll(),\n\t\tput: async (record) => {\n\t\t\tconst records = readAll().filter(\n\t\t\t\t(r) => r.previewId !== record.previewId\n\t\t\t);\n\t\t\trecords.push(record);\n\t\t\twriteAll(records);\n\t\t},\n\t\tremove: async (previewId) => {\n\t\t\tconst records = readAll().filter((r) => r.previewId !== previewId);\n\t\t\twriteAll(records);\n\t\t}\n\t};\n};\n\n// =============================================================================\n// In-memory store (for tests + ephemeral fleets)\n// =============================================================================\n\nexport const createMemoryPreviewStore = (): PreviewStore => {\n\tconst map = new Map<string, PreviewRecord>();\n\treturn {\n\t\tget: async (previewId) => map.get(previewId) ?? null,\n\t\tlist: async () => Array.from(map.values()),\n\t\tput: async (record) => {\n\t\t\tmap.set(record.previewId, record);\n\t\t},\n\t\tremove: async (previewId) => {\n\t\t\tmap.delete(previewId);\n\t\t}\n\t};\n};\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nconst slugify = (id: string): string =>\n\tid\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9-]+/g, '-')\n\t\t.replace(/^-+|-+$/g, '')\n\t\t.slice(0, 50);\n\nconst defaultAllocatePort = (\n\tused: Set<number>,\n\trange: { start: number; end: number }\n): number => {\n\tfor (let port = range.start; port <= range.end; port += 1) {\n\t\tif (!used.has(port)) return port;\n\t}\n\tthrow new Error(\n\t\t`preview-fleet: no free ports in [${range.start}, ${range.end}] (${used.size} in use)`\n\t);\n};\n\n// =============================================================================\n// Fleet\n// =============================================================================\n\nexport type CreatePreviewInput = {\n\tpreviewId: string;\n\tcommitSha?: string;\n\tannotations?: ReleaseAnnotations;\n\t/**\n\t * Override hostname. Default = `<slug(previewId)>.<baseDomain>`.\n\t * Use this for previews with a vanity name that shouldn't echo\n\t * the PR id directly.\n\t */\n\thostname?: string;\n};\n\nexport type CreatePreviewResult = {\n\trecord: PreviewRecord;\n\tdeploy: DeployResult;\n};\n\nexport type PreviewFleet = {\n\t/**\n\t * Spin up a preview. Idempotent on `previewId` — if a record\n\t * already exists, the fleet re-uses its port + DNS and runs a\n\t * fresh `deployer.deploy()` (so PRs that push new commits roll\n\t * forward).\n\t */\n\tcreate: (input: CreatePreviewInput) => Promise<CreatePreviewResult>;\n\t/** Tear down a single preview (stop → DNS remove → registry remove → afterTeardown). */\n\tteardown: (previewId: string) => Promise<void>;\n\t/** List active previews. */\n\tlist: () => Promise<PreviewRecord[]>;\n\t/** Get one preview by id. */\n\tget: (previewId: string) => Promise<PreviewRecord | null>;\n\t/**\n\t * Tear down all previews older than `olderThanMs`. Returns the\n\t * list of preview ids torn down. Failures on individual previews\n\t * are swallowed and reported on `errors`.\n\t */\n\tgc: (options: { olderThanMs: number }) => Promise<{\n\t\tremoved: string[];\n\t\terrors: { previewId: string; error: Error }[];\n\t}>;\n};\n\nexport const createPreviewFleet = (\n\toptions: PreviewFleetOptions\n): PreviewFleet => {\n\tconst scheme = options.scheme ?? 'https';\n\tconst portRange = options.portRange ?? { end: 3899, start: 3100 };\n\tconst dnsTtl = options.dnsTtl ?? 60;\n\tconst dnsProxied = options.dnsProxied ?? false;\n\tconst clock = options.clock ?? Date.now;\n\tconst store =\n\t\toptions.store ??\n\t\tcreateFilePreviewStore(\n\t\t\toptions.registryRoot ?? join(process.cwd(), '.preview-fleet')\n\t\t);\n\n\tif (options.dns !== undefined && options.ipv4 === undefined) {\n\t\tthrow new Error(\n\t\t\t'preview-fleet: `ipv4` is required when `dns` is configured'\n\t\t);\n\t}\n\n\tconst ensureDns = async (\n\t\thostname: string\n\t): Promise<DnsRecord | undefined> => {\n\t\tif (options.dns === undefined) return undefined;\n\t\tconst spec: DnsRecordSpec = {\n\t\t\tcontent: options.ipv4!,\n\t\t\tname: hostname,\n\t\t\tproxied: dnsProxied,\n\t\t\tttl: dnsTtl,\n\t\t\ttype: 'A'\n\t\t};\n\t\treturn await options.dns.upsert(spec);\n\t};\n\n\tconst removeDns = async (record: PreviewRecord): Promise<void> => {\n\t\tif (options.dns === undefined || record.dnsRecordId === undefined) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tawait options.dns.delete(record.dnsRecordId);\n\t\t} catch {\n\t\t\t// Idempotent teardown — the record may already be gone. Don't\n\t\t\t// block the rest of teardown on a stale id.\n\t\t}\n\t};\n\n\tconst buildHostname = (previewId: string, override?: string): string => {\n\t\tif (override !== undefined) return override;\n\t\tconst slug = slugify(previewId);\n\t\tif (slug === '') {\n\t\t\tthrow new Error(\n\t\t\t\t`preview-fleet: previewId ${JSON.stringify(previewId)} slugifies to empty`\n\t\t\t);\n\t\t}\n\t\treturn `${slug}.${options.baseDomain}`;\n\t};\n\n\tconst create = async (\n\t\tinput: CreatePreviewInput\n\t): Promise<CreatePreviewResult> => {\n\t\tconst existing = await store.get(input.previewId);\n\t\tconst hostname = buildHostname(input.previewId, input.hostname);\n\t\tconst url = `${scheme}://${hostname}`;\n\n\t\t// Allocate a port — reuse existing if we're re-deploying.\n\t\tlet port: number;\n\t\tif (existing !== null) {\n\t\t\tport = existing.port;\n\t\t} else if (options.allocatePort !== undefined) {\n\t\t\tport = await options.allocatePort({ hostname, previewId: input.previewId });\n\t\t} else {\n\t\t\tconst used = new Set(\n\t\t\t\t(await store.list()).map((record) => record.port)\n\t\t\t);\n\t\t\tport = defaultAllocatePort(used, portRange);\n\t\t}\n\n\t\tconst deployer = await options.makeDeployer({\n\t\t\thostname,\n\t\t\tport,\n\t\t\tpreviewId: input.previewId,\n\t\t\turl\n\t\t});\n\n\t\tconst deployOptions =\n\t\t\tinput.annotations !== undefined\n\t\t\t\t? { annotations: input.annotations }\n\t\t\t\t: input.commitSha !== undefined\n\t\t\t\t\t? { annotations: { commitSha: input.commitSha } }\n\t\t\t\t\t: undefined;\n\t\tconst deployResult = await deployer.deploy(deployOptions);\n\n\t\t// DNS only after the deploy succeeded — no point flipping the\n\t\t// record at a half-baked release.\n\t\tconst dnsRecord = await ensureDns(hostname);\n\n\t\tconst record: PreviewRecord = {\n\t\t\tcreatedAt: existing?.createdAt ?? clock(),\n\t\t\thostname,\n\t\t\tport,\n\t\t\tpreviewId: input.previewId,\n\t\t\treleaseId: deployResult.releaseId,\n\t\t\turl,\n\t\t\t...(dnsRecord !== undefined ? { dnsRecordId: dnsRecord.id } : {}),\n\t\t\t...(input.commitSha !== undefined ? { commitSha: input.commitSha } : {}),\n\t\t\t...(input.annotations !== undefined\n\t\t\t\t? { annotations: input.annotations }\n\t\t\t\t: {})\n\t\t};\n\n\t\tawait store.put(record);\n\t\treturn { deploy: deployResult, record };\n\t};\n\n\tconst teardown = async (previewId: string): Promise<void> => {\n\t\tconst record = await store.get(previewId);\n\t\tif (record === null) return;\n\n\t\tif (options.stop !== undefined) {\n\t\t\ttry {\n\t\t\t\tawait options.stop(record);\n\t\t\t} catch {\n\t\t\t\t// Don't block DNS removal on a stop failure. The caller\n\t\t\t\t// can see the error via afterTeardown by re-querying.\n\t\t\t}\n\t\t}\n\n\t\tawait removeDns(record);\n\t\tawait store.remove(previewId);\n\n\t\tif (options.afterTeardown !== undefined) {\n\t\t\ttry {\n\t\t\t\tawait options.afterTeardown(record);\n\t\t\t} catch {\n\t\t\t\t// Caller's own teardown errors are their problem; the\n\t\t\t\t// substrate has already removed the record + DNS.\n\t\t\t}\n\t\t}\n\t};\n\n\tconst gc = async ({\n\t\tolderThanMs\n\t}: {\n\t\tolderThanMs: number;\n\t}): Promise<{\n\t\tremoved: string[];\n\t\terrors: { previewId: string; error: Error }[];\n\t}> => {\n\t\tconst cutoff = clock() - olderThanMs;\n\t\tconst all = await store.list();\n\t\tconst expired = all.filter((record) => record.createdAt < cutoff);\n\t\tconst removed: string[] = [];\n\t\tconst errors: { previewId: string; error: Error }[] = [];\n\t\tfor (const record of expired) {\n\t\t\ttry {\n\t\t\t\tawait teardown(record.previewId);\n\t\t\t\tremoved.push(record.previewId);\n\t\t\t} catch (e) {\n\t\t\t\terrors.push({\n\t\t\t\t\terror: e instanceof Error ? e : new Error(String(e)),\n\t\t\t\t\tpreviewId: record.previewId\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn { errors, removed };\n\t};\n\n\treturn {\n\t\tcreate,\n\t\tgc,\n\t\tget: (previewId) => store.get(previewId),\n\t\tlist: () => store.list(),\n\t\tteardown\n\t};\n};\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA;AA2HO,IAAM,yBAAyB,CAAC,SAA+B;AAAA,EACrE,MAAM,OAAO,KAAK,MAAM,eAAe;AAAA,EAEvC,MAAM,UAAU,MAAuB;AAAA,IACtC,IAAI,CAAC,WAAW,IAAI;AAAA,MAAG,OAAO,CAAC;AAAA,IAC/B,IAAI;AAAA,MACH,MAAM,MAAM,aAAa,MAAM,MAAM;AAAA,MACrC,IAAI,IAAI,KAAK,MAAM;AAAA,QAAI,OAAO,CAAC;AAAA,MAC/B,MAAM,SAAS,KAAK,MAAM,GAAG;AAAA,MAC7B,OAAO,MAAM,QAAQ,OAAO,QAAQ,IAAI,OAAO,WAAW,CAAC;AAAA,MAC1D,MAAM;AAAA,MACP,OAAO,CAAC;AAAA;AAAA;AAAA,EAIV,MAAM,WAAW,CAAC,YAAmC;AAAA,IACpD,UAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAAA,IAC5C,MAAM,MAAM,GAAG,YAAY,QAAQ,OAAO,KAAK,IAAI;AAAA,IACnD,cAAc,KAAK,KAAK,UAAU,EAAE,UAAU,QAAQ,GAAG,MAAM,CAAC,CAAC;AAAA,IACjE,WAAW,KAAK,IAAI;AAAA;AAAA,EAGrB,OAAO;AAAA,IACN,KAAK,OAAO,cACX,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE,cAAc,SAAS,KAAK;AAAA,IACrD,MAAM,YAAY,QAAQ;AAAA,IAC1B,KAAK,OAAO,WAAW;AAAA,MACtB,MAAM,UAAU,QAAQ,EAAE,OACzB,CAAC,MAAM,EAAE,cAAc,OAAO,SAC/B;AAAA,MACA,QAAQ,KAAK,MAAM;AAAA,MACnB,SAAS,OAAO;AAAA;AAAA,IAEjB,QAAQ,OAAO,cAAc;AAAA,MAC5B,MAAM,UAAU,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,cAAc,SAAS;AAAA,MACjE,SAAS,OAAO;AAAA;AAAA,EAElB;AAAA;AAOM,IAAM,2BAA2B,MAAoB;AAAA,EAC3D,MAAM,MAAM,IAAI;AAAA,EAChB,OAAO;AAAA,IACN,KAAK,OAAO,cAAc,IAAI,IAAI,SAAS,KAAK;AAAA,IAChD,MAAM,YAAY,MAAM,KAAK,IAAI,OAAO,CAAC;AAAA,IACzC,KAAK,OAAO,WAAW;AAAA,MACtB,IAAI,IAAI,OAAO,WAAW,MAAM;AAAA;AAAA,IAEjC,QAAQ,OAAO,cAAc;AAAA,MAC5B,IAAI,OAAO,SAAS;AAAA;AAAA,EAEtB;AAAA;AAOD,IAAM,UAAU,CAAC,OAChB,GACE,YAAY,EACZ,QAAQ,gBAAgB,GAAG,EAC3B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE;AAEd,IAAM,sBAAsB,CAC3B,MACA,UACY;AAAA,EACZ,SAAS,OAAO,MAAM,MAAO,QAAQ,MAAM,KAAK,QAAQ,GAAG;AAAA,IAC1D,IAAI,CAAC,KAAK,IAAI,IAAI;AAAA,MAAG,OAAO;AAAA,EAC7B;AAAA,EACA,MAAM,IAAI,MACT,oCAAoC,MAAM,UAAU,MAAM,SAAS,KAAK,cACzE;AAAA;AAiDM,IAAM,qBAAqB,CACjC,YACkB;AAAA,EAClB,MAAM,SAAS,QAAQ,UAAU;AAAA,EACjC,MAAM,YAAY,QAAQ,aAAa,EAAE,KAAK,MAAM,OAAO,KAAK;AAAA,EAChE,MAAM,SAAS,QAAQ,UAAU;AAAA,EACjC,MAAM,aAAa,QAAQ,cAAc;AAAA,EACzC,MAAM,QAAQ,QAAQ,SAAS,KAAK;AAAA,EACpC,MAAM,QACL,QAAQ,SACR,uBACC,QAAQ,gBAAgB,KAAK,QAAQ,IAAI,GAAG,gBAAgB,CAC7D;AAAA,EAED,IAAI,QAAQ,QAAQ,aAAa,QAAQ,SAAS,WAAW;AAAA,IAC5D,MAAM,IAAI,MACT,4DACD;AAAA,EACD;AAAA,EAEA,MAAM,YAAY,OACjB,aACoC;AAAA,IACpC,IAAI,QAAQ,QAAQ;AAAA,MAAW;AAAA,IAC/B,MAAM,OAAsB;AAAA,MAC3B,SAAS,QAAQ;AAAA,MACjB,MAAM;AAAA,MACN,SAAS;AAAA,MACT,KAAK;AAAA,MACL,MAAM;AAAA,IACP;AAAA,IACA,OAAO,MAAM,QAAQ,IAAI,OAAO,IAAI;AAAA;AAAA,EAGrC,MAAM,YAAY,OAAO,WAAyC;AAAA,IACjE,IAAI,QAAQ,QAAQ,aAAa,OAAO,gBAAgB,WAAW;AAAA,MAClE;AAAA,IACD;AAAA,IACA,IAAI;AAAA,MACH,MAAM,QAAQ,IAAI,OAAO,OAAO,WAAW;AAAA,MAC1C,MAAM;AAAA;AAAA,EAMT,MAAM,gBAAgB,CAAC,WAAmB,aAA8B;AAAA,IACvE,IAAI,aAAa;AAAA,MAAW,OAAO;AAAA,IACnC,MAAM,OAAO,QAAQ,SAAS;AAAA,IAC9B,IAAI,SAAS,IAAI;AAAA,MAChB,MAAM,IAAI,MACT,4BAA4B,KAAK,UAAU,SAAS,sBACrD;AAAA,IACD;AAAA,IACA,OAAO,GAAG,QAAQ,QAAQ;AAAA;AAAA,EAG3B,MAAM,SAAS,OACd,UACkC;AAAA,IAClC,MAAM,WAAW,MAAM,MAAM,IAAI,MAAM,SAAS;AAAA,IAChD,MAAM,WAAW,cAAc,MAAM,WAAW,MAAM,QAAQ;AAAA,IAC9D,MAAM,MAAM,GAAG,YAAY;AAAA,IAG3B,IAAI;AAAA,IACJ,IAAI,aAAa,MAAM;AAAA,MACtB,OAAO,SAAS;AAAA,IACjB,EAAO,SAAI,QAAQ,iBAAiB,WAAW;AAAA,MAC9C,OAAO,MAAM,QAAQ,aAAa,EAAE,UAAU,WAAW,MAAM,UAAU,CAAC;AAAA,IAC3E,EAAO;AAAA,MACN,MAAM,OAAO,IAAI,KACf,MAAM,MAAM,KAAK,GAAG,IAAI,CAAC,YAAW,QAAO,IAAI,CACjD;AAAA,MACA,OAAO,oBAAoB,MAAM,SAAS;AAAA;AAAA,IAG3C,MAAM,WAAW,MAAM,QAAQ,aAAa;AAAA,MAC3C;AAAA,MACA;AAAA,MACA,WAAW,MAAM;AAAA,MACjB;AAAA,IACD,CAAC;AAAA,IAED,MAAM,gBACL,MAAM,gBAAgB,YACnB,EAAE,aAAa,MAAM,YAAY,IACjC,MAAM,cAAc,YACnB,EAAE,aAAa,EAAE,WAAW,MAAM,UAAU,EAAE,IAC9C;AAAA,IACL,MAAM,eAAe,MAAM,SAAS,OAAO,aAAa;AAAA,IAIxD,MAAM,YAAY,MAAM,UAAU,QAAQ;AAAA,IAE1C,MAAM,SAAwB;AAAA,MAC7B,WAAW,UAAU,aAAa,MAAM;AAAA,MACxC;AAAA,MACA;AAAA,MACA,WAAW,MAAM;AAAA,MACjB,WAAW,aAAa;AAAA,MACxB;AAAA,SACI,cAAc,YAAY,EAAE,aAAa,UAAU,GAAG,IAAI,CAAC;AAAA,SAC3D,MAAM,cAAc,YAAY,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,SAClE,MAAM,gBAAgB,YACvB,EAAE,aAAa,MAAM,YAAY,IACjC,CAAC;AAAA,IACL;AAAA,IAEA,MAAM,MAAM,IAAI,MAAM;AAAA,IACtB,OAAO,EAAE,QAAQ,cAAc,OAAO;AAAA;AAAA,EAGvC,MAAM,WAAW,OAAO,cAAqC;AAAA,IAC5D,MAAM,SAAS,MAAM,MAAM,IAAI,SAAS;AAAA,IACxC,IAAI,WAAW;AAAA,MAAM;AAAA,IAErB,IAAI,QAAQ,SAAS,WAAW;AAAA,MAC/B,IAAI;AAAA,QACH,MAAM,QAAQ,KAAK,MAAM;AAAA,QACxB,MAAM;AAAA,IAIT;AAAA,IAEA,MAAM,UAAU,MAAM;AAAA,IACtB,MAAM,MAAM,OAAO,SAAS;AAAA,IAE5B,IAAI,QAAQ,kBAAkB,WAAW;AAAA,MACxC,IAAI;AAAA,QACH,MAAM,QAAQ,cAAc,MAAM;AAAA,QACjC,MAAM;AAAA,IAIT;AAAA;AAAA,EAGD,MAAM,KAAK;AAAA,IACV;AAAA,QAMK;AAAA,IACL,MAAM,SAAS,MAAM,IAAI;AAAA,IACzB,MAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC7B,MAAM,UAAU,IAAI,OAAO,CAAC,WAAW,OAAO,YAAY,MAAM;AAAA,IAChE,MAAM,UAAoB,CAAC;AAAA,IAC3B,MAAM,SAAgD,CAAC;AAAA,IACvD,WAAW,UAAU,SAAS;AAAA,MAC7B,IAAI;AAAA,QACH,MAAM,SAAS,OAAO,SAAS;AAAA,QAC/B,QAAQ,KAAK,OAAO,SAAS;AAAA,QAC5B,OAAO,GAAG;AAAA,QACX,OAAO,KAAK;AAAA,UACX,OAAO,aAAa,QAAQ,IAAI,IAAI,MAAM,OAAO,CAAC,CAAC;AAAA,UACnD,WAAW,OAAO;AAAA,QACnB,CAAC;AAAA;AAAA,IAEH;AAAA,IACA,OAAO,EAAE,QAAQ,QAAQ;AAAA;AAAA,EAG1B,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA,KAAK,CAAC,cAAc,MAAM,IAAI,SAAS;AAAA,IACvC,MAAM,MAAM,MAAM,KAAK;AAAA,IACvB;AAAA,EACD;AAAA;",
|
|
8
|
+
"debugId": "1BF9EBCE13FAA19F64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `reload` / `status` against a `Target` works. PM2, supervisord, runit,
|
|
9
9
|
* @absolutejs/runtime all fit if someone writes the adapter.
|
|
10
10
|
*/
|
|
11
|
-
import type { Target } from
|
|
11
|
+
import type { Target } from './targets';
|
|
12
12
|
export type ProcessManagerContext = {
|
|
13
13
|
/** Absolute path on the target to the active release dir (the symlink target). */
|
|
14
14
|
currentPath: string;
|
|
@@ -21,7 +21,7 @@ export type ProcessManagerContext = {
|
|
|
21
21
|
/** Optional env to set on the process. */
|
|
22
22
|
env: Record<string, string>;
|
|
23
23
|
/** Log sink for any commands the manager runs. */
|
|
24
|
-
onLog?: (line: string, stream:
|
|
24
|
+
onLog?: (line: string, stream: 'stdout' | 'stderr') => void;
|
|
25
25
|
};
|
|
26
26
|
export type ProcessManager = {
|
|
27
27
|
/** Bring the new release up. Called after the `current` symlink has been swapped. */
|
|
@@ -29,7 +29,7 @@ export type ProcessManager = {
|
|
|
29
29
|
/** Stop the running process. */
|
|
30
30
|
stop?: (target: Target, ctx: ProcessManagerContext) => Promise<void>;
|
|
31
31
|
/** Return current status (best-effort; used by callers for diagnostics). */
|
|
32
|
-
status?: (target: Target, ctx: ProcessManagerContext) => Promise<
|
|
32
|
+
status?: (target: Target, ctx: ProcessManagerContext) => Promise<'running' | 'stopped' | 'unknown'>;
|
|
33
33
|
};
|
|
34
34
|
export type BareManagerOptions = {
|
|
35
35
|
/** Command to run. Default `bun run start`. */
|
|
@@ -48,7 +48,7 @@ export type SystemdManagerOptions = {
|
|
|
48
48
|
/** Group. Default the deploy user. */
|
|
49
49
|
group?: string;
|
|
50
50
|
/** Restart policy. Default `always`. */
|
|
51
|
-
restart?:
|
|
51
|
+
restart?: 'always' | 'on-failure' | 'no';
|
|
52
52
|
/** systemctl path. Default `systemctl`. */
|
|
53
53
|
systemctl?: string;
|
|
54
54
|
/** Unit file directory. Default `/etc/systemd/system`. */
|
package/dist/route53.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* of an `@aws-sdk/client-route-53` dependency. Wire your existing
|
|
14
14
|
* AWS SDK client with a 4-line shim — see README.
|
|
15
15
|
*/
|
|
16
|
-
import type { DnsProvider } from
|
|
16
|
+
import type { DnsProvider } from './dns';
|
|
17
17
|
export type Route53ResourceRecord = {
|
|
18
18
|
Value: string;
|
|
19
19
|
};
|
|
@@ -30,7 +30,7 @@ export type Route53ResourceRecordSet = {
|
|
|
30
30
|
SetIdentifier?: string;
|
|
31
31
|
};
|
|
32
32
|
export type Route53Change = {
|
|
33
|
-
Action:
|
|
33
|
+
Action: 'CREATE' | 'DELETE' | 'UPSERT';
|
|
34
34
|
ResourceRecordSet: Route53ResourceRecordSet;
|
|
35
35
|
};
|
|
36
36
|
export type Route53ListInput = {
|
package/dist/route53.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/route53.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @absolutejs/deploy/route53 — AWS Route 53 DNS implementing the\n * {@link DnsProvider} contract from `./dns`.\n *\n * Different shape from Cloudflare/DO/Hetzner DNS: Route 53 has no\n * per-record id. Records are identified by their (Name, Type)\n * composite, and mutations are done via `ChangeResourceRecordSets`\n * with a batch of CREATE / UPSERT / DELETE actions. We exploit\n * Route 53's native `UPSERT` action — one API call replaces the\n * find-then-create-or-update dance the other providers need.\n *\n * Narrow `Route53ClientLike` interface so the package stays free\n * of an `@aws-sdk/client-route-53` dependency. Wire your existing\n * AWS SDK client with a 4-line shim — see README.\n */\n\nimport type {\n DnsProvider,\n DnsRecord,\n DnsRecordFilter,\n DnsRecordSpec,\n DnsRecordType,\n} from \"./dns\";\n\n// =============================================================================\n// AWS API shapes — narrowed to what we use\n// =============================================================================\n\nexport type Route53ResourceRecord = { Value: string };\n\nexport type Route53ResourceRecordSet = {\n Name: string;\n Type: string;\n TTL?: number;\n ResourceRecords?: Route53ResourceRecord[];\n AliasTarget?: {\n HostedZoneId: string;\n DNSName: string;\n EvaluateTargetHealth: boolean;\n };\n SetIdentifier?: string;\n};\n\nexport type Route53Change = {\n Action: \"CREATE\" | \"DELETE\" | \"UPSERT\";\n ResourceRecordSet: Route53ResourceRecordSet;\n};\n\nexport type Route53ListInput = {\n HostedZoneId: string;\n StartRecordName?: string;\n StartRecordType?: string;\n MaxItems?: string;\n};\n\nexport type Route53ListOutput = {\n ResourceRecordSets: Route53ResourceRecordSet[];\n IsTruncated?: boolean;\n NextRecordName?: string;\n NextRecordType?: string;\n};\n\nexport type Route53ChangeInput = {\n HostedZoneId: string;\n ChangeBatch: { Changes: Route53Change[]; Comment?: string };\n};\n\nexport type Route53ChangeOutput = {\n ChangeInfo: { Id: string; Status: string };\n};\n\n/**\n * Minimal subset of the Route 53 client we need. Wire your\n * `@aws-sdk/client-route-53` `Route53Client` with a shim:\n *\n * ```ts\n * import {\n * Route53Client,\n * ListResourceRecordSetsCommand,\n * ChangeResourceRecordSetsCommand,\n * } from '@aws-sdk/client-route-53';\n *\n * const aws = new Route53Client({ region: 'us-east-1' });\n * const client: Route53ClientLike = {\n * listResourceRecordSets: (input) =>\n * aws.send(new ListResourceRecordSetsCommand(input)) as any,\n * changeResourceRecordSets: (input) =>\n * aws.send(new ChangeResourceRecordSetsCommand(input)) as any,\n * };\n * ```\n *\n * Or hand-roll a SigV4-signed fetch client if you want zero deps.\n */\nexport type Route53ClientLike = {\n listResourceRecordSets: (\n input: Route53ListInput,\n ) => Promise<Route53ListOutput>;\n changeResourceRecordSets: (\n input: Route53ChangeInput,\n ) => Promise<Route53ChangeOutput>;\n};\n\nexport type Route53ProviderOptions = {\n client: Route53ClientLike;\n /** Hosted zone id, e.g. `'Z2FDTNDATAQYW2'`. */\n hostedZoneId: string;\n /** Zone name for log + name conversion, e.g. `'example.com'`. */\n zoneName: string;\n /** Default TTL when `spec.ttl` is omitted. Default 300. */\n defaultTtl?: number;\n};\n\n// =============================================================================\n// Name + TXT-value normalization\n// =============================================================================\n\nconst ensureFqdn = (name: string, zoneName: string): string => {\n if (name === \"@\") return ensureTrailingDot(zoneName);\n if (name === zoneName) return ensureTrailingDot(name);\n if (name.endsWith(\".\")) return name;\n return `${name}.`;\n};\n\nconst ensureTrailingDot = (s: string): string =>\n s.endsWith(\".\") ? s : `${s}.`;\n\nconst stripTrailingDot = (s: string): string =>\n s.endsWith(\".\") ? s.slice(0, -1) : s;\n\n/**\n * Route 53 TXT values MUST be enclosed in double quotes in the wire\n * format. Strings > 255 chars get split into multiple quoted chunks.\n * Bare unquoted input → wrap. Already-quoted input → pass through.\n */\nconst encodeTxtValue = (raw: string): string => {\n if (raw.startsWith('\"') && raw.endsWith('\"')) return raw;\n const CHUNK = 255;\n const chunks: string[] = [];\n for (let i = 0; i < raw.length; i += CHUNK) {\n chunks.push(`\"${raw.slice(i, i + CHUNK).replaceAll('\"', '\\\\\"')}\"`);\n }\n return chunks.join(\" \");\n};\n\n/** Inverse of encodeTxtValue: unwrap \" ... \" \" ... \" back to raw. */\nconst decodeTxtValue = (wire: string): string => {\n const parts = wire.match(/\"((?:\\\\.|[^\"])*)\"/g);\n if (parts === null) return wire;\n return parts.map((part) => part.slice(1, -1).replaceAll('\\\\\"', '\"')).join(\"\");\n};\n\n// =============================================================================\n// Synthetic-id encoding — Route 53 has no per-record id\n// =============================================================================\n\n/**\n * Route 53 deletes require the CURRENT record state (Name + Type +\n * TTL + Values), not an opaque id. We encode that state as a\n * base64-JSON synthetic id so the DnsProvider contract (which\n * expects `delete(id)`) works without a second API round-trip.\n */\ntype Route53IdPayload = {\n n: string; // Name (FQDN with trailing dot)\n t: string; // Type\n v: string[]; // Values (as written to ResourceRecords[])\n l?: number; // TTL\n};\n\nconst encodeId = (payload: Route53IdPayload): string =>\n btoa(JSON.stringify(payload))\n .replaceAll(\"+\", \"-\")\n .replaceAll(\"/\", \"_\")\n .replaceAll(\"=\", \"\");\n\nconst decodeId = (id: string): Route53IdPayload => {\n const padded = id\n .replaceAll(\"-\", \"+\")\n .replaceAll(\"_\", \"/\")\n .padEnd(Math.ceil(id.length / 4) * 4, \"=\");\n return JSON.parse(atob(padded)) as Route53IdPayload;\n};\n\n// =============================================================================\n// Conversion: AWS RecordSet ↔ DnsRecord\n// =============================================================================\n\nconst toDnsRecord = (\n set: Route53ResourceRecordSet,\n zoneName: string,\n): DnsRecord | undefined => {\n // Alias records (no ResourceRecords) don't fit our string-content model.\n const values = set.ResourceRecords?.map((r) => r.Value) ?? [];\n if (values.length === 0) return undefined;\n // For TXT, decode the wire format back to raw plaintext.\n const content =\n set.Type === \"TXT\"\n ? decodeTxtValue(values.join(\" \"))\n : (values[0] as string);\n const idPayload: Route53IdPayload = {\n n: set.Name,\n t: set.Type,\n v: values,\n ...(set.TTL !== undefined ? { l: set.TTL } : {}),\n };\n const fqdn = stripTrailingDot(set.Name);\n const name = fqdn === zoneName ? zoneName : fqdn;\n return {\n content,\n id: encodeId(idPayload),\n name,\n type: set.Type as DnsRecordType,\n ...(set.TTL !== undefined ? { ttl: set.TTL } : {}),\n };\n};\n\nconst buildRecordSet = (\n spec: DnsRecordSpec,\n zoneName: string,\n defaultTtl: number,\n): Route53ResourceRecordSet => {\n const Name = ensureFqdn(spec.name, zoneName);\n const ttl = spec.ttl ?? defaultTtl;\n const value =\n spec.type === \"TXT\" ? encodeTxtValue(spec.content) : spec.content;\n return {\n Name,\n ResourceRecords: [{ Value: value }],\n TTL: ttl,\n Type: spec.type,\n };\n};\n\n// =============================================================================\n// Provider factory\n// =============================================================================\n\nexport const route53DnsProvider = (\n options: Route53ProviderOptions,\n): DnsProvider => {\n const { client, hostedZoneId, zoneName } = options;\n const defaultTtl = options.defaultTtl ?? 300;\n const zoneLabel = zoneName;\n\n const fetchAll = async (filter?: DnsRecordFilter): Promise<DnsRecord[]> => {\n const out: DnsRecord[] = [];\n let startName: string | undefined;\n let startType: string | undefined;\n for (;;) {\n const params: Route53ListInput = {\n HostedZoneId: hostedZoneId,\n MaxItems: \"500\",\n ...(startName !== undefined ? { StartRecordName: startName } : {}),\n ...(startType !== undefined ? { StartRecordType: startType } : {}),\n };\n const page = await client.listResourceRecordSets(params);\n for (const set of page.ResourceRecordSets) {\n const dnsRecord = toDnsRecord(set, zoneName);\n if (dnsRecord === undefined) continue;\n if (filter?.name !== undefined && dnsRecord.name !== filter.name) {\n continue;\n }\n if (filter?.type !== undefined && dnsRecord.type !== filter.type) {\n continue;\n }\n out.push(dnsRecord);\n }\n if (page.IsTruncated !== true) break;\n startName = page.NextRecordName;\n startType = page.NextRecordType;\n }\n return out;\n };\n\n const list = (filter?: DnsRecordFilter): Promise<DnsRecord[]> =>\n fetchAll(filter);\n\n const find = async (key: {\n name: string;\n type: DnsRecordType;\n }): Promise<DnsRecord | undefined> => {\n const matches = await fetchAll(key);\n if (matches.length === 0) return undefined;\n if (matches.length > 1) {\n throw new Error(\n `[deploy/route53] multiple ${key.type} records for \"${key.name}\" in zone ${zoneLabel} — drifted state; resolve manually before upsert.`,\n );\n }\n return matches[0];\n };\n\n const submitChange = async (change: Route53Change): Promise<void> => {\n await client.changeResourceRecordSets({\n ChangeBatch: { Changes: [change] },\n HostedZoneId: hostedZoneId,\n });\n };\n\n const create = async (spec: DnsRecordSpec): Promise<DnsRecord> => {\n const set = buildRecordSet(spec, zoneName, defaultTtl);\n await submitChange({ Action: \"CREATE\", ResourceRecordSet: set });\n const synthesized = toDnsRecord(set, zoneName);\n if (synthesized === undefined) {\n throw new Error(\n \"[deploy/route53] internal: built record had no ResourceRecords\",\n );\n }\n return synthesized;\n };\n\n const upsert = async (spec: DnsRecordSpec): Promise<DnsRecord> => {\n const set = buildRecordSet(spec, zoneName, defaultTtl);\n await submitChange({ Action: \"UPSERT\", ResourceRecordSet: set });\n const synthesized = toDnsRecord(set, zoneName);\n if (synthesized === undefined) {\n throw new Error(\n \"[deploy/route53] internal: built record had no ResourceRecords\",\n );\n }\n return synthesized;\n };\n\n const update = async (_id: string, spec: DnsRecordSpec): Promise<DnsRecord> =>\n upsert(spec);\n\n const deleteRecord = async (id: string): Promise<void> => {\n // Route 53's DELETE requires the exact current state. We reconstruct\n // it from the synthetic id we issued at read/write time.\n const payload = decodeId(id);\n const set: Route53ResourceRecordSet = {\n Name: payload.n,\n ResourceRecords: payload.v.map((Value) => ({ Value })),\n Type: payload.t,\n ...(payload.l !== undefined ? { TTL: payload.l } : {}),\n };\n try {\n await submitChange({ Action: \"DELETE\", ResourceRecordSet: set });\n } catch (error) {\n // Route 53 throws InvalidChangeBatch when the record doesn't exist\n // (or its state has drifted from what we encoded). Treat the\n // \"doesn't exist\" subset as idempotent success.\n const message = error instanceof Error ? error.message : String(error);\n if (\n message.includes(\"not found\") ||\n message.includes(\"NoSuchHostedZone\") ||\n message.includes(\"but it was not found\")\n ) {\n return;\n }\n throw error;\n }\n };\n\n return {\n create,\n delete: deleteRecord,\n description: `route53 zone \"${zoneLabel}\"`,\n find,\n list,\n update,\n upsert,\n };\n};\n"
|
|
5
|
+
"/**\n * @absolutejs/deploy/route53 — AWS Route 53 DNS implementing the\n * {@link DnsProvider} contract from `./dns`.\n *\n * Different shape from Cloudflare/DO/Hetzner DNS: Route 53 has no\n * per-record id. Records are identified by their (Name, Type)\n * composite, and mutations are done via `ChangeResourceRecordSets`\n * with a batch of CREATE / UPSERT / DELETE actions. We exploit\n * Route 53's native `UPSERT` action — one API call replaces the\n * find-then-create-or-update dance the other providers need.\n *\n * Narrow `Route53ClientLike` interface so the package stays free\n * of an `@aws-sdk/client-route-53` dependency. Wire your existing\n * AWS SDK client with a 4-line shim — see README.\n */\n\nimport type {\n\tDnsProvider,\n\tDnsRecord,\n\tDnsRecordFilter,\n\tDnsRecordSpec,\n\tDnsRecordType\n} from './dns';\n\n// =============================================================================\n// AWS API shapes — narrowed to what we use\n// =============================================================================\n\nexport type Route53ResourceRecord = { Value: string };\n\nexport type Route53ResourceRecordSet = {\n\tName: string;\n\tType: string;\n\tTTL?: number;\n\tResourceRecords?: Route53ResourceRecord[];\n\tAliasTarget?: {\n\t\tHostedZoneId: string;\n\t\tDNSName: string;\n\t\tEvaluateTargetHealth: boolean;\n\t};\n\tSetIdentifier?: string;\n};\n\nexport type Route53Change = {\n\tAction: 'CREATE' | 'DELETE' | 'UPSERT';\n\tResourceRecordSet: Route53ResourceRecordSet;\n};\n\nexport type Route53ListInput = {\n\tHostedZoneId: string;\n\tStartRecordName?: string;\n\tStartRecordType?: string;\n\tMaxItems?: string;\n};\n\nexport type Route53ListOutput = {\n\tResourceRecordSets: Route53ResourceRecordSet[];\n\tIsTruncated?: boolean;\n\tNextRecordName?: string;\n\tNextRecordType?: string;\n};\n\nexport type Route53ChangeInput = {\n\tHostedZoneId: string;\n\tChangeBatch: { Changes: Route53Change[]; Comment?: string };\n};\n\nexport type Route53ChangeOutput = {\n\tChangeInfo: { Id: string; Status: string };\n};\n\n/**\n * Minimal subset of the Route 53 client we need. Wire your\n * `@aws-sdk/client-route-53` `Route53Client` with a shim:\n *\n * ```ts\n * import {\n * Route53Client,\n * ListResourceRecordSetsCommand,\n * ChangeResourceRecordSetsCommand,\n * } from '@aws-sdk/client-route-53';\n *\n * const aws = new Route53Client({ region: 'us-east-1' });\n * const client: Route53ClientLike = {\n * listResourceRecordSets: (input) =>\n * aws.send(new ListResourceRecordSetsCommand(input)) as any,\n * changeResourceRecordSets: (input) =>\n * aws.send(new ChangeResourceRecordSetsCommand(input)) as any,\n * };\n * ```\n *\n * Or hand-roll a SigV4-signed fetch client if you want zero deps.\n */\nexport type Route53ClientLike = {\n\tlistResourceRecordSets: (\n\t\tinput: Route53ListInput\n\t) => Promise<Route53ListOutput>;\n\tchangeResourceRecordSets: (\n\t\tinput: Route53ChangeInput\n\t) => Promise<Route53ChangeOutput>;\n};\n\nexport type Route53ProviderOptions = {\n\tclient: Route53ClientLike;\n\t/** Hosted zone id, e.g. `'Z2FDTNDATAQYW2'`. */\n\thostedZoneId: string;\n\t/** Zone name for log + name conversion, e.g. `'example.com'`. */\n\tzoneName: string;\n\t/** Default TTL when `spec.ttl` is omitted. Default 300. */\n\tdefaultTtl?: number;\n};\n\n// =============================================================================\n// Name + TXT-value normalization\n// =============================================================================\n\nconst ensureFqdn = (name: string, zoneName: string): string => {\n\tif (name === '@') return ensureTrailingDot(zoneName);\n\tif (name === zoneName) return ensureTrailingDot(name);\n\tif (name.endsWith('.')) return name;\n\treturn `${name}.`;\n};\n\nconst ensureTrailingDot = (s: string): string => (s.endsWith('.') ? s : `${s}.`);\n\nconst stripTrailingDot = (s: string): string =>\n\ts.endsWith('.') ? s.slice(0, -1) : s;\n\n/**\n * Route 53 TXT values MUST be enclosed in double quotes in the wire\n * format. Strings > 255 chars get split into multiple quoted chunks.\n * Bare unquoted input → wrap. Already-quoted input → pass through.\n */\nconst encodeTxtValue = (raw: string): string => {\n\tif (raw.startsWith('\"') && raw.endsWith('\"')) return raw;\n\tconst CHUNK = 255;\n\tconst chunks: string[] = [];\n\tfor (let i = 0; i < raw.length; i += CHUNK) {\n\t\tchunks.push(`\"${raw.slice(i, i + CHUNK).replaceAll('\"', '\\\\\"')}\"`);\n\t}\n\treturn chunks.join(' ');\n};\n\n/** Inverse of encodeTxtValue: unwrap \" ... \" \" ... \" back to raw. */\nconst decodeTxtValue = (wire: string): string => {\n\tconst parts = wire.match(/\"((?:\\\\.|[^\"])*)\"/g);\n\tif (parts === null) return wire;\n\treturn parts\n\t\t.map((part) => part.slice(1, -1).replaceAll('\\\\\"', '\"'))\n\t\t.join('');\n};\n\n// =============================================================================\n// Synthetic-id encoding — Route 53 has no per-record id\n// =============================================================================\n\n/**\n * Route 53 deletes require the CURRENT record state (Name + Type +\n * TTL + Values), not an opaque id. We encode that state as a\n * base64-JSON synthetic id so the DnsProvider contract (which\n * expects `delete(id)`) works without a second API round-trip.\n */\ntype Route53IdPayload = {\n\tn: string; // Name (FQDN with trailing dot)\n\tt: string; // Type\n\tv: string[]; // Values (as written to ResourceRecords[])\n\tl?: number; // TTL\n};\n\nconst encodeId = (payload: Route53IdPayload): string =>\n\tbtoa(JSON.stringify(payload))\n\t\t.replaceAll('+', '-')\n\t\t.replaceAll('/', '_')\n\t\t.replaceAll('=', '');\n\nconst decodeId = (id: string): Route53IdPayload => {\n\tconst padded = id\n\t\t.replaceAll('-', '+')\n\t\t.replaceAll('_', '/')\n\t\t.padEnd(Math.ceil(id.length / 4) * 4, '=');\n\treturn JSON.parse(atob(padded)) as Route53IdPayload;\n};\n\n// =============================================================================\n// Conversion: AWS RecordSet ↔ DnsRecord\n// =============================================================================\n\nconst toDnsRecord = (\n\tset: Route53ResourceRecordSet,\n\tzoneName: string\n): DnsRecord | undefined => {\n\t// Alias records (no ResourceRecords) don't fit our string-content model.\n\tconst values = set.ResourceRecords?.map((r) => r.Value) ?? [];\n\tif (values.length === 0) return undefined;\n\t// For TXT, decode the wire format back to raw plaintext.\n\tconst content =\n\t\tset.Type === 'TXT' ? decodeTxtValue(values.join(' ')) : (values[0] as string);\n\tconst idPayload: Route53IdPayload = {\n\t\tn: set.Name,\n\t\tt: set.Type,\n\t\tv: values,\n\t\t...(set.TTL !== undefined ? { l: set.TTL } : {})\n\t};\n\tconst fqdn = stripTrailingDot(set.Name);\n\tconst name = fqdn === zoneName ? zoneName : fqdn;\n\treturn {\n\t\tcontent,\n\t\tid: encodeId(idPayload),\n\t\tname,\n\t\ttype: set.Type as DnsRecordType,\n\t\t...(set.TTL !== undefined ? { ttl: set.TTL } : {})\n\t};\n};\n\nconst buildRecordSet = (\n\tspec: DnsRecordSpec,\n\tzoneName: string,\n\tdefaultTtl: number\n): Route53ResourceRecordSet => {\n\tconst Name = ensureFqdn(spec.name, zoneName);\n\tconst ttl = spec.ttl ?? defaultTtl;\n\tconst value = spec.type === 'TXT' ? encodeTxtValue(spec.content) : spec.content;\n\treturn {\n\t\tName,\n\t\tResourceRecords: [{ Value: value }],\n\t\tTTL: ttl,\n\t\tType: spec.type\n\t};\n};\n\n// =============================================================================\n// Provider factory\n// =============================================================================\n\nexport const route53DnsProvider = (\n\toptions: Route53ProviderOptions\n): DnsProvider => {\n\tconst { client, hostedZoneId, zoneName } = options;\n\tconst defaultTtl = options.defaultTtl ?? 300;\n\tconst zoneLabel = zoneName;\n\n\tconst fetchAll = async (\n\t\tfilter?: DnsRecordFilter\n\t): Promise<DnsRecord[]> => {\n\t\tconst out: DnsRecord[] = [];\n\t\tlet startName: string | undefined;\n\t\tlet startType: string | undefined;\n\t\tfor (;;) {\n\t\t\tconst params: Route53ListInput = {\n\t\t\t\tHostedZoneId: hostedZoneId,\n\t\t\t\tMaxItems: '500',\n\t\t\t\t...(startName !== undefined ? { StartRecordName: startName } : {}),\n\t\t\t\t...(startType !== undefined ? { StartRecordType: startType } : {})\n\t\t\t};\n\t\t\tconst page = await client.listResourceRecordSets(params);\n\t\t\tfor (const set of page.ResourceRecordSets) {\n\t\t\t\tconst dnsRecord = toDnsRecord(set, zoneName);\n\t\t\t\tif (dnsRecord === undefined) continue;\n\t\t\t\tif (\n\t\t\t\t\tfilter?.name !== undefined &&\n\t\t\t\t\tdnsRecord.name !== filter.name\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\tfilter?.type !== undefined &&\n\t\t\t\t\tdnsRecord.type !== filter.type\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tout.push(dnsRecord);\n\t\t\t}\n\t\t\tif (page.IsTruncated !== true) break;\n\t\t\tstartName = page.NextRecordName;\n\t\t\tstartType = page.NextRecordType;\n\t\t}\n\t\treturn out;\n\t};\n\n\tconst list = (filter?: DnsRecordFilter): Promise<DnsRecord[]> =>\n\t\tfetchAll(filter);\n\n\tconst find = async (key: {\n\t\tname: string;\n\t\ttype: DnsRecordType;\n\t}): Promise<DnsRecord | undefined> => {\n\t\tconst matches = await fetchAll(key);\n\t\tif (matches.length === 0) return undefined;\n\t\tif (matches.length > 1) {\n\t\t\tthrow new Error(\n\t\t\t\t`[deploy/route53] multiple ${key.type} records for \"${key.name}\" in zone ${zoneLabel} — drifted state; resolve manually before upsert.`\n\t\t\t);\n\t\t}\n\t\treturn matches[0];\n\t};\n\n\tconst submitChange = async (change: Route53Change): Promise<void> => {\n\t\tawait client.changeResourceRecordSets({\n\t\t\tChangeBatch: { Changes: [change] },\n\t\t\tHostedZoneId: hostedZoneId\n\t\t});\n\t};\n\n\tconst create = async (spec: DnsRecordSpec): Promise<DnsRecord> => {\n\t\tconst set = buildRecordSet(spec, zoneName, defaultTtl);\n\t\tawait submitChange({ Action: 'CREATE', ResourceRecordSet: set });\n\t\tconst synthesized = toDnsRecord(set, zoneName);\n\t\tif (synthesized === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t'[deploy/route53] internal: built record had no ResourceRecords'\n\t\t\t);\n\t\t}\n\t\treturn synthesized;\n\t};\n\n\tconst upsert = async (spec: DnsRecordSpec): Promise<DnsRecord> => {\n\t\tconst set = buildRecordSet(spec, zoneName, defaultTtl);\n\t\tawait submitChange({ Action: 'UPSERT', ResourceRecordSet: set });\n\t\tconst synthesized = toDnsRecord(set, zoneName);\n\t\tif (synthesized === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t'[deploy/route53] internal: built record had no ResourceRecords'\n\t\t\t);\n\t\t}\n\t\treturn synthesized;\n\t};\n\n\tconst update = async (\n\t\t_id: string,\n\t\tspec: DnsRecordSpec\n\t): Promise<DnsRecord> => upsert(spec);\n\n\tconst deleteRecord = async (id: string): Promise<void> => {\n\t\t// Route 53's DELETE requires the exact current state. We reconstruct\n\t\t// it from the synthetic id we issued at read/write time.\n\t\tconst payload = decodeId(id);\n\t\tconst set: Route53ResourceRecordSet = {\n\t\t\tName: payload.n,\n\t\t\tResourceRecords: payload.v.map((Value) => ({ Value })),\n\t\t\tType: payload.t,\n\t\t\t...(payload.l !== undefined ? { TTL: payload.l } : {})\n\t\t};\n\t\ttry {\n\t\t\tawait submitChange({ Action: 'DELETE', ResourceRecordSet: set });\n\t\t} catch (error) {\n\t\t\t// Route 53 throws InvalidChangeBatch when the record doesn't exist\n\t\t\t// (or its state has drifted from what we encoded). Treat the\n\t\t\t// \"doesn't exist\" subset as idempotent success.\n\t\t\tconst message =\n\t\t\t\terror instanceof Error ? error.message : String(error);\n\t\t\tif (\n\t\t\t\tmessage.includes('not found') ||\n\t\t\t\tmessage.includes('NoSuchHostedZone') ||\n\t\t\t\tmessage.includes(\n\t\t\t\t\t'but it was not found'\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t};\n\n\treturn {\n\t\tcreate,\n\t\tdelete: deleteRecord,\n\t\tdescription: `route53 zone \"${zoneLabel}\"`,\n\t\tfind,\n\t\tlist,\n\t\tupdate,\n\t\tupsert\n\t};\n};\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoHA,IAAM,aAAa,CAAC,MAAc,aAA6B;AAAA,
|
|
7
|
+
"mappings": "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",
|
|
8
8
|
"debugId": "9560FABB96092BEC64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/targets.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type ExecOptions = {
|
|
|
28
28
|
/** Hard kill after this many ms. Default 600_000 (10 min). 0 disables. */
|
|
29
29
|
timeoutMs?: number;
|
|
30
30
|
/** Pipe stdout/stderr through here as it streams (lines, newline-stripped). */
|
|
31
|
-
onLog?: (line: string, stream:
|
|
31
|
+
onLog?: (line: string, stream: 'stdout' | 'stderr') => void;
|
|
32
32
|
/** Stdin payload — a string is written verbatim. */
|
|
33
33
|
stdin?: string;
|
|
34
34
|
};
|
package/dist/tls.d.ts
CHANGED
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
* (reuse the same account across cert renewals — cheaper, doesn't
|
|
21
21
|
* hit Let's Encrypt's account-creation rate limit)
|
|
22
22
|
*/
|
|
23
|
-
import type { Target } from
|
|
24
|
-
import type { DnsProvider } from
|
|
23
|
+
import type { Target } from './targets';
|
|
24
|
+
import type { DnsProvider } from './dns';
|
|
25
25
|
export declare const LETSENCRYPT_PRODUCTION = "https://acme-v02.api.letsencrypt.org/directory";
|
|
26
26
|
export declare const LETSENCRYPT_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory";
|
|
27
27
|
type JwsHeader = {
|
|
28
|
-
alg:
|
|
28
|
+
alg: 'ES256';
|
|
29
29
|
nonce: string;
|
|
30
30
|
url: string;
|
|
31
31
|
jwk?: JsonWebKey;
|
|
@@ -191,10 +191,10 @@ export type RenewCertificateOptions = IssueCertificateOptions & {
|
|
|
191
191
|
export type RenewalResult = {
|
|
192
192
|
renewed: true;
|
|
193
193
|
certificate: IssuedCertificate;
|
|
194
|
-
reason:
|
|
194
|
+
reason: 'forced' | 'no-current-cert' | 'expiring-soon';
|
|
195
195
|
} | {
|
|
196
196
|
renewed: false;
|
|
197
|
-
reason:
|
|
197
|
+
reason: 'still-fresh';
|
|
198
198
|
inspection: CertificateInspection;
|
|
199
199
|
};
|
|
200
200
|
/**
|
package/dist/tls.js
CHANGED
|
@@ -219,10 +219,7 @@ var buildExtensionRequestAttribute = (domains) => {
|
|
|
219
219
|
derSet([extensions])
|
|
220
220
|
]);
|
|
221
221
|
};
|
|
222
|
-
var generateCertKeyPair = async () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
|
|
223
|
-
"sign",
|
|
224
|
-
"verify"
|
|
225
|
-
]);
|
|
222
|
+
var generateCertKeyPair = async () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
|
|
226
223
|
var buildCsr = async (domains, keypair) => {
|
|
227
224
|
if (domains.length === 0) {
|
|
228
225
|
throw new Error("[deploy/tls] CSR requires at least one domain");
|
|
@@ -568,5 +565,5 @@ export {
|
|
|
568
565
|
AcmeError
|
|
569
566
|
};
|
|
570
567
|
|
|
571
|
-
//# debugId=
|
|
568
|
+
//# debugId=6D2588C0F93BA9DA64756E2164756E21
|
|
572
569
|
//# sourceMappingURL=tls.js.map
|
package/dist/tls.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/tls.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * @absolutejs/deploy/tls — ACME (RFC 8555) client for Let's Encrypt and\n * compatible CAs. DNS-01 challenges only — uses the {@link DnsProvider}\n * abstraction from `./dns`, so the same Cloudflare / Route 53 /\n * Hetzner-DNS providers that point hostnames at boxes also satisfy\n * ACME's challenge requirement.\n *\n * Zero peer deps. Bun's `crypto.subtle` covers JWS signing and ECDSA\n * key generation; a small DER encoder builds the CSR.\n *\n * Public API:\n *\n * issueCertificate({ domains, dnsProvider, email })\n * → { certificatePem, privateKeyPem, account, domains }\n *\n * installCertificateOnTarget(target, cert, { certPath, keyPath, reload? })\n * → uploads PEM files via Target.upload + optional reload exec\n *\n * exportAccountKey / importAccountKey for persistence between runs\n * (reuse the same account across cert renewals — cheaper, doesn't\n * hit Let's Encrypt's account-creation rate limit)\n */\n\nimport { X509Certificate } from \"node:crypto\";\nimport type { Target } from \"./targets\";\nimport type { DnsProvider } from \"./dns\";\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nexport const LETSENCRYPT_PRODUCTION =\n \"https://acme-v02.api.letsencrypt.org/directory\";\nexport const LETSENCRYPT_STAGING =\n \"https://acme-staging-v02.api.letsencrypt.org/directory\";\n\n// =============================================================================\n// base64url\n// =============================================================================\n\nconst base64UrlEncode = (bytes: Uint8Array | ArrayBuffer): string => {\n const buf = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n let binary = \"\";\n for (const byte of buf) binary += String.fromCharCode(byte);\n return btoa(binary)\n .replaceAll(\"+\", \"-\")\n .replaceAll(\"/\", \"_\")\n .replaceAll(\"=\", \"\");\n};\n\nconst base64UrlEncodeString = (value: string): string =>\n base64UrlEncode(new TextEncoder().encode(value));\n\nconst base64UrlDecode = (value: string): Uint8Array => {\n const padded = value\n .replaceAll(\"-\", \"+\")\n .replaceAll(\"_\", \"/\")\n .padEnd(Math.ceil(value.length / 4) * 4, \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n};\n\n// =============================================================================\n// DER encoding (minimal subset for CSR)\n// =============================================================================\n\nconst derLength = (length: number): Uint8Array => {\n if (length < 0x80) return Uint8Array.from([length]);\n const bytes: number[] = [];\n let remaining = length;\n while (remaining > 0) {\n bytes.unshift(remaining & 0xff);\n remaining >>= 8;\n }\n return Uint8Array.from([0x80 | bytes.length, ...bytes]);\n};\n\nconst derTag = (tag: number, payload: Uint8Array): Uint8Array => {\n const length = derLength(payload.length);\n const out = new Uint8Array(1 + length.length + payload.length);\n out[0] = tag;\n out.set(length, 1);\n out.set(payload, 1 + length.length);\n return out;\n};\n\nconst derSeq = (children: Uint8Array[]): Uint8Array => {\n const total = children.reduce((sum, child) => sum + child.length, 0);\n const payload = new Uint8Array(total);\n let offset = 0;\n for (const child of children) {\n payload.set(child, offset);\n offset += child.length;\n }\n return derTag(0x30, payload);\n};\n\nconst derSet = (children: Uint8Array[]): Uint8Array => {\n const total = children.reduce((sum, child) => sum + child.length, 0);\n const payload = new Uint8Array(total);\n let offset = 0;\n for (const child of children) {\n payload.set(child, offset);\n offset += child.length;\n }\n return derTag(0x31, payload);\n};\n\nconst derInt = (value: number | Uint8Array): Uint8Array => {\n if (typeof value === \"number\") {\n // Small unsigned int — used for the CSR version field (0).\n if (value === 0) return derTag(0x02, Uint8Array.from([0]));\n const bytes: number[] = [];\n let remaining = value;\n while (remaining > 0) {\n bytes.unshift(remaining & 0xff);\n remaining >>= 8;\n }\n if ((bytes[0] as number) & 0x80) bytes.unshift(0); // ensure positive\n return derTag(0x02, Uint8Array.from(bytes));\n }\n // Big-endian unsigned bytes — strip leading zeros, then prepend 0x00 if\n // the high bit is set (DER INTEGER is signed two's complement).\n let start = 0;\n while (start < value.length - 1 && value[start] === 0) start += 1;\n let payload = value.subarray(start);\n if ((payload[0] as number) & 0x80) {\n const padded = new Uint8Array(payload.length + 1);\n padded.set(payload, 1);\n payload = padded;\n }\n return derTag(0x02, payload);\n};\n\nconst derOid = (oid: string): Uint8Array => {\n const parts = oid.split(\".\").map((part) => Number.parseInt(part, 10));\n const first = parts[0];\n const second = parts[1];\n if (first === undefined || second === undefined) {\n throw new Error(`[deploy/tls] invalid OID: ${oid}`);\n }\n const bytes: number[] = [first * 40 + second];\n for (let index = 2; index < parts.length; index += 1) {\n const value = parts[index] as number;\n const chunks: number[] = [];\n let remaining = value;\n do {\n chunks.unshift(remaining & 0x7f);\n remaining >>= 7;\n } while (remaining > 0);\n for (let chunkIndex = 0; chunkIndex < chunks.length - 1; chunkIndex += 1) {\n chunks[chunkIndex] = (chunks[chunkIndex] as number) | 0x80;\n }\n bytes.push(...chunks);\n }\n return derTag(0x06, Uint8Array.from(bytes));\n};\n\nconst derPrintableString = (value: string): Uint8Array =>\n derTag(0x13, new TextEncoder().encode(value));\n\nconst derUtf8String = (value: string): Uint8Array =>\n derTag(0x0c, new TextEncoder().encode(value));\n\nconst derIa5String = (value: string): Uint8Array =>\n derTag(0x16, new TextEncoder().encode(value));\n\nconst derOctetString = (payload: Uint8Array): Uint8Array =>\n derTag(0x04, payload);\n\nconst derBitString = (payload: Uint8Array): Uint8Array => {\n // 0x00 prefix = number of unused bits in the final byte; for byte-aligned\n // signatures + keys this is always zero.\n const bits = new Uint8Array(payload.length + 1);\n bits[0] = 0;\n bits.set(payload, 1);\n return derTag(0x03, bits);\n};\n\nconst derContextTag = (\n tag: number,\n payload: Uint8Array,\n constructed = true,\n): Uint8Array => derTag(0xa0 + tag + (constructed ? 0 : -0x20), payload);\n\n// =============================================================================\n// ECDSA signature: raw r||s ↔ DER SEQUENCE\n// =============================================================================\n\nconst ecdsaRawToDer = (rawSig: Uint8Array): Uint8Array => {\n const half = rawSig.length / 2;\n const r = rawSig.subarray(0, half);\n const s = rawSig.subarray(half);\n return derSeq([derInt(r), derInt(s)]);\n};\n\n// =============================================================================\n// JWS (RFC 7515) Flattened JSON Serialization for ACME\n// =============================================================================\n\ntype JwsHeader = {\n alg: \"ES256\";\n nonce: string;\n url: string;\n jwk?: JsonWebKey;\n kid?: string;\n};\n\ntype JwsBody = {\n protected: string;\n payload: string;\n signature: string;\n};\n\nconst signJws = async (\n privateKey: CryptoKey,\n header: JwsHeader,\n payload: object | string,\n): Promise<JwsBody> => {\n const protectedHeader = base64UrlEncodeString(JSON.stringify(header));\n const payloadEncoded =\n payload === \"\"\n ? \"\" // POST-as-GET: empty string, NOT empty object\n : base64UrlEncodeString(JSON.stringify(payload));\n const signingInput = new TextEncoder().encode(\n `${protectedHeader}.${payloadEncoded}`,\n );\n const rawSig = new Uint8Array(\n await crypto.subtle.sign(\n { hash: \"SHA-256\", name: \"ECDSA\" },\n privateKey,\n signingInput,\n ),\n );\n return {\n payload: payloadEncoded,\n protected: protectedHeader,\n signature: base64UrlEncode(rawSig),\n };\n};\n\n// =============================================================================\n// JWK helpers — canonical thumbprint, public-key extraction\n// =============================================================================\n\nconst exportPublicJwk = async (publicKey: CryptoKey): Promise<JsonWebKey> => {\n const jwk = await crypto.subtle.exportKey(\"jwk\", publicKey);\n // Strip private fields if the export included them (shouldn't, on a\n // public key, but be defensive).\n return {\n crv: jwk.crv,\n kty: jwk.kty,\n x: jwk.x,\n y: jwk.y,\n };\n};\n\nconst jwkThumbprint = async (publicJwk: JsonWebKey): Promise<string> => {\n // RFC 7638 — canonical JSON: required fields only, lex order.\n const canonical = JSON.stringify({\n crv: publicJwk.crv,\n kty: publicJwk.kty,\n x: publicJwk.x,\n y: publicJwk.y,\n });\n const hash = await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(canonical),\n );\n return base64UrlEncode(hash);\n};\n\n// =============================================================================\n// Account key — generation + JSON export/import for persistence\n// =============================================================================\n\nexport type AcmeAccount = {\n key: CryptoKeyPair;\n /** Account URL — set after first registration; persisted for renewals. */\n kid?: string;\n};\n\nexport type AcmeAccountJson = {\n publicJwk: JsonWebKey;\n privateJwk: JsonWebKey;\n kid?: string;\n};\n\nexport const generateAccountKey = async (): Promise<AcmeAccount> => {\n const key = await crypto.subtle.generateKey(\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n true,\n [\"sign\", \"verify\"],\n );\n return { key };\n};\n\nexport const exportAccount = async (\n account: AcmeAccount,\n): Promise<AcmeAccountJson> => {\n const publicJwk = await crypto.subtle.exportKey(\"jwk\", account.key.publicKey);\n const privateJwk = await crypto.subtle.exportKey(\n \"jwk\",\n account.key.privateKey,\n );\n return {\n privateJwk,\n publicJwk,\n ...(account.kid !== undefined ? { kid: account.kid } : {}),\n };\n};\n\nexport const importAccount = async (\n json: AcmeAccountJson,\n): Promise<AcmeAccount> => {\n const publicKey = await crypto.subtle.importKey(\n \"jwk\",\n json.publicJwk,\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n true,\n [\"verify\"],\n );\n const privateKey = await crypto.subtle.importKey(\n \"jwk\",\n json.privateJwk,\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n true,\n [\"sign\"],\n );\n return {\n key: { privateKey, publicKey },\n ...(json.kid !== undefined ? { kid: json.kid } : {}),\n };\n};\n\n// =============================================================================\n// CSR — Certificate Signing Request\n// =============================================================================\n\nconst buildSanExtension = (domains: string[]): Uint8Array => {\n // SubjectAltName extension value: SEQUENCE OF GeneralName\n const generalNames = domains.map((domain) =>\n derTag(0x82, new TextEncoder().encode(domain)),\n );\n const sanSequence = derSeq(generalNames);\n const extensionValue = derOctetString(sanSequence);\n return derSeq([derOid(\"2.5.29.17\"), extensionValue]);\n};\n\nconst buildExtensionRequestAttribute = (domains: string[]): Uint8Array => {\n const extensions = derSeq([buildSanExtension(domains)]);\n return derSeq([\n derOid(\"1.2.840.113549.1.9.14\"), // PKCS#9 extensionRequest\n derSet([extensions]),\n ]);\n};\n\nconst generateCertKeyPair = async (): Promise<CryptoKeyPair> =>\n crypto.subtle.generateKey({ name: \"ECDSA\", namedCurve: \"P-256\" }, true, [\n \"sign\",\n \"verify\",\n ]);\n\nconst buildCsr = async (\n domains: string[],\n keypair: CryptoKeyPair,\n): Promise<Uint8Array> => {\n if (domains.length === 0) {\n throw new Error(\"[deploy/tls] CSR requires at least one domain\");\n }\n const commonName = domains[0] as string;\n\n const spkiDer = new Uint8Array(\n await crypto.subtle.exportKey(\"spki\", keypair.publicKey),\n );\n\n const version = derInt(0);\n const subject = derSeq([\n derSet([\n derSeq([\n derOid(\"2.5.4.3\"), // commonName\n derUtf8String(commonName),\n ]),\n ]),\n ]);\n\n const attributes = derContextTag(0, buildExtensionRequestAttribute(domains));\n\n const certificationRequestInfo = derSeq([\n version,\n subject,\n spkiDer,\n attributes,\n ]);\n\n const rawSig = new Uint8Array(\n await crypto.subtle.sign(\n { hash: \"SHA-256\", name: \"ECDSA\" },\n keypair.privateKey,\n certificationRequestInfo as BufferSource,\n ),\n );\n const sigDer = ecdsaRawToDer(rawSig);\n\n const sigAlgorithm = derSeq([derOid(\"1.2.840.10045.4.3.2\")]);\n // ecdsa-with-SHA256\n const signatureBits = derBitString(sigDer);\n\n return derSeq([certificationRequestInfo, sigAlgorithm, signatureBits]);\n};\n\n// =============================================================================\n// PEM\n// =============================================================================\n\nconst derToPem = (label: string, der: Uint8Array): string => {\n const b64 = btoa(String.fromCharCode(...der));\n const lines = b64.match(/.{1,64}/g) ?? [];\n return `-----BEGIN ${label}-----\\n${lines.join(\"\\n\")}\\n-----END ${label}-----\\n`;\n};\n\nconst exportEcPrivateKeyPem = async (\n keypair: CryptoKeyPair,\n): Promise<string> => {\n const pkcs8 = new Uint8Array(\n await crypto.subtle.exportKey(\"pkcs8\", keypair.privateKey),\n );\n return derToPem(\"PRIVATE KEY\", pkcs8);\n};\n\n// =============================================================================\n// ACME client\n// =============================================================================\n\ntype AcmeDirectory = {\n newNonce: string;\n newAccount: string;\n newOrder: string;\n};\n\ntype AcmeOrder = {\n status: string;\n identifiers: Array<{ type: string; value: string }>;\n authorizations: string[];\n finalize: string;\n certificate?: string;\n};\n\ntype AcmeAuthorization = {\n status: string;\n identifier: { type: string; value: string };\n challenges: Array<{\n type: string;\n status: string;\n url: string;\n token: string;\n }>;\n};\n\nexport type IssueCertificateOptions = {\n /** Domain(s) to include. First is the CN; all are SANs. */\n domains: string[];\n /** DNS provider used for DNS-01 challenges (must own the zone). */\n dnsProvider: DnsProvider;\n /**\n * Map the public ACME challenge name to the record name written through\n * `dnsProvider`. Use this for CNAME-delegated DNS-01, where customers point\n * `_acme-challenge.app.example.com` at a platform-owned validation zone.\n * The propagation checker still receives the public challenge name because\n * that is what the certificate authority resolves.\n */\n mapDnsChallengeRecord?: (context: {\n domain: string;\n recordName: string;\n }) => string;\n /** Contact email for the ACME account. */\n email: string;\n /** ACME directory URL. Default Let's Encrypt production. */\n directoryUrl?: string;\n /** Reuse an existing account. Pass `exportAccount`'s output via `importAccount`. */\n account?: AcmeAccount;\n /** Override fetch (tests). Default global fetch. */\n fetch?: typeof fetch;\n /** Poll interval. Default 3 s. */\n pollIntervalMs?: number;\n /**\n * Max wait before notifying ACME that the DNS challenge is ready.\n * Set ~30-60s for Cloudflare; longer for slower providers. Default 30 s.\n */\n dnsPropagationDelayMs?: number;\n /** Max wait for the order to become valid. Default 5 min. */\n orderTimeoutMs?: number;\n /** Status log lines. */\n onLog?: (line: string) => void;\n /** Override sleep (tests). */\n sleep?: (ms: number) => Promise<void>;\n /**\n * Optional pre-check: returns true when DNS has propagated globally.\n * Default: just wait `dnsPropagationDelayMs` then proceed. Override\n * for production to actually verify (e.g. resolve from multiple\n * public resolvers).\n */\n checkDnsPropagated?: (\n recordName: string,\n expectedValue: string,\n ) => Promise<boolean>;\n};\n\nexport type IssuedCertificate = {\n certificatePem: string;\n privateKeyPem: string;\n account: AcmeAccount;\n domains: string[];\n};\n\nexport class AcmeError extends Error {\n readonly status: number;\n readonly body: unknown;\n constructor(message: string, status: number, body: unknown) {\n super(message);\n this.name = \"AcmeError\";\n this.status = status;\n this.body = body;\n }\n}\n\nconst defaultSleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\nexport const issueCertificate = async (\n options: IssueCertificateOptions,\n): Promise<IssuedCertificate> => {\n const log = options.onLog ?? (() => {});\n const sleep = options.sleep ?? defaultSleep;\n const fetcher = options.fetch ?? fetch;\n const directoryUrl = options.directoryUrl ?? LETSENCRYPT_PRODUCTION;\n const pollMs = options.pollIntervalMs ?? 3_000;\n const dnsDelay = options.dnsPropagationDelayMs ?? 30_000;\n const orderTimeout = options.orderTimeoutMs ?? 5 * 60_000;\n const account = options.account ?? (await generateAccountKey());\n\n if (options.domains.length === 0) {\n throw new Error(\"[deploy/tls] at least one domain is required\");\n }\n\n // 1. Fetch directory.\n log(`[tls] fetching ACME directory ${directoryUrl}`);\n const directoryResponse = await fetcher(directoryUrl);\n if (!directoryResponse.ok) {\n throw new AcmeError(\n `failed to fetch ACME directory: ${directoryResponse.status}`,\n directoryResponse.status,\n await directoryResponse.text(),\n );\n }\n const directory = (await directoryResponse.json()) as AcmeDirectory;\n\n // 2. Get initial nonce.\n let nonce = await fetchNonce(fetcher, directory.newNonce);\n\n // Helper: signed POST. Tracks nonce; returns parsed JSON body + headers.\n const post = async (\n url: string,\n payload: object | string,\n identification: { kid?: string; jwk?: JsonWebKey },\n ): Promise<{ status: number; body: unknown; headers: Headers }> => {\n const header: JwsHeader = {\n alg: \"ES256\",\n nonce,\n url,\n ...(identification.kid !== undefined ? { kid: identification.kid } : {}),\n ...(identification.jwk !== undefined ? { jwk: identification.jwk } : {}),\n };\n const jws = await signJws(account.key.privateKey, header, payload);\n const response = await fetcher(url, {\n body: JSON.stringify(jws),\n headers: { \"content-type\": \"application/jose+json\" },\n method: \"POST\",\n });\n const newNonce = response.headers.get(\"replay-nonce\");\n if (newNonce !== null) nonce = newNonce;\n const text = await response.text();\n const body =\n text.length > 0 && response.headers.get(\"content-type\")?.includes(\"json\")\n ? JSON.parse(text)\n : text;\n if (!response.ok) {\n throw new AcmeError(\n `ACME ${url} → ${response.status}`,\n response.status,\n body,\n );\n }\n return { body, headers: response.headers, status: response.status };\n };\n\n // 3. Register the account (or reuse if kid is set).\n const publicJwk = await exportPublicJwk(account.key.publicKey);\n if (account.kid === undefined) {\n log(`[tls] registering ACME account for ${options.email}`);\n const result = await post(\n directory.newAccount,\n {\n contact: [`mailto:${options.email}`],\n termsOfServiceAgreed: true,\n },\n { jwk: publicJwk },\n );\n account.kid = result.headers.get(\"location\") ?? undefined;\n if (account.kid === undefined) {\n throw new Error(\n \"[deploy/tls] ACME newAccount response missing Location header\",\n );\n }\n log(`[tls] account registered: ${account.kid}`);\n } else {\n log(`[tls] reusing account ${account.kid}`);\n }\n\n const accountKid = account.kid;\n\n // 4. Submit the order.\n log(`[tls] submitting order for ${options.domains.join(\", \")}`);\n const orderResult = await post(\n directory.newOrder,\n {\n identifiers: options.domains.map((domain) => ({\n type: \"dns\",\n value: domain,\n })),\n },\n { kid: accountKid },\n );\n let order = orderResult.body as AcmeOrder;\n const orderUrl = orderResult.headers.get(\"location\");\n if (orderUrl === null) {\n throw new Error(\n \"[deploy/tls] ACME newOrder response missing Location header\",\n );\n }\n\n // 5. For each authorization, complete the DNS-01 challenge.\n const cleanups: Array<() => Promise<void>> = [];\n const dnsCreated: Array<{\n recordId: string;\n recordName: string;\n recordValue: string;\n }> = [];\n\n try {\n const thumbprint = await jwkThumbprint(publicJwk);\n\n for (const authUrl of order.authorizations) {\n const authResult = await post(authUrl, \"\", { kid: accountKid });\n const auth = authResult.body as AcmeAuthorization;\n const challenge = auth.challenges.find((c) => c.type === \"dns-01\");\n if (challenge === undefined) {\n throw new Error(\n `[deploy/tls] no dns-01 challenge for ${auth.identifier.value}`,\n );\n }\n const keyAuthorization = `${challenge.token}.${thumbprint}`;\n const txtBytes = new Uint8Array(\n await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(keyAuthorization),\n ),\n );\n const txtValue = base64UrlEncode(txtBytes);\n const recordName = `_acme-challenge.${auth.identifier.value}`;\n const providerRecordName =\n options.mapDnsChallengeRecord?.({\n domain: auth.identifier.value,\n recordName,\n }) ?? recordName;\n if (providerRecordName.trim().length === 0) {\n throw new Error(\n \"[deploy/tls] mapped DNS-01 record name must not be empty\",\n );\n }\n log(\n providerRecordName === recordName\n ? `[tls] DNS-01: setting ${recordName} = \"${txtValue}\"`\n : `[tls] DNS-01: setting delegated ${providerRecordName} for ${recordName} = \"${txtValue}\"`,\n );\n const record = await options.dnsProvider.upsert({\n content: txtValue,\n name: providerRecordName,\n ttl: 60,\n type: \"TXT\",\n });\n dnsCreated.push({\n recordId: record.id,\n recordName: providerRecordName,\n recordValue: txtValue,\n });\n cleanups.push(() => options.dnsProvider.delete(record.id));\n\n if (options.checkDnsPropagated !== undefined) {\n log(\"[tls] waiting for DNS propagation (custom checker)…\");\n const deadline = Date.now() + dnsDelay;\n while (Date.now() < deadline) {\n if (await options.checkDnsPropagated(recordName, txtValue)) break;\n await sleep(pollMs);\n }\n } else {\n log(`[tls] sleeping ${dnsDelay}ms for DNS propagation`);\n await sleep(dnsDelay);\n }\n\n log(\n `[tls] notifying ACME of dns-01 readiness for ${auth.identifier.value}`,\n );\n await post(challenge.url, {}, { kid: accountKid });\n\n // Poll the authorization until valid/invalid.\n const authDeadline = Date.now() + orderTimeout;\n while (Date.now() < authDeadline) {\n const polledResult = await post(authUrl, \"\", { kid: accountKid });\n const polled = polledResult.body as AcmeAuthorization;\n if (polled.status === \"valid\") break;\n if (polled.status === \"invalid\") {\n throw new Error(\n `[deploy/tls] authorization failed for ${auth.identifier.value}: ${JSON.stringify(polled.challenges)}`,\n );\n }\n await sleep(pollMs);\n }\n }\n\n // 6. Finalize — generate cert keypair + CSR.\n log(\"[tls] generating cert keypair + CSR\");\n const certKey = await generateCertKeyPair();\n const csrDer = await buildCsr(options.domains, certKey);\n const csr = base64UrlEncode(csrDer);\n\n await post(order.finalize, { csr }, { kid: accountKid });\n\n // 7. Poll the order until valid + certificate URL appears.\n const orderDeadline = Date.now() + orderTimeout;\n while (Date.now() < orderDeadline) {\n const refreshed = await post(orderUrl, \"\", { kid: accountKid });\n order = refreshed.body as AcmeOrder;\n if (order.status === \"valid\" && order.certificate !== undefined) break;\n if (order.status === \"invalid\") {\n throw new Error(`[deploy/tls] order failed: ${JSON.stringify(order)}`);\n }\n await sleep(pollMs);\n }\n\n if (order.certificate === undefined) {\n throw new Error(\n \"[deploy/tls] order timed out before issuing certificate\",\n );\n }\n\n // 8. Download the cert (PEM bundle).\n log(`[tls] downloading certificate from ${order.certificate}`);\n const certResult = await post(order.certificate, \"\", { kid: accountKid });\n const certificatePem =\n typeof certResult.body === \"string\"\n ? certResult.body\n : String(certResult.body);\n\n const privateKeyPem = await exportEcPrivateKeyPem(certKey);\n\n return {\n account,\n certificatePem,\n domains: options.domains,\n privateKeyPem,\n };\n } finally {\n for (const cleanup of cleanups) {\n try {\n await cleanup();\n } catch (error) {\n log(`[tls] cleanup failed (continuing): ${String(error)}`);\n }\n }\n // Suppress unused-variable lint without changing behavior.\n void dnsCreated;\n }\n};\n\nconst fetchNonce = async (\n fetcher: typeof fetch,\n url: string,\n): Promise<string> => {\n const response = await fetcher(url, { method: \"HEAD\" });\n const nonce = response.headers.get(\"replay-nonce\");\n if (nonce === null) {\n throw new Error(\n \"[deploy/tls] newNonce response missing Replay-Nonce header\",\n );\n }\n return nonce;\n};\n\n// =============================================================================\n// installCertificateOnTarget\n// =============================================================================\n\nexport type InstallCertificateOptions = {\n /** Remote path for the cert chain. Default `/etc/ssl/<firstDomain>/fullchain.pem`. */\n certPath?: string;\n /** Remote path for the private key. Default `/etc/ssl/<firstDomain>/privkey.pem`. */\n keyPath?: string;\n /** Mode (chmod) for the cert + key. Default `600`. */\n mode?: string;\n /** Owner (chown) for the cert + key. Default unchanged. */\n owner?: string;\n /** Optional reload command run after install (e.g. `'systemctl reload nginx'`). */\n reload?: string;\n /** Override the writeTo helper (tests). */\n writeFile?: (target: Target, path: string, contents: string) => Promise<void>;\n};\n\nconst defaultWriteFile = async (\n target: Target,\n remotePath: string,\n contents: string,\n): Promise<void> => {\n const escaped = contents.replaceAll(\"'\", \"'\\\\''\");\n await target.exec(`mkdir -p \"$(dirname '${remotePath}')\"`);\n await target.exec(\n `cat > '${remotePath}' <<'__ABS_TLS_EOF__'\\n${contents}__ABS_TLS_EOF__\\n`,\n );\n void escaped;\n};\n\n/**\n * @internal — exposed for unit testing of cryptographic primitives.\n * Not part of the public API; consumers should NOT depend on this.\n */\nexport const __testing = {\n base64UrlDecode,\n base64UrlEncode,\n base64UrlEncodeString,\n buildCsr,\n derBitString,\n derInt,\n derOid,\n derSeq,\n derSet,\n ecdsaRawToDer,\n exportEcPrivateKeyPem,\n exportPublicJwk,\n generateCertKeyPair,\n jwkThumbprint,\n signJws,\n};\n\n/**\n * Upload cert + private key to the target. Composable with the deploy\n * pipeline as a verify-step or post-deploy hook.\n */\nexport const installCertificateOnTarget = async (\n target: Target,\n cert: IssuedCertificate,\n options: InstallCertificateOptions = {},\n): Promise<{ certPath: string; keyPath: string }> => {\n const domain = cert.domains[0];\n if (domain === undefined) {\n throw new Error(\"[deploy/tls] certificate has no domains\");\n }\n const certPath = options.certPath ?? `/etc/ssl/${domain}/fullchain.pem`;\n const keyPath = options.keyPath ?? `/etc/ssl/${domain}/privkey.pem`;\n const mode = options.mode ?? \"600\";\n const writer = options.writeFile ?? defaultWriteFile;\n\n await writer(target, certPath, cert.certificatePem);\n await writer(target, keyPath, cert.privateKeyPem);\n await target.exec(`chmod ${mode} '${certPath}' '${keyPath}'`);\n if (options.owner !== undefined) {\n await target.exec(`chown ${options.owner} '${certPath}' '${keyPath}'`);\n }\n if (options.reload !== undefined) {\n await target.exec(options.reload);\n }\n\n return { certPath, keyPath };\n};\n\n// =============================================================================\n// Certificate inspection + renewal scheduling\n// =============================================================================\n\nexport type CertificateInspection = {\n /** CN + every SAN, deduplicated. */\n subjects: string[];\n /** Issuance time, ms since epoch. */\n validFrom: number;\n /** Expiration time, ms since epoch. */\n validTo: number;\n /** Whole days remaining before `validTo`. Negative if expired. */\n daysRemaining: number;\n /** True when the cert is past `validTo`. */\n expired: boolean;\n /** Issuer DN string (for \"Let's Encrypt vs staging vs other CA\" checks). */\n issuer: string;\n};\n\nconst parseSubjects = (x509: X509Certificate): string[] => {\n const subjects = new Set<string>();\n const cnMatch = x509.subject.match(/CN=([^,\\n]+)/);\n if (cnMatch !== null && cnMatch[1] !== undefined) {\n subjects.add(cnMatch[1].trim());\n }\n const san = x509.subjectAltName;\n if (san !== undefined && san !== null) {\n for (const part of san.split(\",\")) {\n const dnsMatch = part.trim().match(/^DNS:(.+)$/);\n if (dnsMatch !== null && dnsMatch[1] !== undefined) {\n subjects.add(dnsMatch[1].trim());\n }\n }\n }\n return [...subjects];\n};\n\n/**\n * Parse a PEM certificate into operator-shaped metadata. Used by\n * {@link renewCertificate} to decide whether to re-issue; also fine\n * for status pages and observability.\n */\nexport const inspectCertificate = (\n pem: string,\n options: { now?: () => number } = {},\n): CertificateInspection => {\n const x509 = new X509Certificate(pem);\n const validFrom = new Date(x509.validFrom).getTime();\n const validTo = new Date(x509.validTo).getTime();\n const now = (options.now ?? Date.now)();\n const daysRemaining = Math.floor((validTo - now) / (24 * 60 * 60 * 1000));\n return {\n daysRemaining,\n expired: validTo < now,\n issuer: x509.issuer,\n subjects: parseSubjects(x509),\n validFrom,\n validTo,\n };\n};\n\nexport type RenewCertificateOptions = IssueCertificateOptions & {\n /**\n * Current cert PEM. When provided, the cert's `validTo` decides\n * whether to re-issue; when absent, renewal always issues.\n */\n currentCertificatePem?: string;\n /**\n * Re-issue when fewer than this many days remain. Default 30,\n * matching certbot's standard schedule (Let's Encrypt issues 90-day\n * certs; renewing at 30 days leaves a 60-day error budget).\n */\n renewWhenDaysRemaining?: number;\n /** Force re-issue regardless of remaining days. Default false. */\n force?: boolean;\n /** Override `Date.now()` for testing. */\n now?: () => number;\n};\n\nexport type RenewalResult =\n | {\n renewed: true;\n certificate: IssuedCertificate;\n reason: \"forced\" | \"no-current-cert\" | \"expiring-soon\";\n }\n | {\n renewed: false;\n reason: \"still-fresh\";\n inspection: CertificateInspection;\n };\n\n/**\n * Conditional cert renewal. Parses the supplied cert (if any),\n * decides whether to re-issue based on remaining days, and either\n * returns the existing cert info (\"still fresh\") or issues a new\n * one via {@link issueCertificate}.\n *\n * Wire to a scheduled function (cron / @absolutejs/sync schedule /\n * GitHub Action) running at least once a week. Idempotent: a fresh\n * cert returns `renewed: false` cheaply (one PEM parse, zero\n * network IO).\n *\n * @example\n * const result = await renewCertificate({\n * currentCertificatePem: existingPem, // omit to force first-issue\n * domains: ['api.example.com'],\n * dnsProvider: dns,\n * email: 'ops@example.com',\n * account: persistedAccount, // reuse to avoid CA rate limit\n * });\n * if (result.renewed) {\n * await installCertificateOnTarget(target, result.certificate, { reload });\n * }\n */\nexport const renewCertificate = async (\n options: RenewCertificateOptions,\n): Promise<RenewalResult> => {\n const threshold = options.renewWhenDaysRemaining ?? 30;\n if (threshold < 0) {\n throw new Error(\"[deploy/tls] renewWhenDaysRemaining must be non-negative\");\n }\n\n if (options.force === true) {\n const certificate = await issueCertificate(options);\n return { certificate, reason: \"forced\", renewed: true };\n }\n\n if (options.currentCertificatePem === undefined) {\n const certificate = await issueCertificate(options);\n return { certificate, reason: \"no-current-cert\", renewed: true };\n }\n\n const nowFn = options.now ?? Date.now;\n const inspection = inspectCertificate(options.currentCertificatePem, {\n now: nowFn,\n });\n\n if (!inspection.expired && inspection.daysRemaining >= threshold) {\n return { inspection, reason: \"still-fresh\", renewed: false };\n }\n\n const certificate = await issueCertificate(options);\n return { certificate, reason: \"expiring-soon\", renewed: true };\n};\n"
|
|
5
|
+
"/**\n * @absolutejs/deploy/tls — ACME (RFC 8555) client for Let's Encrypt and\n * compatible CAs. DNS-01 challenges only — uses the {@link DnsProvider}\n * abstraction from `./dns`, so the same Cloudflare / Route 53 /\n * Hetzner-DNS providers that point hostnames at boxes also satisfy\n * ACME's challenge requirement.\n *\n * Zero peer deps. Bun's `crypto.subtle` covers JWS signing and ECDSA\n * key generation; a small DER encoder builds the CSR.\n *\n * Public API:\n *\n * issueCertificate({ domains, dnsProvider, email })\n * → { certificatePem, privateKeyPem, account, domains }\n *\n * installCertificateOnTarget(target, cert, { certPath, keyPath, reload? })\n * → uploads PEM files via Target.upload + optional reload exec\n *\n * exportAccountKey / importAccountKey for persistence between runs\n * (reuse the same account across cert renewals — cheaper, doesn't\n * hit Let's Encrypt's account-creation rate limit)\n */\n\nimport { X509Certificate } from 'node:crypto';\nimport type { Target } from './targets';\nimport type { DnsProvider } from './dns';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nexport const LETSENCRYPT_PRODUCTION =\n\t'https://acme-v02.api.letsencrypt.org/directory';\nexport const LETSENCRYPT_STAGING =\n\t'https://acme-staging-v02.api.letsencrypt.org/directory';\n\n// =============================================================================\n// base64url\n// =============================================================================\n\nconst base64UrlEncode = (bytes: Uint8Array | ArrayBuffer): string => {\n\tconst buf = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n\tlet binary = '';\n\tfor (const byte of buf) binary += String.fromCharCode(byte);\n\treturn btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n};\n\nconst base64UrlEncodeString = (value: string): string =>\n\tbase64UrlEncode(new TextEncoder().encode(value));\n\nconst base64UrlDecode = (value: string): Uint8Array => {\n\tconst padded = value.replaceAll('-', '+').replaceAll('_', '/').padEnd(\n\t\tMath.ceil(value.length / 4) * 4,\n\t\t'='\n\t);\n\tconst binary = atob(padded);\n\tconst bytes = new Uint8Array(binary.length);\n\tfor (let index = 0; index < binary.length; index += 1) {\n\t\tbytes[index] = binary.charCodeAt(index);\n\t}\n\treturn bytes;\n};\n\n// =============================================================================\n// DER encoding (minimal subset for CSR)\n// =============================================================================\n\nconst derLength = (length: number): Uint8Array => {\n\tif (length < 0x80) return Uint8Array.from([length]);\n\tconst bytes: number[] = [];\n\tlet remaining = length;\n\twhile (remaining > 0) {\n\t\tbytes.unshift(remaining & 0xff);\n\t\tremaining >>= 8;\n\t}\n\treturn Uint8Array.from([0x80 | bytes.length, ...bytes]);\n};\n\nconst derTag = (tag: number, payload: Uint8Array): Uint8Array => {\n\tconst length = derLength(payload.length);\n\tconst out = new Uint8Array(1 + length.length + payload.length);\n\tout[0] = tag;\n\tout.set(length, 1);\n\tout.set(payload, 1 + length.length);\n\treturn out;\n};\n\nconst derSeq = (children: Uint8Array[]): Uint8Array => {\n\tconst total = children.reduce((sum, child) => sum + child.length, 0);\n\tconst payload = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const child of children) {\n\t\tpayload.set(child, offset);\n\t\toffset += child.length;\n\t}\n\treturn derTag(0x30, payload);\n};\n\nconst derSet = (children: Uint8Array[]): Uint8Array => {\n\tconst total = children.reduce((sum, child) => sum + child.length, 0);\n\tconst payload = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const child of children) {\n\t\tpayload.set(child, offset);\n\t\toffset += child.length;\n\t}\n\treturn derTag(0x31, payload);\n};\n\nconst derInt = (value: number | Uint8Array): Uint8Array => {\n\tif (typeof value === 'number') {\n\t\t// Small unsigned int — used for the CSR version field (0).\n\t\tif (value === 0) return derTag(0x02, Uint8Array.from([0]));\n\t\tconst bytes: number[] = [];\n\t\tlet remaining = value;\n\t\twhile (remaining > 0) {\n\t\t\tbytes.unshift(remaining & 0xff);\n\t\t\tremaining >>= 8;\n\t\t}\n\t\tif ((bytes[0] as number) & 0x80) bytes.unshift(0); // ensure positive\n\t\treturn derTag(0x02, Uint8Array.from(bytes));\n\t}\n\t// Big-endian unsigned bytes — strip leading zeros, then prepend 0x00 if\n\t// the high bit is set (DER INTEGER is signed two's complement).\n\tlet start = 0;\n\twhile (start < value.length - 1 && value[start] === 0) start += 1;\n\tlet payload = value.subarray(start);\n\tif ((payload[0] as number) & 0x80) {\n\t\tconst padded = new Uint8Array(payload.length + 1);\n\t\tpadded.set(payload, 1);\n\t\tpayload = padded;\n\t}\n\treturn derTag(0x02, payload);\n};\n\nconst derOid = (oid: string): Uint8Array => {\n\tconst parts = oid.split('.').map((part) => Number.parseInt(part, 10));\n\tconst first = parts[0];\n\tconst second = parts[1];\n\tif (first === undefined || second === undefined) {\n\t\tthrow new Error(`[deploy/tls] invalid OID: ${oid}`);\n\t}\n\tconst bytes: number[] = [first * 40 + second];\n\tfor (let index = 2; index < parts.length; index += 1) {\n\t\tconst value = parts[index] as number;\n\t\tconst chunks: number[] = [];\n\t\tlet remaining = value;\n\t\tdo {\n\t\t\tchunks.unshift(remaining & 0x7f);\n\t\t\tremaining >>= 7;\n\t\t} while (remaining > 0);\n\t\tfor (let chunkIndex = 0; chunkIndex < chunks.length - 1; chunkIndex += 1) {\n\t\t\tchunks[chunkIndex] = (chunks[chunkIndex] as number) | 0x80;\n\t\t}\n\t\tbytes.push(...chunks);\n\t}\n\treturn derTag(0x06, Uint8Array.from(bytes));\n};\n\nconst derPrintableString = (value: string): Uint8Array =>\n\tderTag(0x13, new TextEncoder().encode(value));\n\nconst derUtf8String = (value: string): Uint8Array =>\n\tderTag(0x0c, new TextEncoder().encode(value));\n\nconst derIa5String = (value: string): Uint8Array =>\n\tderTag(0x16, new TextEncoder().encode(value));\n\nconst derOctetString = (payload: Uint8Array): Uint8Array =>\n\tderTag(0x04, payload);\n\nconst derBitString = (payload: Uint8Array): Uint8Array => {\n\t// 0x00 prefix = number of unused bits in the final byte; for byte-aligned\n\t// signatures + keys this is always zero.\n\tconst bits = new Uint8Array(payload.length + 1);\n\tbits[0] = 0;\n\tbits.set(payload, 1);\n\treturn derTag(0x03, bits);\n};\n\nconst derContextTag = (\n\ttag: number,\n\tpayload: Uint8Array,\n\tconstructed = true\n): Uint8Array => derTag(0xa0 + tag + (constructed ? 0 : -0x20), payload);\n\n// =============================================================================\n// ECDSA signature: raw r||s ↔ DER SEQUENCE\n// =============================================================================\n\nconst ecdsaRawToDer = (rawSig: Uint8Array): Uint8Array => {\n\tconst half = rawSig.length / 2;\n\tconst r = rawSig.subarray(0, half);\n\tconst s = rawSig.subarray(half);\n\treturn derSeq([derInt(r), derInt(s)]);\n};\n\n// =============================================================================\n// JWS (RFC 7515) Flattened JSON Serialization for ACME\n// =============================================================================\n\ntype JwsHeader = {\n\talg: 'ES256';\n\tnonce: string;\n\turl: string;\n\tjwk?: JsonWebKey;\n\tkid?: string;\n};\n\ntype JwsBody = {\n\tprotected: string;\n\tpayload: string;\n\tsignature: string;\n};\n\nconst signJws = async (\n\tprivateKey: CryptoKey,\n\theader: JwsHeader,\n\tpayload: object | string\n): Promise<JwsBody> => {\n\tconst protectedHeader = base64UrlEncodeString(JSON.stringify(header));\n\tconst payloadEncoded =\n\t\tpayload === ''\n\t\t\t? '' // POST-as-GET: empty string, NOT empty object\n\t\t\t: base64UrlEncodeString(JSON.stringify(payload));\n\tconst signingInput = new TextEncoder().encode(\n\t\t`${protectedHeader}.${payloadEncoded}`\n\t);\n\tconst rawSig = new Uint8Array(\n\t\tawait crypto.subtle.sign(\n\t\t\t{ hash: 'SHA-256', name: 'ECDSA' },\n\t\t\tprivateKey,\n\t\t\tsigningInput\n\t\t)\n\t);\n\treturn {\n\t\tpayload: payloadEncoded,\n\t\tprotected: protectedHeader,\n\t\tsignature: base64UrlEncode(rawSig)\n\t};\n};\n\n// =============================================================================\n// JWK helpers — canonical thumbprint, public-key extraction\n// =============================================================================\n\nconst exportPublicJwk = async (publicKey: CryptoKey): Promise<JsonWebKey> => {\n\tconst jwk = await crypto.subtle.exportKey('jwk', publicKey);\n\t// Strip private fields if the export included them (shouldn't, on a\n\t// public key, but be defensive).\n\treturn {\n\t\tcrv: jwk.crv,\n\t\tkty: jwk.kty,\n\t\tx: jwk.x,\n\t\ty: jwk.y\n\t};\n};\n\nconst jwkThumbprint = async (publicJwk: JsonWebKey): Promise<string> => {\n\t// RFC 7638 — canonical JSON: required fields only, lex order.\n\tconst canonical = JSON.stringify({\n\t\tcrv: publicJwk.crv,\n\t\tkty: publicJwk.kty,\n\t\tx: publicJwk.x,\n\t\ty: publicJwk.y\n\t});\n\tconst hash = await crypto.subtle.digest(\n\t\t'SHA-256',\n\t\tnew TextEncoder().encode(canonical)\n\t);\n\treturn base64UrlEncode(hash);\n};\n\n// =============================================================================\n// Account key — generation + JSON export/import for persistence\n// =============================================================================\n\nexport type AcmeAccount = {\n\tkey: CryptoKeyPair;\n\t/** Account URL — set after first registration; persisted for renewals. */\n\tkid?: string;\n};\n\nexport type AcmeAccountJson = {\n\tpublicJwk: JsonWebKey;\n\tprivateJwk: JsonWebKey;\n\tkid?: string;\n};\n\nexport const generateAccountKey = async (): Promise<AcmeAccount> => {\n\tconst key = await crypto.subtle.generateKey(\n\t\t{ name: 'ECDSA', namedCurve: 'P-256' },\n\t\ttrue,\n\t\t['sign', 'verify']\n\t);\n\treturn { key };\n};\n\nexport const exportAccount = async (\n\taccount: AcmeAccount\n): Promise<AcmeAccountJson> => {\n\tconst publicJwk = await crypto.subtle.exportKey('jwk', account.key.publicKey);\n\tconst privateJwk = await crypto.subtle.exportKey(\n\t\t'jwk',\n\t\taccount.key.privateKey\n\t);\n\treturn {\n\t\tprivateJwk,\n\t\tpublicJwk,\n\t\t...(account.kid !== undefined ? { kid: account.kid } : {})\n\t};\n};\n\nexport const importAccount = async (\n\tjson: AcmeAccountJson\n): Promise<AcmeAccount> => {\n\tconst publicKey = await crypto.subtle.importKey(\n\t\t'jwk',\n\t\tjson.publicJwk,\n\t\t{ name: 'ECDSA', namedCurve: 'P-256' },\n\t\ttrue,\n\t\t['verify']\n\t);\n\tconst privateKey = await crypto.subtle.importKey(\n\t\t'jwk',\n\t\tjson.privateJwk,\n\t\t{ name: 'ECDSA', namedCurve: 'P-256' },\n\t\ttrue,\n\t\t['sign']\n\t);\n\treturn {\n\t\tkey: { privateKey, publicKey },\n\t\t...(json.kid !== undefined ? { kid: json.kid } : {})\n\t};\n};\n\n// =============================================================================\n// CSR — Certificate Signing Request\n// =============================================================================\n\nconst buildSanExtension = (domains: string[]): Uint8Array => {\n\t// SubjectAltName extension value: SEQUENCE OF GeneralName\n\tconst generalNames = domains.map((domain) =>\n\t\tderTag(0x82, new TextEncoder().encode(domain))\n\t);\n\tconst sanSequence = derSeq(generalNames);\n\tconst extensionValue = derOctetString(sanSequence);\n\treturn derSeq([derOid('2.5.29.17'), extensionValue]);\n};\n\nconst buildExtensionRequestAttribute = (domains: string[]): Uint8Array => {\n\tconst extensions = derSeq([buildSanExtension(domains)]);\n\treturn derSeq([\n\t\tderOid('1.2.840.113549.1.9.14'), // PKCS#9 extensionRequest\n\t\tderSet([extensions])\n\t]);\n};\n\nconst generateCertKeyPair = async (): Promise<CryptoKeyPair> =>\n\tcrypto.subtle.generateKey(\n\t\t{ name: 'ECDSA', namedCurve: 'P-256' },\n\t\ttrue,\n\t\t['sign', 'verify']\n\t);\n\nconst buildCsr = async (\n\tdomains: string[],\n\tkeypair: CryptoKeyPair\n): Promise<Uint8Array> => {\n\tif (domains.length === 0) {\n\t\tthrow new Error('[deploy/tls] CSR requires at least one domain');\n\t}\n\tconst commonName = domains[0] as string;\n\n\tconst spkiDer = new Uint8Array(\n\t\tawait crypto.subtle.exportKey('spki', keypair.publicKey)\n\t);\n\n\tconst version = derInt(0);\n\tconst subject = derSeq([\n\t\tderSet([\n\t\t\tderSeq([\n\t\t\t\tderOid('2.5.4.3'), // commonName\n\t\t\t\tderUtf8String(commonName)\n\t\t\t])\n\t\t])\n\t]);\n\n\tconst attributes = derContextTag(0, buildExtensionRequestAttribute(domains));\n\n\tconst certificationRequestInfo = derSeq([\n\t\tversion,\n\t\tsubject,\n\t\tspkiDer,\n\t\tattributes\n\t]);\n\n\tconst rawSig = new Uint8Array(\n\t\tawait crypto.subtle.sign(\n\t\t\t{ hash: 'SHA-256', name: 'ECDSA' },\n\t\t\tkeypair.privateKey,\n\t\t\tcertificationRequestInfo as BufferSource\n\t\t)\n\t);\n\tconst sigDer = ecdsaRawToDer(rawSig);\n\n\tconst sigAlgorithm = derSeq([derOid('1.2.840.10045.4.3.2')]);\n\t// ecdsa-with-SHA256\n\tconst signatureBits = derBitString(sigDer);\n\n\treturn derSeq([certificationRequestInfo, sigAlgorithm, signatureBits]);\n};\n\n// =============================================================================\n// PEM\n// =============================================================================\n\nconst derToPem = (label: string, der: Uint8Array): string => {\n\tconst b64 = btoa(String.fromCharCode(...der));\n\tconst lines = b64.match(/.{1,64}/g) ?? [];\n\treturn `-----BEGIN ${label}-----\\n${lines.join('\\n')}\\n-----END ${label}-----\\n`;\n};\n\nconst exportEcPrivateKeyPem = async (\n\tkeypair: CryptoKeyPair\n): Promise<string> => {\n\tconst pkcs8 = new Uint8Array(\n\t\tawait crypto.subtle.exportKey('pkcs8', keypair.privateKey)\n\t);\n\treturn derToPem('PRIVATE KEY', pkcs8);\n};\n\n// =============================================================================\n// ACME client\n// =============================================================================\n\ntype AcmeDirectory = {\n\tnewNonce: string;\n\tnewAccount: string;\n\tnewOrder: string;\n};\n\ntype AcmeOrder = {\n\tstatus: string;\n\tidentifiers: Array<{ type: string; value: string }>;\n\tauthorizations: string[];\n\tfinalize: string;\n\tcertificate?: string;\n};\n\ntype AcmeAuthorization = {\n\tstatus: string;\n\tidentifier: { type: string; value: string };\n\tchallenges: Array<{\n\t\ttype: string;\n\t\tstatus: string;\n\t\turl: string;\n\t\ttoken: string;\n\t}>;\n};\n\nexport type IssueCertificateOptions = {\n\t/** Domain(s) to include. First is the CN; all are SANs. */\n\tdomains: string[];\n\t/** DNS provider used for DNS-01 challenges (must own the zone). */\n\tdnsProvider: DnsProvider;\n\t/**\n\t * Map the public ACME challenge name to the record name written through\n\t * `dnsProvider`. Use this for CNAME-delegated DNS-01, where customers point\n\t * `_acme-challenge.app.example.com` at a platform-owned validation zone.\n\t * The propagation checker still receives the public challenge name because\n\t * that is what the certificate authority resolves.\n\t */\n\tmapDnsChallengeRecord?: (context: {\n\t\tdomain: string;\n\t\trecordName: string;\n\t}) => string;\n\t/** Contact email for the ACME account. */\n\temail: string;\n\t/** ACME directory URL. Default Let's Encrypt production. */\n\tdirectoryUrl?: string;\n\t/** Reuse an existing account. Pass `exportAccount`'s output via `importAccount`. */\n\taccount?: AcmeAccount;\n\t/** Override fetch (tests). Default global fetch. */\n\tfetch?: typeof fetch;\n\t/** Poll interval. Default 3 s. */\n\tpollIntervalMs?: number;\n\t/**\n\t * Max wait before notifying ACME that the DNS challenge is ready.\n\t * Set ~30-60s for Cloudflare; longer for slower providers. Default 30 s.\n\t */\n\tdnsPropagationDelayMs?: number;\n\t/** Max wait for the order to become valid. Default 5 min. */\n\torderTimeoutMs?: number;\n\t/** Status log lines. */\n\tonLog?: (line: string) => void;\n\t/** Override sleep (tests). */\n\tsleep?: (ms: number) => Promise<void>;\n\t/**\n\t * Optional pre-check: returns true when DNS has propagated globally.\n\t * Default: just wait `dnsPropagationDelayMs` then proceed. Override\n\t * for production to actually verify (e.g. resolve from multiple\n\t * public resolvers).\n\t */\n\tcheckDnsPropagated?: (\n\t\trecordName: string,\n\t\texpectedValue: string\n\t) => Promise<boolean>;\n};\n\nexport type IssuedCertificate = {\n\tcertificatePem: string;\n\tprivateKeyPem: string;\n\taccount: AcmeAccount;\n\tdomains: string[];\n};\n\nexport class AcmeError extends Error {\n\treadonly status: number;\n\treadonly body: unknown;\n\tconstructor(message: string, status: number, body: unknown) {\n\t\tsuper(message);\n\t\tthis.name = 'AcmeError';\n\t\tthis.status = status;\n\t\tthis.body = body;\n\t}\n}\n\nconst defaultSleep = (ms: number): Promise<void> =>\n\tnew Promise((resolve) => setTimeout(resolve, ms));\n\nexport const issueCertificate = async (\n\toptions: IssueCertificateOptions\n): Promise<IssuedCertificate> => {\n\tconst log = options.onLog ?? (() => {});\n\tconst sleep = options.sleep ?? defaultSleep;\n\tconst fetcher = options.fetch ?? fetch;\n\tconst directoryUrl = options.directoryUrl ?? LETSENCRYPT_PRODUCTION;\n\tconst pollMs = options.pollIntervalMs ?? 3_000;\n\tconst dnsDelay = options.dnsPropagationDelayMs ?? 30_000;\n\tconst orderTimeout = options.orderTimeoutMs ?? 5 * 60_000;\n\tconst account = options.account ?? (await generateAccountKey());\n\n\tif (options.domains.length === 0) {\n\t\tthrow new Error('[deploy/tls] at least one domain is required');\n\t}\n\n\t// 1. Fetch directory.\n\tlog(`[tls] fetching ACME directory ${directoryUrl}`);\n\tconst directoryResponse = await fetcher(directoryUrl);\n\tif (!directoryResponse.ok) {\n\t\tthrow new AcmeError(\n\t\t\t`failed to fetch ACME directory: ${directoryResponse.status}`,\n\t\t\tdirectoryResponse.status,\n\t\t\tawait directoryResponse.text()\n\t\t);\n\t}\n\tconst directory = (await directoryResponse.json()) as AcmeDirectory;\n\n\t// 2. Get initial nonce.\n\tlet nonce = await fetchNonce(fetcher, directory.newNonce);\n\n\t// Helper: signed POST. Tracks nonce; returns parsed JSON body + headers.\n\tconst post = async (\n\t\turl: string,\n\t\tpayload: object | string,\n\t\tidentification: { kid?: string; jwk?: JsonWebKey }\n\t): Promise<{ status: number; body: unknown; headers: Headers }> => {\n\t\tconst header: JwsHeader = {\n\t\t\talg: 'ES256',\n\t\t\tnonce,\n\t\t\turl,\n\t\t\t...(identification.kid !== undefined ? { kid: identification.kid } : {}),\n\t\t\t...(identification.jwk !== undefined ? { jwk: identification.jwk } : {})\n\t\t};\n\t\tconst jws = await signJws(account.key.privateKey, header, payload);\n\t\tconst response = await fetcher(url, {\n\t\t\tbody: JSON.stringify(jws),\n\t\t\theaders: { 'content-type': 'application/jose+json' },\n\t\t\tmethod: 'POST'\n\t\t});\n\t\tconst newNonce = response.headers.get('replay-nonce');\n\t\tif (newNonce !== null) nonce = newNonce;\n\t\tconst text = await response.text();\n\t\tconst body =\n\t\t\ttext.length > 0 && response.headers.get('content-type')?.includes('json')\n\t\t\t\t? JSON.parse(text)\n\t\t\t\t: text;\n\t\tif (!response.ok) {\n\t\t\tthrow new AcmeError(\n\t\t\t\t`ACME ${url} → ${response.status}`,\n\t\t\t\tresponse.status,\n\t\t\t\tbody\n\t\t\t);\n\t\t}\n\t\treturn { body, headers: response.headers, status: response.status };\n\t};\n\n\t// 3. Register the account (or reuse if kid is set).\n\tconst publicJwk = await exportPublicJwk(account.key.publicKey);\n\tif (account.kid === undefined) {\n\t\tlog(`[tls] registering ACME account for ${options.email}`);\n\t\tconst result = await post(\n\t\t\tdirectory.newAccount,\n\t\t\t{\n\t\t\t\tcontact: [`mailto:${options.email}`],\n\t\t\t\ttermsOfServiceAgreed: true\n\t\t\t},\n\t\t\t{ jwk: publicJwk }\n\t\t);\n\t\taccount.kid = result.headers.get('location') ?? undefined;\n\t\tif (account.kid === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t'[deploy/tls] ACME newAccount response missing Location header'\n\t\t\t);\n\t\t}\n\t\tlog(`[tls] account registered: ${account.kid}`);\n\t} else {\n\t\tlog(`[tls] reusing account ${account.kid}`);\n\t}\n\n\tconst accountKid = account.kid;\n\n\t// 4. Submit the order.\n\tlog(`[tls] submitting order for ${options.domains.join(', ')}`);\n\tconst orderResult = await post(\n\t\tdirectory.newOrder,\n\t\t{\n\t\t\tidentifiers: options.domains.map((domain) => ({\n\t\t\t\ttype: 'dns',\n\t\t\t\tvalue: domain\n\t\t\t}))\n\t\t},\n\t\t{ kid: accountKid }\n\t);\n\tlet order = orderResult.body as AcmeOrder;\n\tconst orderUrl = orderResult.headers.get('location');\n\tif (orderUrl === null) {\n\t\tthrow new Error(\n\t\t\t'[deploy/tls] ACME newOrder response missing Location header'\n\t\t);\n\t}\n\n\t// 5. For each authorization, complete the DNS-01 challenge.\n\tconst cleanups: Array<() => Promise<void>> = [];\n\tconst dnsCreated: Array<{\n\t\trecordId: string;\n\t\trecordName: string;\n\t\trecordValue: string;\n\t}> = [];\n\n\ttry {\n\t\tconst thumbprint = await jwkThumbprint(publicJwk);\n\n\t\tfor (const authUrl of order.authorizations) {\n\t\t\tconst authResult = await post(authUrl, '', { kid: accountKid });\n\t\t\tconst auth = authResult.body as AcmeAuthorization;\n\t\t\tconst challenge = auth.challenges.find((c) => c.type === 'dns-01');\n\t\t\tif (challenge === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[deploy/tls] no dns-01 challenge for ${auth.identifier.value}`\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst keyAuthorization = `${challenge.token}.${thumbprint}`;\n\t\t\tconst txtBytes = new Uint8Array(\n\t\t\t\tawait crypto.subtle.digest(\n\t\t\t\t\t'SHA-256',\n\t\t\t\t\tnew TextEncoder().encode(keyAuthorization)\n\t\t\t\t)\n\t\t\t);\n\t\t\tconst txtValue = base64UrlEncode(txtBytes);\n\t\t\tconst recordName = `_acme-challenge.${auth.identifier.value}`;\n\t\t\tconst providerRecordName =\n\t\t\t\toptions.mapDnsChallengeRecord?.({\n\t\t\t\t\tdomain: auth.identifier.value,\n\t\t\t\t\trecordName\n\t\t\t\t}) ?? recordName;\n\t\t\tif (providerRecordName.trim().length === 0) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'[deploy/tls] mapped DNS-01 record name must not be empty'\n\t\t\t\t);\n\t\t\t}\n\t\t\tlog(\n\t\t\t\tproviderRecordName === recordName\n\t\t\t\t\t? `[tls] DNS-01: setting ${recordName} = \"${txtValue}\"`\n\t\t\t\t\t: `[tls] DNS-01: setting delegated ${providerRecordName} for ${recordName} = \"${txtValue}\"`\n\t\t\t);\n\t\t\tconst record = await options.dnsProvider.upsert({\n\t\t\t\tcontent: txtValue,\n\t\t\t\tname: providerRecordName,\n\t\t\t\tttl: 60,\n\t\t\t\ttype: 'TXT'\n\t\t\t});\n\t\t\tdnsCreated.push({\n\t\t\t\trecordId: record.id,\n\t\t\t\trecordName: providerRecordName,\n\t\t\t\trecordValue: txtValue\n\t\t\t});\n\t\t\tcleanups.push(() => options.dnsProvider.delete(record.id));\n\n\t\t\tif (options.checkDnsPropagated !== undefined) {\n\t\t\t\tlog('[tls] waiting for DNS propagation (custom checker)…');\n\t\t\t\tconst deadline = Date.now() + dnsDelay;\n\t\t\t\twhile (Date.now() < deadline) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tawait options.checkDnsPropagated(recordName, txtValue)\n\t\t\t\t\t) break;\n\t\t\t\t\tawait sleep(pollMs);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog(`[tls] sleeping ${dnsDelay}ms for DNS propagation`);\n\t\t\t\tawait sleep(dnsDelay);\n\t\t\t}\n\n\t\t\tlog(`[tls] notifying ACME of dns-01 readiness for ${auth.identifier.value}`);\n\t\t\tawait post(challenge.url, {}, { kid: accountKid });\n\n\t\t\t// Poll the authorization until valid/invalid.\n\t\t\tconst authDeadline = Date.now() + orderTimeout;\n\t\t\twhile (Date.now() < authDeadline) {\n\t\t\t\tconst polledResult = await post(authUrl, '', { kid: accountKid });\n\t\t\t\tconst polled = polledResult.body as AcmeAuthorization;\n\t\t\t\tif (polled.status === 'valid') break;\n\t\t\t\tif (polled.status === 'invalid') {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`[deploy/tls] authorization failed for ${auth.identifier.value}: ${JSON.stringify(polled.challenges)}`\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tawait sleep(pollMs);\n\t\t\t}\n\t\t}\n\n\t\t// 6. Finalize — generate cert keypair + CSR.\n\t\tlog('[tls] generating cert keypair + CSR');\n\t\tconst certKey = await generateCertKeyPair();\n\t\tconst csrDer = await buildCsr(options.domains, certKey);\n\t\tconst csr = base64UrlEncode(csrDer);\n\n\t\tawait post(order.finalize, { csr }, { kid: accountKid });\n\n\t\t// 7. Poll the order until valid + certificate URL appears.\n\t\tconst orderDeadline = Date.now() + orderTimeout;\n\t\twhile (Date.now() < orderDeadline) {\n\t\t\tconst refreshed = await post(orderUrl, '', { kid: accountKid });\n\t\t\torder = refreshed.body as AcmeOrder;\n\t\t\tif (order.status === 'valid' && order.certificate !== undefined) break;\n\t\t\tif (order.status === 'invalid') {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`[deploy/tls] order failed: ${JSON.stringify(order)}`\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait sleep(pollMs);\n\t\t}\n\n\t\tif (order.certificate === undefined) {\n\t\t\tthrow new Error('[deploy/tls] order timed out before issuing certificate');\n\t\t}\n\n\t\t// 8. Download the cert (PEM bundle).\n\t\tlog(`[tls] downloading certificate from ${order.certificate}`);\n\t\tconst certResult = await post(order.certificate, '', { kid: accountKid });\n\t\tconst certificatePem =\n\t\t\ttypeof certResult.body === 'string'\n\t\t\t\t? certResult.body\n\t\t\t\t: String(certResult.body);\n\n\t\tconst privateKeyPem = await exportEcPrivateKeyPem(certKey);\n\n\t\treturn {\n\t\t\taccount,\n\t\t\tcertificatePem,\n\t\t\tdomains: options.domains,\n\t\t\tprivateKeyPem\n\t\t};\n\t} finally {\n\t\tfor (const cleanup of cleanups) {\n\t\t\ttry {\n\t\t\t\tawait cleanup();\n\t\t\t} catch (error) {\n\t\t\t\tlog(`[tls] cleanup failed (continuing): ${String(error)}`);\n\t\t\t}\n\t\t}\n\t\t// Suppress unused-variable lint without changing behavior.\n\t\tvoid dnsCreated;\n\t}\n};\n\nconst fetchNonce = async (\n\tfetcher: typeof fetch,\n\turl: string\n): Promise<string> => {\n\tconst response = await fetcher(url, { method: 'HEAD' });\n\tconst nonce = response.headers.get('replay-nonce');\n\tif (nonce === null) {\n\t\tthrow new Error('[deploy/tls] newNonce response missing Replay-Nonce header');\n\t}\n\treturn nonce;\n};\n\n// =============================================================================\n// installCertificateOnTarget\n// =============================================================================\n\nexport type InstallCertificateOptions = {\n\t/** Remote path for the cert chain. Default `/etc/ssl/<firstDomain>/fullchain.pem`. */\n\tcertPath?: string;\n\t/** Remote path for the private key. Default `/etc/ssl/<firstDomain>/privkey.pem`. */\n\tkeyPath?: string;\n\t/** Mode (chmod) for the cert + key. Default `600`. */\n\tmode?: string;\n\t/** Owner (chown) for the cert + key. Default unchanged. */\n\towner?: string;\n\t/** Optional reload command run after install (e.g. `'systemctl reload nginx'`). */\n\treload?: string;\n\t/** Override the writeTo helper (tests). */\n\twriteFile?: (target: Target, path: string, contents: string) => Promise<void>;\n};\n\nconst defaultWriteFile = async (\n\ttarget: Target,\n\tremotePath: string,\n\tcontents: string\n): Promise<void> => {\n\tconst escaped = contents.replaceAll(\"'\", \"'\\\\''\");\n\tawait target.exec(`mkdir -p \"$(dirname '${remotePath}')\"`);\n\tawait target.exec(`cat > '${remotePath}' <<'__ABS_TLS_EOF__'\\n${contents}__ABS_TLS_EOF__\\n`);\n\tvoid escaped;\n};\n\n/**\n * @internal — exposed for unit testing of cryptographic primitives.\n * Not part of the public API; consumers should NOT depend on this.\n */\nexport const __testing = {\n\tbase64UrlDecode,\n\tbase64UrlEncode,\n\tbase64UrlEncodeString,\n\tbuildCsr,\n\tderBitString,\n\tderInt,\n\tderOid,\n\tderSeq,\n\tderSet,\n\tecdsaRawToDer,\n\texportEcPrivateKeyPem,\n\texportPublicJwk,\n\tgenerateCertKeyPair,\n\tjwkThumbprint,\n\tsignJws\n};\n\n/**\n * Upload cert + private key to the target. Composable with the deploy\n * pipeline as a verify-step or post-deploy hook.\n */\nexport const installCertificateOnTarget = async (\n\ttarget: Target,\n\tcert: IssuedCertificate,\n\toptions: InstallCertificateOptions = {}\n): Promise<{ certPath: string; keyPath: string }> => {\n\tconst domain = cert.domains[0];\n\tif (domain === undefined) {\n\t\tthrow new Error('[deploy/tls] certificate has no domains');\n\t}\n\tconst certPath = options.certPath ?? `/etc/ssl/${domain}/fullchain.pem`;\n\tconst keyPath = options.keyPath ?? `/etc/ssl/${domain}/privkey.pem`;\n\tconst mode = options.mode ?? '600';\n\tconst writer = options.writeFile ?? defaultWriteFile;\n\n\tawait writer(target, certPath, cert.certificatePem);\n\tawait writer(target, keyPath, cert.privateKeyPem);\n\tawait target.exec(`chmod ${mode} '${certPath}' '${keyPath}'`);\n\tif (options.owner !== undefined) {\n\t\tawait target.exec(`chown ${options.owner} '${certPath}' '${keyPath}'`);\n\t}\n\tif (options.reload !== undefined) {\n\t\tawait target.exec(options.reload);\n\t}\n\n\treturn { certPath, keyPath };\n};\n\n// =============================================================================\n// Certificate inspection + renewal scheduling\n// =============================================================================\n\nexport type CertificateInspection = {\n\t/** CN + every SAN, deduplicated. */\n\tsubjects: string[];\n\t/** Issuance time, ms since epoch. */\n\tvalidFrom: number;\n\t/** Expiration time, ms since epoch. */\n\tvalidTo: number;\n\t/** Whole days remaining before `validTo`. Negative if expired. */\n\tdaysRemaining: number;\n\t/** True when the cert is past `validTo`. */\n\texpired: boolean;\n\t/** Issuer DN string (for \"Let's Encrypt vs staging vs other CA\" checks). */\n\tissuer: string;\n};\n\nconst parseSubjects = (x509: X509Certificate): string[] => {\n\tconst subjects = new Set<string>();\n\tconst cnMatch = x509.subject.match(/CN=([^,\\n]+)/);\n\tif (cnMatch !== null && cnMatch[1] !== undefined) {\n\t\tsubjects.add(cnMatch[1].trim());\n\t}\n\tconst san = x509.subjectAltName;\n\tif (san !== undefined && san !== null) {\n\t\tfor (const part of san.split(',')) {\n\t\t\tconst dnsMatch = part.trim().match(/^DNS:(.+)$/);\n\t\t\tif (dnsMatch !== null && dnsMatch[1] !== undefined) {\n\t\t\t\tsubjects.add(dnsMatch[1].trim());\n\t\t\t}\n\t\t}\n\t}\n\treturn [...subjects];\n};\n\n/**\n * Parse a PEM certificate into operator-shaped metadata. Used by\n * {@link renewCertificate} to decide whether to re-issue; also fine\n * for status pages and observability.\n */\nexport const inspectCertificate = (\n\tpem: string,\n\toptions: { now?: () => number } = {}\n): CertificateInspection => {\n\tconst x509 = new X509Certificate(pem);\n\tconst validFrom = new Date(x509.validFrom).getTime();\n\tconst validTo = new Date(x509.validTo).getTime();\n\tconst now = (options.now ?? Date.now)();\n\tconst daysRemaining = Math.floor(\n\t\t(validTo - now) / (24 * 60 * 60 * 1000)\n\t);\n\treturn {\n\t\tdaysRemaining,\n\t\texpired: validTo < now,\n\t\tissuer: x509.issuer,\n\t\tsubjects: parseSubjects(x509),\n\t\tvalidFrom,\n\t\tvalidTo\n\t};\n};\n\nexport type RenewCertificateOptions = IssueCertificateOptions & {\n\t/**\n\t * Current cert PEM. When provided, the cert's `validTo` decides\n\t * whether to re-issue; when absent, renewal always issues.\n\t */\n\tcurrentCertificatePem?: string;\n\t/**\n\t * Re-issue when fewer than this many days remain. Default 30,\n\t * matching certbot's standard schedule (Let's Encrypt issues 90-day\n\t * certs; renewing at 30 days leaves a 60-day error budget).\n\t */\n\trenewWhenDaysRemaining?: number;\n\t/** Force re-issue regardless of remaining days. Default false. */\n\tforce?: boolean;\n\t/** Override `Date.now()` for testing. */\n\tnow?: () => number;\n};\n\nexport type RenewalResult =\n\t| {\n\t\t\trenewed: true;\n\t\t\tcertificate: IssuedCertificate;\n\t\t\treason: 'forced' | 'no-current-cert' | 'expiring-soon';\n\t }\n\t| {\n\t\t\trenewed: false;\n\t\t\treason: 'still-fresh';\n\t\t\tinspection: CertificateInspection;\n\t };\n\n/**\n * Conditional cert renewal. Parses the supplied cert (if any),\n * decides whether to re-issue based on remaining days, and either\n * returns the existing cert info (\"still fresh\") or issues a new\n * one via {@link issueCertificate}.\n *\n * Wire to a scheduled function (cron / @absolutejs/sync schedule /\n * GitHub Action) running at least once a week. Idempotent: a fresh\n * cert returns `renewed: false` cheaply (one PEM parse, zero\n * network IO).\n *\n * @example\n * const result = await renewCertificate({\n * currentCertificatePem: existingPem, // omit to force first-issue\n * domains: ['api.example.com'],\n * dnsProvider: dns,\n * email: 'ops@example.com',\n * account: persistedAccount, // reuse to avoid CA rate limit\n * });\n * if (result.renewed) {\n * await installCertificateOnTarget(target, result.certificate, { reload });\n * }\n */\nexport const renewCertificate = async (\n\toptions: RenewCertificateOptions\n): Promise<RenewalResult> => {\n\tconst threshold = options.renewWhenDaysRemaining ?? 30;\n\tif (threshold < 0) {\n\t\tthrow new Error(\n\t\t\t'[deploy/tls] renewWhenDaysRemaining must be non-negative'\n\t\t);\n\t}\n\n\tif (options.force === true) {\n\t\tconst certificate = await issueCertificate(options);\n\t\treturn { certificate, reason: 'forced', renewed: true };\n\t}\n\n\tif (options.currentCertificatePem === undefined) {\n\t\tconst certificate = await issueCertificate(options);\n\t\treturn { certificate, reason: 'no-current-cert', renewed: true };\n\t}\n\n\tconst nowFn = options.now ?? Date.now;\n\tconst inspection = inspectCertificate(options.currentCertificatePem, {\n\t\tnow: nowFn\n\t});\n\n\tif (!inspection.expired && inspection.daysRemaining >= threshold) {\n\t\treturn { inspection, reason: 'still-fresh', renewed: false };\n\t}\n\n\tconst certificate = await issueCertificate(options);\n\treturn { certificate, reason: 'expiring-soon', renewed: true };\n};\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "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",
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "6D2588C0F93BA9DA64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/vultr.d.ts
CHANGED
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
* Vultr and referenced by UUID — different from Linode (raw keys)
|
|
9
9
|
* and DO/Hetzner (fingerprints/ids/names).
|
|
10
10
|
*/
|
|
11
|
-
import type { Target } from
|
|
11
|
+
import type { Target } from './targets';
|
|
12
12
|
export type VultrClientLike = {
|
|
13
|
-
request: <T = unknown>(method:
|
|
13
|
+
request: <T = unknown>(method: 'GET' | 'POST' | 'DELETE' | 'PATCH', path: string, body?: unknown) => Promise<T>;
|
|
14
14
|
};
|
|
15
15
|
/** A Vultr instance, narrowed to what we inspect. */
|
|
16
16
|
export type VultrInstance = {
|
|
17
17
|
id: string;
|
|
18
18
|
label: string;
|
|
19
|
-
status:
|
|
20
|
-
power_status?:
|
|
19
|
+
status: 'active' | 'pending' | 'suspended' | 'resizing';
|
|
20
|
+
power_status?: 'running' | 'stopped' | 'starting';
|
|
21
21
|
server_status?: string;
|
|
22
22
|
main_ip: string;
|
|
23
23
|
internal_ip?: string;
|