doomain 0.1.21 → 0.1.23

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.
Files changed (37) hide show
  1. package/README.md +59 -2
  2. package/bin/dev.js +3 -1
  3. package/bin/ensure-current-manifest.js +18 -0
  4. package/bin/run.js +4 -1
  5. package/dist/commands/dns/delete.d.ts +1 -0
  6. package/dist/commands/dns/delete.js +1 -0
  7. package/dist/commands/dns/diagnose.d.ts +16 -0
  8. package/dist/commands/dns/diagnose.js +49 -0
  9. package/dist/commands/dns/point.js +17 -0
  10. package/dist/commands/dns/remove.d.ts +18 -0
  11. package/dist/commands/dns/remove.js +77 -0
  12. package/dist/commands/providers/status.js +1 -2
  13. package/dist/commands/version.d.ts +5 -0
  14. package/dist/commands/version.js +8 -0
  15. package/dist/lib/command-schema.d.ts +1 -1
  16. package/dist/lib/command-schema.js +69 -1
  17. package/dist/lib/diagnose-dns.d.ts +53 -0
  18. package/dist/lib/diagnose-dns.js +156 -0
  19. package/dist/lib/dns-propagation.d.ts +45 -0
  20. package/dist/lib/dns-propagation.js +114 -0
  21. package/dist/lib/dns-reconciliation.d.ts +31 -0
  22. package/dist/lib/dns-reconciliation.js +53 -0
  23. package/dist/lib/dns-records.d.ts +18 -0
  24. package/dist/lib/dns-records.js +73 -0
  25. package/dist/lib/domain-provider.js +1 -1
  26. package/dist/lib/errors.d.ts +1 -1
  27. package/dist/lib/point-domain.d.ts +6 -3
  28. package/dist/lib/point-domain.js +36 -70
  29. package/dist/lib/providers/hostinger/index.d.ts +1 -0
  30. package/dist/lib/providers/hostinger/index.js +46 -10
  31. package/dist/lib/providers/spaceship/index.js +16 -7
  32. package/dist/lib/providers/status.d.ts +3 -0
  33. package/dist/lib/providers/status.js +2 -0
  34. package/dist/lib/remove-domain.d.ts +38 -0
  35. package/dist/lib/remove-domain.js +125 -0
  36. package/oclif.manifest.json +264 -1
  37. package/package.json +9 -2
