@absolutejs/deploy 0.8.0 → 0.9.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/README.md CHANGED
@@ -448,6 +448,7 @@ DNS (`DnsProvider`):
448
448
  - `@absolutejs/deploy/cloudflare`
449
449
  - `@absolutejs/deploy/digitalocean-dns`
450
450
  - `@absolutejs/deploy/hetzner-dns`
451
+ - `@absolutejs/deploy/route53` — narrow client interface; BYO `@aws-sdk/client-route-53` via a 4-line shim, or hand-roll a SigV4 fetch client.
451
452
 
452
453
  All compute adapters share the same `createCloudTarget` machinery
453
454
  (find-or-create + wait-for-ready + wait-for-SSH + sshTarget wrap)
@@ -456,11 +457,10 @@ implement the same `DnsProvider` contract (`list / find / create /
456
457
  update / delete / upsert`) so swapping providers is a one-line
457
458
  constructor change.
458
459
 
459
- ## What v0.8.0 does NOT include
460
+ ## What v0.9.0 does NOT include
460
461
 
461
462
  - Fly Machines compute (different abstraction — ephemeral machines
462
463
  with app-scoped naming).
463
- - Route 53 DNS (AWS SigV4 signing — meaningful design surface).
464
464
  - A CLI front-end. Library is complete; the CLI is sugar.
465
465
  - Bun installation on the remote — caller does it once, out of band.
