doomain 0.1.20 → 0.1.22
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 +58 -1
- package/bin/dev.js +3 -1
- package/bin/ensure-current-manifest.js +18 -0
- package/bin/run.js +4 -1
- package/dist/commands/dns/delete.d.ts +1 -0
- package/dist/commands/dns/delete.js +1 -0
- package/dist/commands/dns/diagnose.d.ts +16 -0
- package/dist/commands/dns/diagnose.js +49 -0
- package/dist/commands/dns/point.js +17 -0
- package/dist/commands/dns/remove.d.ts +18 -0
- package/dist/commands/dns/remove.js +77 -0
- package/dist/commands/providers/status.js +1 -2
- package/dist/commands/update.d.ts +9 -0
- package/dist/commands/update.js +28 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/commands/upgrade.js +1 -0
- package/dist/lib/command-schema.d.ts +1 -1
- package/dist/lib/command-schema.js +76 -1
- package/dist/lib/diagnose-dns.d.ts +53 -0
- package/dist/lib/diagnose-dns.js +156 -0
- package/dist/lib/dns-propagation.d.ts +45 -0
- package/dist/lib/dns-propagation.js +114 -0
- package/dist/lib/dns-reconciliation.d.ts +31 -0
- package/dist/lib/dns-reconciliation.js +53 -0
- package/dist/lib/dns-records.d.ts +18 -0
- package/dist/lib/dns-records.js +73 -0
- package/dist/lib/domain-provider.js +1 -1
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/point-domain.d.ts +6 -3
- package/dist/lib/point-domain.js +36 -70
- package/dist/lib/providers/hostinger/index.d.ts +1 -0
- package/dist/lib/providers/hostinger/index.js +46 -10
- package/dist/lib/providers/spaceship/index.js +16 -7
- package/dist/lib/providers/status.d.ts +3 -0
- package/dist/lib/providers/status.js +2 -0
- package/dist/lib/remove-domain.d.ts +38 -0
- package/dist/lib/remove-domain.js +125 -0
- package/dist/lib/self-update.d.ts +27 -0
- package/dist/lib/self-update.js +63 -0
- package/oclif.manifest.json +306 -1
- package/package.json +3 -2
package/dist/lib/point-domain.js
CHANGED
|
@@ -1,87 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { waitForDnsPropagation } from './dns-propagation.js';
|
|
2
|
+
import { reconcileDesiredRecord } from './dns-reconciliation.js';
|
|
3
|
+
import { inferAddressRecordType, normalizeAddressRecordTarget } from './dns-records.js';
|
|
3
4
|
import { resolveProviderTarget } from './domain-provider.js';
|
|
4
5
|
import { DoomainError } from './errors.js';
|
|
5
6
|
import { withProviderRecordOptions } from './link-domain.js';
|
|
6
7
|
import { createProvider } from './providers/registry.js';
|
|
7
|
-
import { normalizeDomain } from './validate.js';
|
|
8
8
|
const defaultDependencies = {
|
|
9
9
|
createProvider,
|
|
10
10
|
resolveTarget: resolveProviderTarget,
|
|
11
11
|
};
|
|
12
|
-
function cleanTarget(value) {
|
|
13
|
-
return value.trim().replace(/\.$/, '');
|
|
14
|
-
}
|
|
15
|
-
function inferredRecordType(target) {
|
|
16
|
-
const version = isIP(target);
|
|
17
|
-
if (version === 4)
|
|
18
|
-
return 'A';
|
|
19
|
-
if (version === 6)
|
|
20
|
-
return 'AAAA';
|
|
21
|
-
return 'CNAME';
|
|
22
|
-
}
|
|
23
|
-
function validateTarget(recordType, target) {
|
|
24
|
-
const version = isIP(target);
|
|
25
|
-
if (recordType === 'A' && version !== 4)
|
|
26
|
-
throw new DoomainError('INVALID_INPUT', 'A records require an IPv4 target.');
|
|
27
|
-
if (recordType === 'AAAA' && version !== 6)
|
|
28
|
-
throw new DoomainError('INVALID_INPUT', 'AAAA records require an IPv6 target.');
|
|
29
|
-
if (recordType === 'CNAME' && version !== 0)
|
|
30
|
-
throw new DoomainError('INVALID_INPUT', 'CNAME records require a hostname target.');
|
|
31
|
-
}
|
|
32
12
|
export function createPointRecord(input) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
throw new DoomainError('MISSING_ARGUMENT', 'A DNS target is required.');
|
|
36
|
-
const recordType = input.recordType ?? inferredRecordType(target);
|
|
37
|
-
validateTarget(recordType, target);
|
|
13
|
+
const recordType = input.recordType ?? inferAddressRecordType(input.target.trim());
|
|
14
|
+
const target = normalizeAddressRecordTarget(recordType, input.target);
|
|
38
15
|
return withProviderRecordOptions(input.provider, {
|
|
39
16
|
name: input.recordName,
|
|
40
17
|
ttl: input.ttl ?? 300,
|
|
41
18
|
type: recordType,
|
|
42
|
-
value:
|
|
19
|
+
value: target,
|
|
43
20
|
});
|
|
44
21
|
}
|
|
45
22
|
function recordFqdn(record, zoneDomain) {
|
|
46
23
|
return record.name === '@' ? zoneDomain : `${record.name}.${zoneDomain}`;
|
|
47
24
|
}
|
|
48
|
-
function cleanDnsValue(value) {
|
|
49
|
-
return value.toLowerCase().replace(/\.$/, '');
|
|
50
|
-
}
|
|
51
|
-
function normalizeIpv6(value) {
|
|
52
|
-
return new URL(`http://[${value}]`).hostname.slice(1, -1);
|
|
53
|
-
}
|
|
54
|
-
async function isPropagated(record, zoneDomain, dependencies) {
|
|
55
|
-
const fqdn = recordFqdn(record, zoneDomain);
|
|
56
|
-
try {
|
|
57
|
-
if (record.type === 'A')
|
|
58
|
-
return (await (dependencies.resolve4 ?? resolve4)(fqdn)).includes(record.value);
|
|
59
|
-
if (record.type === 'AAAA') {
|
|
60
|
-
const expected = normalizeIpv6(record.value);
|
|
61
|
-
return (await (dependencies.resolve6 ?? resolve6)(fqdn)).some((value) => normalizeIpv6(value) === expected);
|
|
62
|
-
}
|
|
63
|
-
if (record.type === 'CNAME') {
|
|
64
|
-
return (await (dependencies.resolveCname ?? resolveCname)(fqdn))
|
|
65
|
-
.map(cleanDnsValue)
|
|
66
|
-
.includes(cleanDnsValue(record.value));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
async function waitForPropagation(record, zoneDomain, timeoutSeconds, dependencies) {
|
|
75
|
-
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
76
|
-
while (true) {
|
|
77
|
-
if (await isPropagated(record, zoneDomain, dependencies))
|
|
78
|
-
return true;
|
|
79
|
-
const remaining = deadline - Date.now();
|
|
80
|
-
if (remaining <= 0)
|
|
81
|
-
return false;
|
|
82
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(5000, remaining)));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
25
|
function conflictWarning(resolved, provider, record, conflicts) {
|
|
86
26
|
return {
|
|
87
27
|
account: resolved.account,
|
|
@@ -170,6 +110,14 @@ export async function pointDomain(input, dependencies = defaultDependencies) {
|
|
|
170
110
|
provider: resolved.provider,
|
|
171
111
|
providerInferred: resolved.providerInferred,
|
|
172
112
|
propagated: false,
|
|
113
|
+
propagation: {
|
|
114
|
+
elapsedMs: 0,
|
|
115
|
+
expected: record.value,
|
|
116
|
+
observations: [],
|
|
117
|
+
status: 'not_checked',
|
|
118
|
+
},
|
|
119
|
+
reconciled: false,
|
|
120
|
+
reconciliationAttempts: 0,
|
|
173
121
|
record,
|
|
174
122
|
skipped: [],
|
|
175
123
|
updated: false,
|
|
@@ -191,10 +139,25 @@ export async function pointDomain(input, dependencies = defaultDependencies) {
|
|
|
191
139
|
}
|
|
192
140
|
input.progress?.(`Pointing ${resolved.target.fullDomain} to ${record.value}`);
|
|
193
141
|
const result = await provider.applyChanges(zone, plan, { force });
|
|
142
|
+
const reconciliation = await reconcileDesiredRecord({
|
|
143
|
+
desired: record,
|
|
144
|
+
progress: input.progress,
|
|
145
|
+
provider,
|
|
146
|
+
timeoutMs: (input.reconcileTimeoutSeconds ?? 30) * 1000,
|
|
147
|
+
zone,
|
|
148
|
+
});
|
|
194
149
|
const shouldWait = input.wait ?? true;
|
|
195
|
-
const
|
|
196
|
-
? await
|
|
197
|
-
|
|
150
|
+
const propagation = shouldWait
|
|
151
|
+
? await waitForDnsPropagation({
|
|
152
|
+
fqdn: recordFqdn(record, resolved.target.zoneDomain),
|
|
153
|
+
observe: dependencies.observeDns,
|
|
154
|
+
record,
|
|
155
|
+
timeoutSeconds: input.timeoutSeconds ?? 300,
|
|
156
|
+
})
|
|
157
|
+
: { elapsedMs: 0, expected: record.value, observations: [], status: 'not_checked' };
|
|
158
|
+
const propagated = propagation.status === 'propagated' ||
|
|
159
|
+
propagation.status === 'local_or_vpn_cache_stale' ||
|
|
160
|
+
propagation.status === 'system_resolver_unavailable';
|
|
198
161
|
return {
|
|
199
162
|
account: resolved.account,
|
|
200
163
|
accountInferred: resolved.accountInferred,
|
|
@@ -204,9 +167,12 @@ export async function pointDomain(input, dependencies = defaultDependencies) {
|
|
|
204
167
|
provider: resolved.provider,
|
|
205
168
|
providerInferred: resolved.providerInferred,
|
|
206
169
|
propagated,
|
|
170
|
+
propagation,
|
|
171
|
+
reconciled: reconciliation.reconciled,
|
|
172
|
+
reconciliationAttempts: reconciliation.attempts,
|
|
207
173
|
record,
|
|
208
174
|
skipped: result.skipped,
|
|
209
|
-
updated: result.applied.length > 0,
|
|
175
|
+
updated: result.applied.length > 0 || reconciliation.appliedChanges > 0,
|
|
210
176
|
zoneDomain: resolved.target.zoneDomain,
|
|
211
177
|
};
|
|
212
178
|
}
|
|
@@ -19,5 +19,6 @@ export declare class HostingerProvider implements DnsProvider {
|
|
|
19
19
|
upsertRecord(zone: DnsZone, record: DnsRecordInput): Promise<DnsRecord>;
|
|
20
20
|
deleteRecord(zone: DnsZone, record: DnsRecord): Promise<void>;
|
|
21
21
|
private putRecords;
|
|
22
|
+
private putHostingerRecordSets;
|
|
22
23
|
}
|
|
23
24
|
export declare const hostingerProviderDefinition: DnsProviderDefinition;
|
|
@@ -47,7 +47,7 @@ function toDnsRecords(record, zone) {
|
|
|
47
47
|
return [];
|
|
48
48
|
return [
|
|
49
49
|
{
|
|
50
|
-
metadata: { hostinger:
|
|
50
|
+
metadata: { hostinger: record },
|
|
51
51
|
name,
|
|
52
52
|
ttl: record.ttl,
|
|
53
53
|
type,
|
|
@@ -56,12 +56,13 @@ function toDnsRecords(record, zone) {
|
|
|
56
56
|
];
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
-
function
|
|
59
|
+
function toHostingerRecordSet(records) {
|
|
60
|
+
const first = records[0];
|
|
60
61
|
return {
|
|
61
|
-
name:
|
|
62
|
-
records:
|
|
63
|
-
ttl:
|
|
64
|
-
type:
|
|
62
|
+
name: first.name,
|
|
63
|
+
records: records.map((record) => ({ content: record.value })),
|
|
64
|
+
ttl: first.ttl ?? capabilities.defaultTtl,
|
|
65
|
+
type: first.type,
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
68
|
function deleteFilter(record) {
|
|
@@ -142,15 +143,40 @@ export class HostingerProvider {
|
|
|
142
143
|
assertNoConflicts(this.id, plan);
|
|
143
144
|
const applied = [];
|
|
144
145
|
const skipped = [];
|
|
146
|
+
const deletionSets = new Map();
|
|
147
|
+
for (const change of plan.changes) {
|
|
148
|
+
if (change.action !== 'delete')
|
|
149
|
+
continue;
|
|
150
|
+
const key = `${change.existing.type}\0${change.existing.name}`;
|
|
151
|
+
const records = deletionSets.get(key) ?? [];
|
|
152
|
+
records.push(change.existing);
|
|
153
|
+
deletionSets.set(key, records);
|
|
154
|
+
}
|
|
155
|
+
for (const deleted of deletionSets.values()) {
|
|
156
|
+
const sample = deleted[0];
|
|
157
|
+
const source = sample.metadata?.hostinger;
|
|
158
|
+
const deletedValues = new Set(deleted.map((record) => record.value));
|
|
159
|
+
const sourceRecords = source?.records?.filter((record) => record.is_disabled || !record.content || !deletedValues.has(record.content));
|
|
160
|
+
if (source && sourceRecords && sourceRecords.length > 0) {
|
|
161
|
+
await this.putHostingerRecordSets(zone, [{ ...source, records: sourceRecords }], true);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const remaining = plan.existing.filter((record) => sameRecordSet(record, sample) && !deleted.includes(record));
|
|
165
|
+
if (remaining.length > 0)
|
|
166
|
+
await this.putRecords(zone, remaining, true);
|
|
167
|
+
else
|
|
168
|
+
await this.deleteRecord(zone, sample);
|
|
169
|
+
}
|
|
170
|
+
applied.push(...plan.changes.filter((change) => change.action === 'delete' && deleted.includes(change.existing)));
|
|
171
|
+
}
|
|
145
172
|
for (const change of plan.changes) {
|
|
146
173
|
if (change.action === 'skip') {
|
|
147
174
|
skipped.push(change.record);
|
|
148
175
|
continue;
|
|
149
176
|
}
|
|
150
177
|
if (change.action === 'delete')
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
await this.putRecords(zone, [change.record], change.action === 'update');
|
|
178
|
+
continue;
|
|
179
|
+
await this.putRecords(zone, [change.record], change.action === 'update');
|
|
154
180
|
applied.push(change);
|
|
155
181
|
}
|
|
156
182
|
return { applied, skipped };
|
|
@@ -166,8 +192,18 @@ export class HostingerProvider {
|
|
|
166
192
|
});
|
|
167
193
|
}
|
|
168
194
|
async putRecords(zone, records, overwrite) {
|
|
195
|
+
const recordSets = new Map();
|
|
196
|
+
for (const record of records) {
|
|
197
|
+
const key = `${record.type}\0${record.name}`;
|
|
198
|
+
const values = recordSets.get(key) ?? [];
|
|
199
|
+
values.push(record);
|
|
200
|
+
recordSets.set(key, values);
|
|
201
|
+
}
|
|
202
|
+
await this.putHostingerRecordSets(zone, [...recordSets.values()].map(toHostingerRecordSet), overwrite);
|
|
203
|
+
}
|
|
204
|
+
async putHostingerRecordSets(zone, recordSets, overwrite) {
|
|
169
205
|
await this.http.request(`/api/dns/v1/zones/${encodeURIComponent(zone.name)}`, {
|
|
170
|
-
body: { overwrite, zone:
|
|
206
|
+
body: { overwrite, zone: recordSets },
|
|
171
207
|
method: 'PUT',
|
|
172
208
|
});
|
|
173
209
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { normalizeDomain } from '../../validate.js';
|
|
2
2
|
import { createProviderHttpClient } from '../core/http.js';
|
|
3
3
|
import { paginateBySkip } from '../core/pagination.js';
|
|
4
|
-
import {
|
|
4
|
+
import { assertNoConflicts, planDnsChanges } from '../core/planner.js';
|
|
5
5
|
const SPACESHIP_API_URL = 'https://spaceship.dev/api/v1';
|
|
6
6
|
const capabilities = {
|
|
7
7
|
defaultTtl: 3600,
|
|
@@ -108,12 +108,21 @@ export class SpaceshipProvider {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
async applyChanges(zone, plan) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
assertNoConflicts(this.id, plan);
|
|
112
|
+
const skipped = plan.changes.flatMap((change) => (change.action === 'skip' ? [change.record] : []));
|
|
113
|
+
const applied = plan.changes.filter((change) => change.action !== 'skip');
|
|
114
|
+
// Spaceship records do not have stable ids. Delete every old value before creating
|
|
115
|
+
// replacements so an API that processes accepted writes out of order cannot leave
|
|
116
|
+
// two address records in the same slot without the reconciler noticing and retrying.
|
|
117
|
+
for (const change of applied) {
|
|
118
|
+
if (change.action === 'delete' || change.action === 'update')
|
|
119
|
+
await this.deleteRecord(zone, change.existing);
|
|
120
|
+
}
|
|
121
|
+
for (const change of applied) {
|
|
122
|
+
if (change.action === 'create' || change.action === 'update')
|
|
123
|
+
await this.upsertRecord(zone, change.record);
|
|
124
|
+
}
|
|
125
|
+
return { applied, skipped };
|
|
117
126
|
}
|
|
118
127
|
async upsertRecord(zone, record) {
|
|
119
128
|
await this.http.request(`/dns/records/${zone.name}`, {
|
|
@@ -2,7 +2,9 @@ import { type DoomainConfig } from '../config.js';
|
|
|
2
2
|
import type { DnsProviderDefinition } from './types.js';
|
|
3
3
|
export interface ProviderStatus {
|
|
4
4
|
account: string;
|
|
5
|
+
accountLabel: string;
|
|
5
6
|
configured: boolean;
|
|
7
|
+
/** @deprecated Use isPreferredProvider. */
|
|
6
8
|
default: boolean;
|
|
7
9
|
displayName: string;
|
|
8
10
|
docsUrl?: string;
|
|
@@ -10,6 +12,7 @@ export interface ProviderStatus {
|
|
|
10
12
|
error?: string;
|
|
11
13
|
id: string;
|
|
12
14
|
isDefaultAccount: boolean;
|
|
15
|
+
isPreferredProvider: boolean;
|
|
13
16
|
verified?: boolean;
|
|
14
17
|
}
|
|
15
18
|
export declare function isProviderConfigured(definition: DnsProviderDefinition, config: DoomainConfig, opts?: {
|
|
@@ -17,12 +17,14 @@ export async function listProviderStatuses(opts = {}) {
|
|
|
17
17
|
const configured = accounts.some((item) => item.account === account);
|
|
18
18
|
const status = {
|
|
19
19
|
account,
|
|
20
|
+
accountLabel: ref.isDefaultAccount ? `${definition.id}/default` : `${definition.id}/${account}`,
|
|
20
21
|
configured,
|
|
21
22
|
default: config.defaults?.provider === definition.id,
|
|
22
23
|
displayName: definition.displayName,
|
|
23
24
|
docsUrl: definition.docsUrl,
|
|
24
25
|
id: definition.id,
|
|
25
26
|
isDefaultAccount: ref.isDefaultAccount,
|
|
27
|
+
isPreferredProvider: config.defaults?.provider === definition.id,
|
|
26
28
|
};
|
|
27
29
|
if (configured && opts.verify) {
|
|
28
30
|
try {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type DnsRecordSelector } from './dns-records.js';
|
|
2
|
+
import { type ResolvedDnsTarget } from './domain-provider.js';
|
|
3
|
+
import type { DnsProvider, DnsRecord, DnsRecordType } from './providers/types.js';
|
|
4
|
+
export interface RemoveDomainInput {
|
|
5
|
+
account?: string;
|
|
6
|
+
allMatching?: boolean;
|
|
7
|
+
domain: string;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
provider?: string;
|
|
10
|
+
recordType: DnsRecordType;
|
|
11
|
+
reconcileTimeoutSeconds?: number;
|
|
12
|
+
value?: string;
|
|
13
|
+
confirmMultiple?: (records: DnsRecord[]) => Promise<boolean>;
|
|
14
|
+
progress?: (message: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface RemoveDomainResult {
|
|
17
|
+
account: string;
|
|
18
|
+
accountInferred: boolean;
|
|
19
|
+
domain: string;
|
|
20
|
+
dryRun: boolean;
|
|
21
|
+
isDefaultAccount: boolean;
|
|
22
|
+
matched: DnsRecord[];
|
|
23
|
+
provider: string;
|
|
24
|
+
providerInferred: boolean;
|
|
25
|
+
reconciled: boolean;
|
|
26
|
+
reconciliationAttempts: number;
|
|
27
|
+
removed: number;
|
|
28
|
+
selector: DnsRecordSelector;
|
|
29
|
+
zoneDomain: string;
|
|
30
|
+
}
|
|
31
|
+
interface RemoveDomainDependencies {
|
|
32
|
+
createProvider: (provider: string, opts: {
|
|
33
|
+
account?: string;
|
|
34
|
+
}) => Promise<DnsProvider>;
|
|
35
|
+
resolveTarget: (input: Pick<RemoveDomainInput, 'account' | 'domain' | 'provider'>) => Promise<ResolvedDnsTarget>;
|
|
36
|
+
}
|
|
37
|
+
export declare function removeDomain(input: RemoveDomainInput, dependencies?: RemoveDomainDependencies): Promise<RemoveDomainResult>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { reconcileRecordRemoval } from './dns-reconciliation.js';
|
|
2
|
+
import { recordMatchesSelector } from './dns-records.js';
|
|
3
|
+
import { resolveProviderTarget } from './domain-provider.js';
|
|
4
|
+
import { DoomainError } from './errors.js';
|
|
5
|
+
import { createProvider } from './providers/registry.js';
|
|
6
|
+
const defaultDependencies = { createProvider, resolveTarget: resolveProviderTarget };
|
|
7
|
+
function quoteCommandArgument(value) {
|
|
8
|
+
return /^[a-zA-Z0-9_./:@+-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
9
|
+
}
|
|
10
|
+
function removalCommand(input, options) {
|
|
11
|
+
const arguments_ = ['doomain', 'dns', 'remove', quoteCommandArgument(input.domain)];
|
|
12
|
+
if (input.provider)
|
|
13
|
+
arguments_.push('--provider', quoteCommandArgument(input.provider));
|
|
14
|
+
if (input.account)
|
|
15
|
+
arguments_.push('--account', quoteCommandArgument(input.account));
|
|
16
|
+
arguments_.push('--type', input.recordType);
|
|
17
|
+
if (input.value !== undefined)
|
|
18
|
+
arguments_.push('--value', quoteCommandArgument(input.value));
|
|
19
|
+
if (options?.allMatching ?? input.allMatching)
|
|
20
|
+
arguments_.push('--all-matching');
|
|
21
|
+
if (options?.dryRun ?? input.dryRun)
|
|
22
|
+
arguments_.push('--dry-run');
|
|
23
|
+
arguments_.push('--json');
|
|
24
|
+
return arguments_.join(' ');
|
|
25
|
+
}
|
|
26
|
+
function removalResolutionError(error, input) {
|
|
27
|
+
if (error.code !== 'CONFIG_NOT_FOUND' && error.code !== 'PROVIDER_ZONE_NOT_FOUND')
|
|
28
|
+
return error;
|
|
29
|
+
const details = error.details && typeof error.details === 'object' ? error.details : {};
|
|
30
|
+
const retry = removalCommand(input);
|
|
31
|
+
return new DoomainError(error.code, error.message, {
|
|
32
|
+
...details,
|
|
33
|
+
recovery: `Connect or repair the DNS provider account that owns this domain, then retry \`${retry}\`.`,
|
|
34
|
+
suggestedCommands: ['doomain providers connect', retry],
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function ambiguousDeletionError(input, records) {
|
|
38
|
+
return new DoomainError('DNS_DELETE_AMBIGUOUS', `${records.length} DNS records match this deletion. Pass --all-matching to delete all of them.`, {
|
|
39
|
+
matched: records,
|
|
40
|
+
recovery: 'Narrow the deletion with --value, or explicitly approve all matching records with --all-matching.',
|
|
41
|
+
suggestedCommands: [removalCommand(input, { allMatching: true, dryRun: true })],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export async function removeDomain(input, dependencies = defaultDependencies) {
|
|
45
|
+
if (!input.allMatching && input.value === undefined) {
|
|
46
|
+
throw new DoomainError('INVALID_INPUT', 'An exact --value is required unless --all-matching is passed.', {
|
|
47
|
+
recovery: 'Pass --value <expected-value>, or preview --all-matching with --dry-run.',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
let resolved;
|
|
51
|
+
try {
|
|
52
|
+
resolved = await dependencies.resolveTarget(input);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof DoomainError)
|
|
56
|
+
throw removalResolutionError(error, input);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const provider = await dependencies.createProvider(resolved.provider, { account: resolved.account });
|
|
60
|
+
if (!provider.capabilities.recordTypes.includes(input.recordType)) {
|
|
61
|
+
throw new DoomainError('PROVIDER_UNSUPPORTED_RECORD', `${provider.name} does not support ${input.recordType} records.`);
|
|
62
|
+
}
|
|
63
|
+
const zone = await provider.getZone(resolved.target.zoneDomain);
|
|
64
|
+
if (!zone) {
|
|
65
|
+
throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', `${provider.name} does not have a DNS zone for ${resolved.target.zoneDomain}.`);
|
|
66
|
+
}
|
|
67
|
+
const selector = {
|
|
68
|
+
name: resolved.target.recordName,
|
|
69
|
+
type: input.recordType,
|
|
70
|
+
...(input.value === undefined ? {} : { value: input.value }),
|
|
71
|
+
};
|
|
72
|
+
const existing = await provider.listRecords(zone);
|
|
73
|
+
const matched = existing.filter((record) => recordMatchesSelector(record, selector));
|
|
74
|
+
if (matched.length > 1 && !input.allMatching) {
|
|
75
|
+
const confirmed = (await input.confirmMultiple?.(matched)) === true;
|
|
76
|
+
if (!confirmed)
|
|
77
|
+
throw ambiguousDeletionError(input, matched);
|
|
78
|
+
}
|
|
79
|
+
const base = {
|
|
80
|
+
account: resolved.account,
|
|
81
|
+
accountInferred: resolved.accountInferred,
|
|
82
|
+
domain: resolved.target.fullDomain,
|
|
83
|
+
isDefaultAccount: resolved.isDefaultAccount,
|
|
84
|
+
matched,
|
|
85
|
+
provider: resolved.provider,
|
|
86
|
+
providerInferred: resolved.providerInferred,
|
|
87
|
+
selector,
|
|
88
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
89
|
+
};
|
|
90
|
+
if (input.dryRun) {
|
|
91
|
+
return {
|
|
92
|
+
...base,
|
|
93
|
+
dryRun: true,
|
|
94
|
+
reconciled: false,
|
|
95
|
+
reconciliationAttempts: 0,
|
|
96
|
+
removed: 0,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (matched.length === 0) {
|
|
100
|
+
return { ...base, dryRun: false, reconciled: true, reconciliationAttempts: 1, removed: 0 };
|
|
101
|
+
}
|
|
102
|
+
input.progress?.(`Deleting ${matched.length} DNS record${matched.length === 1 ? '' : 's'}`);
|
|
103
|
+
const plan = {
|
|
104
|
+
changes: matched.map((record) => ({ action: 'delete', existing: record })),
|
|
105
|
+
conflicts: [],
|
|
106
|
+
desired: [],
|
|
107
|
+
existing,
|
|
108
|
+
zone,
|
|
109
|
+
};
|
|
110
|
+
await provider.applyChanges(zone, plan, { force: true });
|
|
111
|
+
const reconciliation = await reconcileRecordRemoval({
|
|
112
|
+
progress: input.progress,
|
|
113
|
+
provider,
|
|
114
|
+
selector,
|
|
115
|
+
timeoutMs: (input.reconcileTimeoutSeconds ?? 30) * 1000,
|
|
116
|
+
zone,
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
...base,
|
|
120
|
+
dryRun: false,
|
|
121
|
+
reconciled: reconciliation.reconciled,
|
|
122
|
+
reconciliationAttempts: reconciliation.attempts,
|
|
123
|
+
removed: matched.length,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
interface ProcessResult {
|
|
2
|
+
exitCode: number | null;
|
|
3
|
+
stderr: string;
|
|
4
|
+
stdout: string;
|
|
5
|
+
}
|
|
6
|
+
interface ProcessOptions {
|
|
7
|
+
env: NodeJS.ProcessEnv;
|
|
8
|
+
shell: boolean;
|
|
9
|
+
}
|
|
10
|
+
type ProcessRunner = (command: string, args: string[], options: ProcessOptions) => Promise<ProcessResult>;
|
|
11
|
+
export interface SelfUpdateResult {
|
|
12
|
+
package: string;
|
|
13
|
+
packageSpec: string;
|
|
14
|
+
packageManager: 'npm';
|
|
15
|
+
}
|
|
16
|
+
export interface SelfUpdateOptions {
|
|
17
|
+
cacheRoot?: string;
|
|
18
|
+
platform?: NodeJS.Platform;
|
|
19
|
+
runner?: ProcessRunner;
|
|
20
|
+
}
|
|
21
|
+
export declare function npmInstallCommand(cacheDirectory: string, platform?: NodeJS.Platform, environment?: NodeJS.ProcessEnv): {
|
|
22
|
+
args: string[];
|
|
23
|
+
command: string;
|
|
24
|
+
options: ProcessOptions;
|
|
25
|
+
};
|
|
26
|
+
export declare function installLatestVersion(options?: SelfUpdateOptions): Promise<SelfUpdateResult>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { DoomainError } from './errors.js';
|
|
6
|
+
function runProcess(command, args, options) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn(command, args, {
|
|
9
|
+
...options,
|
|
10
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
11
|
+
windowsHide: true,
|
|
12
|
+
});
|
|
13
|
+
let stderr = '';
|
|
14
|
+
let stdout = '';
|
|
15
|
+
child.stderr.setEncoding('utf8');
|
|
16
|
+
child.stderr.on('data', (chunk) => {
|
|
17
|
+
stderr += chunk;
|
|
18
|
+
});
|
|
19
|
+
child.stdout.setEncoding('utf8');
|
|
20
|
+
child.stdout.on('data', (chunk) => {
|
|
21
|
+
stdout += chunk;
|
|
22
|
+
});
|
|
23
|
+
child.once('error', reject);
|
|
24
|
+
child.once('close', (exitCode) => resolve({ exitCode, stderr, stdout }));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function npmInstallCommand(cacheDirectory, platform = process.platform, environment = process.env) {
|
|
28
|
+
const env = Object.fromEntries(Object.entries(environment).filter(([key]) => key.toLowerCase() !== 'npm_config_cache'));
|
|
29
|
+
env.npm_config_cache = cacheDirectory;
|
|
30
|
+
return {
|
|
31
|
+
command: 'npm',
|
|
32
|
+
args: ['install', '--global', 'doomain@latest', '--prefer-online', '--offline=false'],
|
|
33
|
+
options: {
|
|
34
|
+
env,
|
|
35
|
+
shell: platform === 'win32',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function installLatestVersion(options = {}) {
|
|
40
|
+
const cacheDirectory = await mkdtemp(join(options.cacheRoot ?? tmpdir(), 'doomain-npm-cache-'));
|
|
41
|
+
try {
|
|
42
|
+
const invocation = npmInstallCommand(cacheDirectory, options.platform);
|
|
43
|
+
const result = await (options.runner ?? runProcess)(invocation.command, invocation.args, invocation.options);
|
|
44
|
+
if (result.exitCode !== 0) {
|
|
45
|
+
const reason = result.stderr.trim() || result.stdout.trim() || `npm exited with code ${result.exitCode ?? 'unknown'}`;
|
|
46
|
+
throw new DoomainError('SELF_UPDATE_FAILED', `Unable to update doomain: ${reason}`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
package: 'doomain',
|
|
50
|
+
packageSpec: 'doomain@latest',
|
|
51
|
+
packageManager: 'npm',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof DoomainError)
|
|
56
|
+
throw error;
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
throw new DoomainError('SELF_UPDATE_FAILED', `Unable to update doomain: ${message}`);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await rm(cacheDirectory, { force: true, recursive: true }).catch(() => undefined);
|
|
62
|
+
}
|
|
63
|
+
}
|