@@ -0,0 +1,156 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { getServers } from 'node:dns';
3
+ import { networkInterfaces, platform } from 'node:os';
4
+ import { promisify } from 'node:util';
5
+ import { classifyDnsPropagation, isNegativeDnsObservation, observeDnsRecord, } from './dns-propagation.js';
6
+ import { desiredSlotPostcondition, dnsRecordNamesEqual, inferAddressRecordType, normalizeAddressRecordTarget, normalizeDnsValue, } from './dns-records.js';
7
+ import { resolveProviderTarget } from './domain-provider.js';
8
+ import { DoomainError } from './errors.js';
9
+ import { createProvider } from './providers/registry.js';
10
+ const execFileAsync = promisify(execFile);
11
+ async function readMacOsResolvers() {
12
+ if (platform() !== 'darwin')
13
+ return [];
14
+ try {
15
+ const { stdout } = await execFileAsync('scutil', ['--dns'], { maxBuffer: 1024 * 1024 });
16
+ return String(stdout)
17
+ .split(/\n(?=resolver #\d+)/)
18
+ .flatMap((block) => {
19
+ const resolver = block.match(/resolver #(\d+)/)?.[1];
20
+ if (!resolver)
21
+ return [];
22
+ const interfaceMatch = block.match(/if_index\s*:\s*(\d+)(?:\s*\(([^)]+)\))?/);
23
+ const nameservers = [...block.matchAll(/nameserver\[\d+\]\s*:\s*(\S+)/g)].map((match) => match[1]);
24
+ const flags = block.match(/flags\s*:\s*(.+)/)?.[1]?.trim();
25
+ return [
26
+ {
27
+ ...(flags ? { flags } : {}),
28
+ ...(interfaceMatch ? { interfaceIndex: Number(interfaceMatch[1]) } : {}),
29
+ ...(interfaceMatch?.[2] ? { interfaceName: interfaceMatch[2] } : {}),
30
+ nameservers,
31
+ resolver: Number(resolver),
32
+ },
33
+ ];
34
+ });
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ }
40
+ const defaultDependencies = {
41
+ createProvider,
42
+ macOsResolvers: readMacOsResolvers,
43
+ observeDns: (fqdn, target, elapsedMs) => observeDnsRecord(fqdn, target, undefined, elapsedMs),
44
+ resolveTarget: (input) => resolveProviderTarget(input, { tolerateProviderAccountErrors: true }),
45
+ };
46
+ function recordConflicts(records) {
47
+ const conflicts = [];
48
+ for (const type of ['A', 'AAAA', 'CNAME']) {
49
+ const typed = records.filter((record) => record.type === type);
50
+ const values = new Set(typed.map((record) => normalizeDnsValue(record.value)));
51
+ if (values.size > 1)
52
+ conflicts.push({ reason: 'multiple_values', records: typed, type });
53
+ }
54
+ const cnames = records.filter((record) => record.type === 'CNAME');
55
+ const otherRecords = records.filter((record) => record.type !== 'CNAME');
56
+ if (cnames.length > 0 && otherRecords.length > 0) {
57
+ conflicts.push({ reason: 'cname_slot_conflict', records: [...cnames, ...otherRecords] });
58
+ }
59
+ return conflicts;
60
+ }
61
+ function activeInterfaceNames() {
62
+ return Object.entries(networkInterfaces())
63
+ .filter(([, addresses]) => addresses?.some((address) => !address.internal))
64
+ .map(([name]) => name)
65
+ .sort();
66
+ }
67
+ function diagnosisStatus(status) {
68
+ if (status === 'propagated')
69
+ return 'consistent';
70
+ if (status === 'local_or_vpn_cache_stale')
71
+ return status;
72
+ if (status === 'system_resolver_unavailable')
73
+ return status;
74
+ return 'public_propagation_pending';
75
+ }
76
+ function diagnosisResolutionError(error, input) {
77
+ if (error.code !== 'CONFIG_NOT_FOUND' && error.code !== 'PROVIDER_ZONE_NOT_FOUND')
78
+ return error;
79
+ const details = error.details && typeof error.details === 'object' ? error.details : {};
80
+ const provider = input.provider ? ` --provider ${input.provider}` : '';
81
+ const account = input.account ? ` --account ${input.account}` : '';
82
+ const type = input.recordType ? ` --type ${input.recordType}` : '';
83
+ const target = input.target ? ` --target ${input.target}` : '';
84
+ const retry = `doomain dns diagnose ${input.domain}${provider}${account}${type}${target} --json`;
85
+ return new DoomainError(error.code, error.message, {
86
+ ...details,
87
+ recovery: `Connect or repair the DNS provider account that owns this domain, then retry \`${retry}\`.`,
88
+ suggestedCommands: ['doomain providers connect', retry],
89
+ });
90
+ }
91
+ export async function diagnoseDns(input, dependencies = defaultDependencies) {
92
+ const recordType = input.recordType ?? (input.target ? inferAddressRecordType(input.target.trim()) : undefined) ?? 'A';
93
+ const requestedTarget = input.target === undefined ? undefined : normalizeAddressRecordTarget(recordType, input.target);
94
+ let resolved;
95
+ try {
96
+ resolved = await dependencies.resolveTarget(input);
97
+ }
98
+ catch (error) {
99
+ if (error instanceof DoomainError)
100
+ throw diagnosisResolutionError(error, input);
101
+ throw error;
102
+ }
103
+ const provider = await dependencies.createProvider(resolved.provider, { account: resolved.account });
104
+ const zone = await provider.getZone(resolved.target.zoneDomain);
105
+ if (!zone) {
106
+ throw diagnosisResolutionError(new DoomainError('PROVIDER_ZONE_NOT_FOUND', `${provider.name} does not have a DNS zone for ${resolved.target.zoneDomain}.`), input);
107
+ }
108
+ const allRecords = await provider.listRecords(zone);
109
+ const providerRecords = allRecords.filter((record) => dnsRecordNamesEqual(record.name, resolved.target.recordName));
110
+ const recordsOfType = providerRecords.filter((record) => record.type === recordType);
111
+ const expected = requestedTarget ?? (recordsOfType.length === 1 ? recordsOfType[0].value : undefined);
112
+ const queryTarget = { type: recordType, value: expected ?? recordsOfType[0]?.value ?? '' };
113
+ const observations = await dependencies.observeDns(resolved.target.fullDomain, queryTarget, 0);
114
+ const macOsResolvers = await dependencies.macOsResolvers?.();
115
+ let status;
116
+ if (expected) {
117
+ const postcondition = desiredSlotPostcondition(allRecords, {
118
+ name: resolved.target.recordName,
119
+ type: recordType,
120
+ value: expected,
121
+ });
122
+ status = postcondition.reconciled ? diagnosisStatus(classifyDnsPropagation(observations)) : 'provider_not_updated';
123
+ }
124
+ else {
125
+ const providerValues = new Set(recordsOfType.map((record) => normalizeDnsValue(record.value)));
126
+ const compared = observations.map((observation) => ({
127
+ ...observation,
128
+ matches: providerValues.size === 0
129
+ ? observation.answers.length === 0 && isNegativeDnsObservation(observation)
130
+ : observation.answers.length === providerValues.size &&
131
+ observation.answers.every((answer) => providerValues.has(normalizeDnsValue(answer.value))),
132
+ }));
133
+ observations.splice(0, observations.length, ...compared);
134
+ status = diagnosisStatus(classifyDnsPropagation(observations));
135
+ }
136
+ return {
137
+ account: resolved.account,
138
+ accountInferred: resolved.accountInferred,
139
+ conflicts: recordConflicts(providerRecords),
140
+ domain: resolved.target.fullDomain,
141
+ ...(expected === undefined ? {} : { expected }),
142
+ interfaces: activeInterfaceNames(),
143
+ isDefaultAccount: resolved.isDefaultAccount,
144
+ ...(macOsResolvers && macOsResolvers.length > 0 ? { macOsResolvers } : {}),
145
+ observations,
146
+ platform: platform(),
147
+ provider: resolved.provider,
148
+ providerInferred: resolved.providerInferred,
149
+ providerRecords,
150
+ recordType,
151
+ status,
152
+ systemResolverServers: getServers(),
153
+ warnings: resolved.warnings,
154
+ zoneDomain: resolved.target.zoneDomain,
155
+ };
156
+ }
@@ -0,0 +1,45 @@
1
+ import type { DnsRecordInput, DnsRecordType } from './providers/types.js';
2
+ export interface DnsAnswer {
3
+ ttl?: number | null;
4
+ ttlUnavailableReason?: string;
5
+ value: string;
6
+ }
7
+ export interface DnsResolverObservation {
8
+ answers: DnsAnswer[];
9
+ elapsedMs: number;
10
+ error?: string;
11
+ errorCode?: string;
12
+ expected?: string;
13
+ kind: 'public' | 'system';
14
+ matches?: boolean;
15
+ resolver: string;
16
+ servers: string[];
17
+ type: DnsRecordType;
18
+ }
19
+ export type DnsPropagationStatus = 'local_or_vpn_cache_stale' | 'not_checked' | 'propagated' | 'public_propagation_pending' | 'system_resolver_unavailable';
20
+ export interface DnsPropagationResult {
21
+ elapsedMs: number;
22
+ expected: string;
23
+ observations: DnsResolverObservation[];
24
+ status: DnsPropagationStatus;
25
+ timeoutReason?: string;
26
+ }
27
+ export interface DnsResolverSpec {
28
+ kind: 'public' | 'system';
29
+ name: string;
30
+ servers?: string[];
31
+ }
32
+ export declare const defaultDnsResolvers: DnsResolverSpec[];
33
+ export declare function isNegativeDnsObservation(observation: DnsResolverObservation): boolean;
34
+ type ResolveTarget = Pick<DnsRecordInput, 'type' | 'value'>;
35
+ export declare function observeDnsRecord(fqdn: string, target: ResolveTarget, specs?: DnsResolverSpec[], elapsedMs?: number): Promise<DnsResolverObservation[]>;
36
+ export declare function classifyDnsPropagation(observations: DnsResolverObservation[]): DnsPropagationStatus;
37
+ export declare function waitForDnsPropagation(input: {
38
+ fqdn: string;
39
+ record: ResolveTarget;
40
+ timeoutSeconds: number;
41
+ observe?: (fqdn: string, target: ResolveTarget, elapsedMs: number) => Promise<DnsResolverObservation[]>;
42
+ now?: () => number;
43
+ sleep?: (milliseconds: number) => Promise<void>;
44
+ }): Promise<DnsPropagationResult>;
45
+ export {};
@@ -0,0 +1,114 @@
1
+ import { getServers, Resolver, resolve4, resolve6, resolveCname } from 'node:dns/promises';
2
+ import { normalizeDnsValue } from './dns-records.js';
3
+ export const defaultDnsResolvers = [
4
+ { kind: 'system', name: 'system' },
5
+ { kind: 'public', name: 'cloudflare', servers: ['1.1.1.1', '1.0.0.1'] },
6
+ { kind: 'public', name: 'google', servers: ['8.8.8.8', '8.8.4.4'] },
7
+ ];
8
+ const negativeAnswerCodes = new Set(['ENODATA', 'ENOTFOUND']);
9
+ export function isNegativeDnsObservation(observation) {
10
+ return observation.errorCode !== undefined && negativeAnswerCodes.has(observation.errorCode);
11
+ }
12
+ function resolverFor(spec) {
13
+ if (!spec.servers)
14
+ return undefined;
15
+ const resolver = new Resolver();
16
+ resolver.setServers(spec.servers);
17
+ return resolver;
18
+ }
19
+ async function queryResolver(fqdn, type, resolver) {
20
+ if (type === 'A') {
21
+ const answers = resolver ? await resolver.resolve4(fqdn, { ttl: true }) : await resolve4(fqdn, { ttl: true });
22
+ return answers.map((answer) => ({ ttl: answer.ttl, value: answer.address }));
23
+ }
24
+ if (type === 'AAAA') {
25
+ const answers = resolver ? await resolver.resolve6(fqdn, { ttl: true }) : await resolve6(fqdn, { ttl: true });
26
+ return answers.map((answer) => ({ ttl: answer.ttl, value: answer.address }));
27
+ }
28
+ if (type === 'CNAME') {
29
+ const answers = resolver ? await resolver.resolveCname(fqdn) : await resolveCname(fqdn);
30
+ return answers.map((value) => ({
31
+ ttl: null,
32
+ ttlUnavailableReason: 'resolver_api_does_not_expose_cname_ttl',
33
+ value,
34
+ }));
35
+ }
36
+ throw new Error(`Resolver observation is not supported for ${type} records.`);
37
+ }
38
+ export async function observeDnsRecord(fqdn, target, specs = defaultDnsResolvers, elapsedMs = 0) {
39
+ return Promise.all(specs.map(async (spec) => {
40
+ const servers = spec.servers ?? getServers();
41
+ try {
42
+ const answers = await queryResolver(fqdn, target.type, resolverFor(spec));
43
+ return {
44
+ answers,
45
+ elapsedMs,
46
+ expected: target.value,
47
+ kind: spec.kind,
48
+ matches: answers.length > 0 &&
49
+ answers.every((answer) => normalizeDnsValue(answer.value) === normalizeDnsValue(target.value)),
50
+ resolver: spec.name,
51
+ servers,
52
+ type: target.type,
53
+ };
54
+ }
55
+ catch (error) {
56
+ const errorCode = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
57
+ ? error.code
58
+ : undefined;
59
+ return {
60
+ answers: [],
61
+ elapsedMs,
62
+ error: error instanceof Error ? error.message : String(error),
63
+ ...(errorCode ? { errorCode } : {}),
64
+ expected: target.value,
65
+ kind: spec.kind,
66
+ matches: false,
67
+ resolver: spec.name,
68
+ servers,
69
+ type: target.type,
70
+ };
71
+ }
72
+ }));
73
+ }
74
+ export function classifyDnsPropagation(observations) {
75
+ const publicResults = observations.filter((observation) => observation.kind === 'public' && (!observation.error || isNegativeDnsObservation(observation)));
76
+ const system = observations.find((observation) => observation.kind === 'system');
77
+ const publicMatches = publicResults.length > 0 && publicResults.every((observation) => observation.matches);
78
+ if (!publicMatches)
79
+ return 'public_propagation_pending';
80
+ if (!system || (system.error && !isNegativeDnsObservation(system)))
81
+ return 'system_resolver_unavailable';
82
+ if (!system.matches)
83
+ return 'local_or_vpn_cache_stale';
84
+ return 'propagated';
85
+ }
86
+ const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
87
+ export async function waitForDnsPropagation(input) {
88
+ const now = input.now ?? Date.now;
89
+ const wait = input.sleep ?? sleep;
90
+ const started = now();
91
+ const deadline = started + input.timeoutSeconds * 1000;
92
+ let observations = [];
93
+ while (true) {
94
+ const elapsedMs = now() - started;
95
+ observations = input.observe
96
+ ? await input.observe(input.fqdn, input.record, elapsedMs)
97
+ : await observeDnsRecord(input.fqdn, input.record, defaultDnsResolvers, elapsedMs);
98
+ const status = classifyDnsPropagation(observations);
99
+ if (status === 'propagated' || status === 'local_or_vpn_cache_stale' || status === 'system_resolver_unavailable') {
100
+ return { elapsedMs, expected: input.record.value, observations, status };
101
+ }
102
+ const remaining = deadline - now();
103
+ if (remaining <= 0) {
104
+ return {
105
+ elapsedMs: now() - started,
106
+ expected: input.record.value,
107
+ observations,
108
+ status,
109
+ timeoutReason: 'public_resolvers_did_not_match_before_timeout',
110
+ };
111
+ }
112
+ await wait(Math.min(5000, remaining));
113
+ }
114
+ }
@@ -0,0 +1,31 @@
1
+ import { type DnsRecordSelector } from './dns-records.js';
2
+ import type { DnsProvider, DnsRecord, DnsRecordInput, DnsZone } from './providers/types.js';
3
+ interface RetryDependencies {
4
+ now?: () => number;
5
+ sleep?: (milliseconds: number) => Promise<void>;
6
+ }
7
+ export interface ReconciliationResult {
8
+ appliedChanges: number;
9
+ attempts: number;
10
+ observed: DnsRecord[];
11
+ reconciled: true;
12
+ }
13
+ export declare function reconcileDesiredRecord(input: {
14
+ desired: DnsRecordInput;
15
+ intervalMs?: number;
16
+ provider: DnsProvider;
17
+ timeoutMs?: number;
18
+ zone: DnsZone;
19
+ progress?: (message: string) => void;
20
+ dependencies?: RetryDependencies;
21
+ }): Promise<ReconciliationResult>;
22
+ export declare function reconcileRecordRemoval(input: {
23
+ intervalMs?: number;
24
+ provider: DnsProvider;
25
+ selector: DnsRecordSelector;
26
+ timeoutMs?: number;
27
+ zone: DnsZone;
28
+ progress?: (message: string) => void;
29
+ dependencies?: RetryDependencies;
30
+ }): Promise<ReconciliationResult>;
31
+ export {};
@@ -0,0 +1,53 @@
1
+ import { desiredSlotPostcondition, recordMatchesSelector } from './dns-records.js';
2
+ import { DoomainError } from './errors.js';
3
+ const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
4
+ export async function reconcileDesiredRecord(input) {
5
+ const now = input.dependencies?.now ?? Date.now;
6
+ const wait = input.dependencies?.sleep ?? sleep;
7
+ const deadline = now() + (input.timeoutMs ?? 30_000);
8
+ let attempts = 0;
9
+ const appliedChanges = 0;
10
+ while (true) {
11
+ attempts += 1;
12
+ const records = await input.provider.listRecords(input.zone);
13
+ const state = desiredSlotPostcondition(records, input.desired);
14
+ if (state.reconciled)
15
+ return { appliedChanges, attempts, observed: state.observed, reconciled: true };
16
+ if (now() >= deadline) {
17
+ throw new DoomainError('DNS_RECONCILIATION_INCOMPLETE', `The DNS provider accepted the change, but the ${input.desired.type} ${input.desired.name} slot is not reconciled yet.`, {
18
+ attempts,
19
+ expected: input.desired,
20
+ observed: state.observed,
21
+ recovery: 'Retry the command. Do not treat the DNS change as complete until reconciled is true.',
22
+ });
23
+ }
24
+ input.progress?.('Waiting for the DNS provider to publish the accepted change');
25
+ const remaining = deadline - now();
26
+ await wait(Math.min(input.intervalMs ?? 1000, Math.max(0, remaining)));
27
+ }
28
+ }
29
+ export async function reconcileRecordRemoval(input) {
30
+ const now = input.dependencies?.now ?? Date.now;
31
+ const wait = input.dependencies?.sleep ?? sleep;
32
+ const deadline = now() + (input.timeoutMs ?? 30_000);
33
+ let attempts = 0;
34
+ const appliedChanges = 0;
35
+ while (true) {
36
+ attempts += 1;
37
+ const records = await input.provider.listRecords(input.zone);
38
+ const observed = records.filter((record) => recordMatchesSelector(record, input.selector));
39
+ if (observed.length === 0)
40
+ return { appliedChanges, attempts, observed, reconciled: true };
41
+ if (now() >= deadline) {
42
+ throw new DoomainError('DNS_RECONCILIATION_INCOMPLETE', `The DNS provider accepted the deletion, but ${observed.length} matching record${observed.length === 1 ? '' : 's'} remain.`, {
43
+ attempts,
44
+ observed,
45
+ selector: input.selector,
46
+ recovery: 'Retry the command. Do not treat the DNS deletion as complete until reconciled is true.',
47
+ });
48
+ }
49
+ input.progress?.('Waiting for the DNS provider to publish the accepted deletion');
50
+ const remaining = deadline - now();
51
+ await wait(Math.min(input.intervalMs ?? 1000, Math.max(0, remaining)));
52
+ }
53
+ }
@@ -0,0 +1,18 @@
1
+ import type { DnsRecord, DnsRecordInput } from './providers/types.js';
2
+ export interface DnsRecordSelector {
3
+ name: string;
4
+ type: DnsRecord['type'];
5
+ value?: string;
6
+ }
7
+ export declare function normalizeDnsValue(value: string): string;
8
+ export declare function inferAddressRecordType(target: string): 'A' | 'AAAA' | 'CNAME';
9
+ export declare function normalizeAddressRecordTarget(recordType: 'A' | 'AAAA' | 'CNAME', value: string): string;
10
+ export declare function dnsRecordNamesEqual(a: string, b: string): boolean;
11
+ export declare function sameDnsRecordTarget(a: DnsRecord | DnsRecordInput, b: DnsRecord | DnsRecordInput): boolean;
12
+ export declare function sameDnsRecordValue(a: DnsRecord | DnsRecordInput, b: DnsRecord | DnsRecordInput): boolean;
13
+ export declare function recordsInNonTxtSlot(records: DnsRecord[], desired: DnsRecordInput): DnsRecord[];
14
+ export declare function desiredSlotPostcondition(records: DnsRecord[], desired: DnsRecordInput): {
15
+ observed: DnsRecord[];
16
+ reconciled: boolean;
17
+ };
18
+ export declare function recordMatchesSelector(record: DnsRecord, selector: DnsRecordSelector): boolean;
@@ -0,0 +1,73 @@
1
+ import { isIP } from 'node:net';
2
+ import { DoomainError } from './errors.js';
3
+ import { normalizeDomain } from './validate.js';
4
+ function normalizeIpv6(value) {
5
+ try {
6
+ return new URL(`http://[${value}]`).hostname.slice(1, -1);
7
+ }
8
+ catch {
9
+ return value.toLowerCase();
10
+ }
11
+ }
12
+ export function normalizeDnsValue(value) {
13
+ const cleaned = value.trim().toLowerCase().replace(/\.$/, '');
14
+ return isIP(cleaned) === 6 ? normalizeIpv6(cleaned) : cleaned;
15
+ }
16
+ export function inferAddressRecordType(target) {
17
+ const version = isIP(target);
18
+ if (version === 4)
19
+ return 'A';
20
+ if (version === 6)
21
+ return 'AAAA';
22
+ return 'CNAME';
23
+ }
24
+ export function normalizeAddressRecordTarget(recordType, value) {
25
+ const target = value.trim().replace(/\.$/, '');
26
+ if (!target)
27
+ throw new DoomainError('MISSING_ARGUMENT', 'A DNS target is required.');
28
+ const version = isIP(target);
29
+ if (recordType === 'A' && version !== 4)
30
+ throw new DoomainError('INVALID_INPUT', 'A records require an IPv4 target.');
31
+ if (recordType === 'AAAA' && version !== 6)
32
+ throw new DoomainError('INVALID_INPUT', 'AAAA records require an IPv6 target.');
33
+ if (recordType === 'CNAME' && version !== 0)
34
+ throw new DoomainError('INVALID_INPUT', 'CNAME records require a hostname target.');
35
+ return recordType === 'CNAME' ? normalizeDomain(target) : target;
36
+ }
37
+ export function dnsRecordNamesEqual(a, b) {
38
+ return a.trim().toLowerCase().replace(/\.$/, '') === b.trim().toLowerCase().replace(/\.$/, '');
39
+ }
40
+ function comparableRecordValue(type, value) {
41
+ return type === 'TXT' ? value : normalizeDnsValue(value);
42
+ }
43
+ export function sameDnsRecordTarget(a, b) {
44
+ return (dnsRecordNamesEqual(a.name, b.name) &&
45
+ a.type === b.type &&
46
+ comparableRecordValue(a.type, a.value) === comparableRecordValue(b.type, b.value));
47
+ }
48
+ export function sameDnsRecordValue(a, b) {
49
+ return (sameDnsRecordTarget(a, b) &&
50
+ (b.ttl === undefined || a.ttl === b.ttl) &&
51
+ (b.priority === undefined || a.priority === b.priority) &&
52
+ (b.proxied === undefined || a.proxied === b.proxied));
53
+ }
54
+ export function recordsInNonTxtSlot(records, desired) {
55
+ if (desired.type === 'TXT')
56
+ return records.filter((record) => dnsRecordNamesEqual(record.name, desired.name) && record.type === 'TXT');
57
+ return records.filter((record) => dnsRecordNamesEqual(record.name, desired.name) &&
58
+ record.type !== 'TXT' &&
59
+ (record.type === desired.type || record.type === 'CNAME' || desired.type === 'CNAME'));
60
+ }
61
+ export function desiredSlotPostcondition(records, desired) {
62
+ const observed = recordsInNonTxtSlot(records, desired);
63
+ return {
64
+ observed,
65
+ reconciled: observed.length === 1 && sameDnsRecordValue(observed[0], desired),
66
+ };
67
+ }
68
+ export function recordMatchesSelector(record, selector) {
69
+ return (dnsRecordNamesEqual(record.name, selector.name) &&
70
+ record.type === selector.type &&
71
+ (selector.value === undefined ||
72
+ comparableRecordValue(record.type, record.value) === comparableRecordValue(selector.type, selector.value)));
73
+ }
@@ -151,7 +151,7 @@ async function loadConfiguredProviderZones(providerId, accountInput, toleratePro
151
151
  : ['doomain providers connect', 'doomain link <domain> --json'],
152
152
  });
153
153
  }
154
- const results = await Promise.all(providerAccounts.map(({ definition, ref }) => loadProviderZonesSafely(definition, ref)));
154
+ const results = await Promise.all(providerAccounts.map(({ definition, ref }) => tolerateProviderAccountErrors ? loadProviderZonesSafely(definition, ref) : loadProviderZones(definition, ref)));
155
155
  return {
156
156
  accountInferred: account === undefined,
157
157
  candidates: results.flatMap((result) => result.candidates),
@@ -1,4 +1,4 @@
1
- export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'CLERK_AUTH_FAILED' | 'CLERK_PRODUCTION_EXISTS' | 'DNS_POINT_FAILED' | 'DNS_TARGET_CONFLICT' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_PROVIDER_DISCOVERY_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'SELF_UPDATE_FAILED' | 'VERCEL_AUTH_FAILED' | 'VERCEL_PROJECT_NOT_LINKED';
1
+ export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'CLERK_AUTH_FAILED' | 'CLERK_PRODUCTION_EXISTS' | 'DNS_POINT_FAILED' | 'DNS_REMOVE_FAILED' | 'DNS_DELETE_AMBIGUOUS' | 'DNS_DIAGNOSE_FAILED' | 'DNS_RECONCILIATION_INCOMPLETE' | 'DNS_TARGET_CONFLICT' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_PROVIDER_DISCOVERY_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'SELF_UPDATE_FAILED' | 'VERCEL_AUTH_FAILED' | 'VERCEL_PROJECT_NOT_LINKED';
2
2
  export declare class DoomainError extends Error {
3
3
  readonly code: DoomainErrorCode;
4
4
  readonly details?: unknown;
@@ -1,3 +1,4 @@
1
+ import { type DnsPropagationResult, type DnsResolverObservation } from './dns-propagation.js';
1
2
  import { type ResolvedDnsTarget } from './domain-provider.js';
2
3
  import { type DnsOverrideWarning } from './link-domain.js';
3
4
  import type { DnsProvider, DnsRecordInput } from './providers/types.js';
@@ -13,6 +14,7 @@ export interface PointDomainInput {
13
14
  timeoutSeconds?: number;
14
15
  ttl?: number;
15
16
  wait?: boolean;
17
+ reconcileTimeoutSeconds?: number;
16
18
  confirmDnsOverride?: (warning: DnsOverrideWarning) => Promise<boolean>;
17
19
  progress?: (message: string) => void;
18
20
  }
@@ -25,6 +27,9 @@ export interface PointDomainResult {
25
27
  provider: string;
26
28
  providerInferred: boolean;
27
29
  propagated: boolean;
30
+ propagation: DnsPropagationResult;
31
+ reconciled: boolean;
32
+ reconciliationAttempts: number;
28
33
  record: DnsRecordInput;
29
34
  skipped: DnsRecordInput[];
30
35
  updated: boolean;
@@ -34,9 +39,7 @@ interface PointDomainDependencies {
34
39
  createProvider: (provider: string, opts: {
35
40
  account?: string;
36
41
  }) => Promise<DnsProvider>;
37
- resolve4?: (hostname: string) => Promise<string[]>;
38
- resolve6?: (hostname: string) => Promise<string[]>;
39
- resolveCname?: (hostname: string) => Promise<string[]>;
42
+ observeDns?: (fqdn: string, target: Pick<DnsRecordInput, 'type' | 'value'>, elapsedMs: number) => Promise<DnsResolverObservation[]>;
40
43
  resolveTarget: (input: Pick<PointDomainInput, 'account' | 'domain' | 'provider'>) => Promise<ResolvedDnsTarget>;
41
44
  }
42
45
  export declare function createPointRecord(input: {