466
466
  - Multi-target / fan-out deploys (caller iterates).
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @absolutejs/deploy/route53 — AWS Route 53 DNS implementing the
3
+ * {@link DnsProvider} contract from `./dns`.
4
+ *
5
+ * Different shape from Cloudflare/DO/Hetzner DNS: Route 53 has no
6
+ * per-record id. Records are identified by their (Name, Type)
7
+ * composite, and mutations are done via `ChangeResourceRecordSets`
8
+ * with a batch of CREATE / UPSERT / DELETE actions. We exploit
9
+ * Route 53's native `UPSERT` action — one API call replaces the
10
+ * find-then-create-or-update dance the other providers need.
11
+ *
12
+ * Narrow `Route53ClientLike` interface so the package stays free
13
+ * of an `@aws-sdk/client-route-53` dependency. Wire your existing
14
+ * AWS SDK client with a 4-line shim — see README.
15
+ */
16
+ import type { DnsProvider } from './dns';
17
+ export type Route53ResourceRecord = {
18
+ Value: string;
19
+ };
20
+ export type Route53ResourceRecordSet = {
21
+ Name: string;
22
+ Type: string;
23
+ TTL?: number;
24
+ ResourceRecords?: Route53ResourceRecord[];
25
+ AliasTarget?: {
26
+ HostedZoneId: string;
27
+ DNSName: string;
28
+ EvaluateTargetHealth: boolean;
29
+ };
30
+ SetIdentifier?: string;
31
+ };
32
+ export type Route53Change = {
33
+ Action: 'CREATE' | 'DELETE' | 'UPSERT';
34
+ ResourceRecordSet: Route53ResourceRecordSet;
35
+ };
36
+ export type Route53ListInput = {
37
+ HostedZoneId: string;
38
+ StartRecordName?: string;
39
+ StartRecordType?: string;
40
+ MaxItems?: string;
41
+ };
42
+ export type Route53ListOutput = {
43
+ ResourceRecordSets: Route53ResourceRecordSet[];
44
+ IsTruncated?: boolean;
45
+ NextRecordName?: string;
46
+ NextRecordType?: string;
47
+ };
48
+ export type Route53ChangeInput = {
49
+ HostedZoneId: string;
50
+ ChangeBatch: {
51
+ Changes: Route53Change[];
52
+ Comment?: string;
53
+ };
54
+ };
55
+ export type Route53ChangeOutput = {
56
+ ChangeInfo: {
57
+ Id: string;
58
+ Status: string;
59
+ };
60
+ };
61
+ /**
62
+ * Minimal subset of the Route 53 client we need. Wire your
63
+ * `@aws-sdk/client-route-53` `Route53Client` with a shim:
64
+ *
65
+ * ```ts
66
+ * import {
67
+ * Route53Client,
68
+ * ListResourceRecordSetsCommand,
69
+ * ChangeResourceRecordSetsCommand,
70
+ * } from '@aws-sdk/client-route-53';
71
+ *
72
+ * const aws = new Route53Client({ region: 'us-east-1' });
73
+ * const client: Route53ClientLike = {
74
+ * listResourceRecordSets: (input) =>
75
+ * aws.send(new ListResourceRecordSetsCommand(input)) as any,
76
+ * changeResourceRecordSets: (input) =>
77
+ * aws.send(new ChangeResourceRecordSetsCommand(input)) as any,
78
+ * };
79
+ * ```
80
+ *
81
+ * Or hand-roll a SigV4-signed fetch client if you want zero deps.
82
+ */
83
+ export type Route53ClientLike = {
84
+ listResourceRecordSets: (input: Route53ListInput) => Promise<Route53ListOutput>;
85
+ changeResourceRecordSets: (input: Route53ChangeInput) => Promise<Route53ChangeOutput>;
86
+ };
87
+ export type Route53ProviderOptions = {
88
+ client: Route53ClientLike;
89
+ /** Hosted zone id, e.g. `'Z2FDTNDATAQYW2'`. */
90
+ hostedZoneId: string;
91
+ /** Zone name for log + name conversion, e.g. `'example.com'`. */
92
+ zoneName: string;
93
+ /** Default TTL when `spec.ttl` is omitted. Default 300. */
94
+ defaultTtl?: number;
95
+ };
96
+ export declare const route53DnsProvider: (options: Route53ProviderOptions) => DnsProvider;
@@ -0,0 +1,170 @@
1
+ // @bun
2
+ // src/route53.ts
3
+ var ensureFqdn = (name, zoneName) => {
4
+ if (name === "@")
5
+ return ensureTrailingDot(zoneName);
6
+ if (name === zoneName)
7
+ return ensureTrailingDot(name);
8
+ if (name.endsWith("."))
9
+ return name;
10
+ return `${name}.`;
11
+ };
12
+ var ensureTrailingDot = (s) => s.endsWith(".") ? s : `${s}.`;
13
+ var stripTrailingDot = (s) => s.endsWith(".") ? s.slice(0, -1) : s;
14
+ var encodeTxtValue = (raw) => {
15
+ if (raw.startsWith('"') && raw.endsWith('"'))
16
+ return raw;
17
+ const CHUNK = 255;
18
+ const chunks = [];
19
+ for (let i = 0;i < raw.length; i += CHUNK) {
20
+ chunks.push(`"${raw.slice(i, i + CHUNK).replaceAll('"', "\\\"")}"`);
21
+ }
22
+ return chunks.join(" ");
23
+ };
24
+ var decodeTxtValue = (wire) => {
25
+ const parts = wire.match(/"((?:\\.|[^"])*)"/g);
26
+ if (parts === null)
27
+ return wire;
28
+ return parts.map((part) => part.slice(1, -1).replaceAll("\\\"", '"')).join("");
29
+ };
30
+ var encodeId = (payload) => btoa(JSON.stringify(payload)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
31
+ var decodeId = (id) => {
32
+ const padded = id.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(id.length / 4) * 4, "=");
33
+ return JSON.parse(atob(padded));
34
+ };
35
+ var toDnsRecord = (set, zoneName) => {
36
+ const values = set.ResourceRecords?.map((r) => r.Value) ?? [];
37
+ if (values.length === 0)
38
+ return;
39
+ const content = set.Type === "TXT" ? decodeTxtValue(values.join(" ")) : values[0];
40
+ const idPayload = {
41
+ n: set.Name,
42
+ t: set.Type,
43
+ v: values,
44
+ ...set.TTL !== undefined ? { l: set.TTL } : {}
45
+ };
46
+ const fqdn = stripTrailingDot(set.Name);
47
+ const name = fqdn === zoneName ? zoneName : fqdn;
48
+ return {
49
+ content,
50
+ id: encodeId(idPayload),
51
+ name,
52
+ type: set.Type,
53
+ ...set.TTL !== undefined ? { ttl: set.TTL } : {}
54
+ };
55
+ };
56
+ var buildRecordSet = (spec, zoneName, defaultTtl) => {
57
+ const Name = ensureFqdn(spec.name, zoneName);
58
+ const ttl = spec.ttl ?? defaultTtl;
59
+ const value = spec.type === "TXT" ? encodeTxtValue(spec.content) : spec.content;
60
+ return {
61
+ Name,
62
+ ResourceRecords: [{ Value: value }],
63
+ TTL: ttl,
64
+ Type: spec.type
65
+ };
66
+ };
67
+ var route53DnsProvider = (options) => {
68
+ const { client, hostedZoneId, zoneName } = options;
69
+ const defaultTtl = options.defaultTtl ?? 300;
70
+ const zoneLabel = zoneName;
71
+ const fetchAll = async (filter) => {
72
+ const out = [];
73
+ let startName;
74
+ let startType;
75
+ for (;; ) {
76
+ const params = {
77
+ HostedZoneId: hostedZoneId,
78
+ MaxItems: "500",
79
+ ...startName !== undefined ? { StartRecordName: startName } : {},
80
+ ...startType !== undefined ? { StartRecordType: startType } : {}
81
+ };
82
+ const page = await client.listResourceRecordSets(params);
83
+ for (const set of page.ResourceRecordSets) {
84
+ const dnsRecord = toDnsRecord(set, zoneName);
85
+ if (dnsRecord === undefined)
86
+ continue;
87
+ if (filter?.name !== undefined && dnsRecord.name !== filter.name) {
88
+ continue;
89
+ }
90
+ if (filter?.type !== undefined && dnsRecord.type !== filter.type) {
91
+ continue;
92
+ }
93
+ out.push(dnsRecord);
94
+ }
95
+ if (page.IsTruncated !== true)
96
+ break;
97
+ startName = page.NextRecordName;
98
+ startType = page.NextRecordType;
99
+ }
100
+ return out;
101
+ };
102
+ const list = (filter) => fetchAll(filter);
103
+ const find = async (key) => {
104
+ const matches = await fetchAll(key);
105
+ if (matches.length === 0)
106
+ return;
107
+ if (matches.length > 1) {
108
+ throw new Error(`[deploy/route53] multiple ${key.type} records for "${key.name}" in zone ${zoneLabel} \u2014 drifted state; resolve manually before upsert.`);
109
+ }
110
+ return matches[0];
111
+ };
112
+ const submitChange = async (change) => {
113
+ await client.changeResourceRecordSets({
114
+ ChangeBatch: { Changes: [change] },
115
+ HostedZoneId: hostedZoneId
116
+ });
117
+ };
118
+ const create = async (spec) => {
119
+ const set = buildRecordSet(spec, zoneName, defaultTtl);
120
+ await submitChange({ Action: "CREATE", ResourceRecordSet: set });
121
+ const synthesized = toDnsRecord(set, zoneName);
122
+ if (synthesized === undefined) {
123
+ throw new Error("[deploy/route53] internal: built record had no ResourceRecords");
124
+ }
125
+ return synthesized;
126
+ };
127
+ const upsert = async (spec) => {
128
+ const set = buildRecordSet(spec, zoneName, defaultTtl);
129
+ await submitChange({ Action: "UPSERT", ResourceRecordSet: set });
130
+ const synthesized = toDnsRecord(set, zoneName);
131
+ if (synthesized === undefined) {
132
+ throw new Error("[deploy/route53] internal: built record had no ResourceRecords");
133
+ }
134
+ return synthesized;
135
+ };
136
+ const update = async (_id, spec) => upsert(spec);
137
+ const deleteRecord = async (id) => {
138
+ const payload = decodeId(id);
139
+ const set = {
140
+ Name: payload.n,
141
+ ResourceRecords: payload.v.map((Value) => ({ Value })),
142
+ Type: payload.t,
143
+ ...payload.l !== undefined ? { TTL: payload.l } : {}
144
+ };
145
+ try {
146
+ await submitChange({ Action: "DELETE", ResourceRecordSet: set });
147
+ } catch (error) {
148
+ const message = error instanceof Error ? error.message : String(error);
149
+ if (message.includes("not found") || message.includes("NoSuchHostedZone") || message.includes("but it was not found")) {
150
+ return;
151
+ }
152
+ throw error;
153
+ }
154
+ };
155
+ return {
156
+ create,
157
+ delete: deleteRecord,
158
+ description: `route53 zone "${zoneLabel}"`,
159
+ find,
160
+ list,
161
+ update,
162
+ upsert
163
+ };
164
+ };
165
+ export {
166
+ route53DnsProvider
167
+ };
168
+
169
+ //# debugId=A9C653578AEC27D364756E2164756E21
170
+ //# sourceMappingURL=route53.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/route53.ts"],
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\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
+ ],
7
+ "mappings": "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",
8
+ "debugId": "A9C653578AEC27D364756E2164756E21",
9
+ "names": []
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/deploy",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Generic Bun-project deploy pipeline. A Target (localTarget / sshTarget) is anywhere you can exec + upload — DigitalOcean droplets, Linode, Hetzner, Vultr, your own boxes. Bundled pipeline: prepare → upload → install → build → link → restart → verify. Atomic symlink swap, release history, prune, hooks. SSH shells out to system ssh/rsync — zero ssh2 deps.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "release"
27
27
  ],
28
28
  "scripts": {
29
- "build": "rm -rf dist && bun build src/index.ts src/digitalocean.ts src/hetzner.ts src/linode.ts src/vultr.ts src/dns.ts src/cloudflare.ts src/digitaloceanDns.ts src/hetznerDns.ts src/tls.ts src/env.ts --outdir dist --root src --sourcemap --target=bun && tsc --project tsconfig.build.json",
29
+ "build": "rm -rf dist && bun build src/index.ts src/digitalocean.ts src/hetzner.ts src/linode.ts src/vultr.ts src/dns.ts src/cloudflare.ts src/digitaloceanDns.ts src/hetznerDns.ts src/route53.ts src/tls.ts src/env.ts --outdir dist --root src --sourcemap --target=bun && tsc --project tsconfig.build.json",
30
30
  "test": "bun test tests/",
31
31
  "typecheck": "tsc --noEmit",
32
32
  "format": "prettier --write \"./**/*.{ts,json,md}\"",
@@ -96,6 +96,11 @@
96
96
  "types": "./dist/hetznerDns.d.ts",
97
97
  "import": "./dist/hetznerDns.js",
98
98
  "default": "./dist/hetznerDns.js"
99
+ },
100
+ "./route53": {
101
+ "types": "./dist/route53.d.ts",
102
+ "import": "./dist/route53.js",
103
+ "default": "./dist/route53.js"
99
104
  }
100
105
  },
101
106
  "files": ["dist", "README.md"]