doomain 0.1.23 → 0.1.24
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/commands/auth/clerk.js +3 -2
- package/dist/commands/auth/logout/clerk.js +3 -2
- package/dist/commands/auth/logout/vercel.js +3 -2
- package/dist/commands/auth/vercel.js +5 -4
- package/dist/commands/clerk/domains/add.js +3 -2
- package/dist/commands/dns/diagnose.js +3 -2
- package/dist/commands/dns/point.js +7 -3
- package/dist/commands/dns/remove.js +3 -2
- package/dist/commands/domains/find.js +3 -2
- package/dist/commands/domains/list.js +46 -33
- package/dist/commands/link.js +4 -3
- package/dist/commands/projects/list.js +5 -2
- package/dist/commands/providers/connect.js +14 -23
- package/dist/commands/providers/disconnect.js +3 -2
- package/dist/commands/providers/status.js +2 -1
- package/dist/commands/providers/verify.js +3 -2
- package/dist/commands/schema.js +2 -1
- package/dist/commands/update.js +2 -1
- package/dist/commands/verify.js +5 -2
- package/dist/commands/wizard.js +45 -46
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/clerk-domain.d.ts +2 -1
- package/dist/lib/clerk-domain.js +122 -110
- package/dist/lib/clerk.d.ts +9 -5
- package/dist/lib/clerk.js +45 -36
- package/dist/lib/command-schema.d.ts +2 -1
- package/dist/lib/command-schema.js +9 -9
- package/dist/lib/config.d.ts +6 -4
- package/dist/lib/config.js +29 -27
- package/dist/lib/diagnose-dns.d.ts +8 -5
- package/dist/lib/diagnose-dns.js +90 -92
- package/dist/lib/dns-propagation.d.ts +5 -4
- package/dist/lib/dns-propagation.js +80 -54
- package/dist/lib/dns-reconciliation.d.ts +4 -3
- package/dist/lib/dns-reconciliation.js +52 -47
- package/dist/lib/domain-provider.d.ts +5 -1
- package/dist/lib/domain-provider.js +136 -131
- package/dist/lib/effect.d.ts +6 -0
- package/dist/lib/effect.js +20 -0
- package/dist/lib/link-domain.d.ts +3 -2
- package/dist/lib/link-domain.js +239 -218
- package/dist/lib/point-domain.d.ts +7 -4
- package/dist/lib/point-domain.js +92 -90
- package/dist/lib/providers/cloudflare/index.d.ts +9 -8
- package/dist/lib/providers/cloudflare/index.js +93 -87
- package/dist/lib/providers/core/config.d.ts +4 -1
- package/dist/lib/providers/core/config.js +17 -13
- package/dist/lib/providers/core/http.d.ts +4 -1
- package/dist/lib/providers/core/http.js +28 -18
- package/dist/lib/providers/core/pagination.d.ts +3 -2
- package/dist/lib/providers/core/pagination.js +17 -14
- package/dist/lib/providers/core/planner.d.ts +5 -4
- package/dist/lib/providers/core/planner.js +18 -15
- package/dist/lib/providers/core/types.d.ts +11 -8
- package/dist/lib/providers/hostinger/index.d.ts +9 -8
- package/dist/lib/providers/hostinger/index.js +74 -72
- package/dist/lib/providers/namecheap/index.d.ts +10 -8
- package/dist/lib/providers/namecheap/index.js +134 -109
- package/dist/lib/providers/registry.d.ts +3 -3
- package/dist/lib/providers/registry.js +7 -3
- package/dist/lib/providers/spaceship/index.d.ts +9 -8
- package/dist/lib/providers/spaceship/index.js +48 -47
- package/dist/lib/providers/status.d.ts +2 -1
- package/dist/lib/providers/status.js +41 -34
- package/dist/lib/public-ip.d.ts +2 -0
- package/dist/lib/public-ip.js +14 -0
- package/dist/lib/remove-domain.d.ts +6 -3
- package/dist/lib/remove-domain.js +83 -77
- package/dist/lib/self-update.d.ts +3 -2
- package/dist/lib/self-update.js +15 -23
- package/dist/lib/vercel-auth.d.ts +2 -1
- package/dist/lib/vercel-auth.js +21 -24
- package/dist/lib/vercel.d.ts +16 -12
- package/dist/lib/vercel.js +174 -114
- package/oclif.manifest.json +1 -1
- package/package.json +2 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DoomainEffect } from './effect.js';
|
|
1
2
|
import { type ProviderStatus } from './providers/status.js';
|
|
2
3
|
export type ProviderConnectionStatus = Pick<ProviderStatus, 'account' | 'accountLabel' | 'configured' | 'default' | 'displayName' | 'docsUrl' | 'id' | 'isDefaultAccount' | 'isPreferredProvider'>;
|
|
3
4
|
export interface CommandSchema {
|
|
@@ -23,4 +24,4 @@ export interface CommandSchema {
|
|
|
23
24
|
}
|
|
24
25
|
export declare const commandSchemas: CommandSchema[];
|
|
25
26
|
export declare function getCommandSchema(name?: string): CommandSchema[] | CommandSchema | undefined;
|
|
26
|
-
export declare function getCommandSchemaForAgents(name?: string):
|
|
27
|
+
export declare function getCommandSchemaForAgents(name?: string): DoomainEffect<CommandSchema[] | CommandSchema | undefined>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
1
2
|
import { listProviderStatuses } from './providers/status.js';
|
|
2
3
|
const dnsRemovalFlags = [
|
|
3
4
|
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
@@ -449,8 +450,8 @@ export function getCommandSchema(name) {
|
|
|
449
450
|
return commandSchemas;
|
|
450
451
|
return commandSchemas.find((schema) => schema.name === name);
|
|
451
452
|
}
|
|
452
|
-
|
|
453
|
-
return
|
|
453
|
+
function configuredProviders() {
|
|
454
|
+
return listProviderStatuses({ verify: false }).pipe(Effect.map((providers) => providers.map((provider) => ({
|
|
454
455
|
account: provider.account,
|
|
455
456
|
accountLabel: provider.accountLabel,
|
|
456
457
|
configured: provider.configured,
|
|
@@ -460,7 +461,7 @@ async function configuredProviders() {
|
|
|
460
461
|
id: provider.id,
|
|
461
462
|
isDefaultAccount: provider.isDefaultAccount,
|
|
462
463
|
isPreferredProvider: provider.isPreferredProvider,
|
|
463
|
-
}));
|
|
464
|
+
}))));
|
|
464
465
|
}
|
|
465
466
|
function withProviderConnections(schema, providers) {
|
|
466
467
|
if (schema.name !== 'link' &&
|
|
@@ -472,12 +473,11 @@ function withProviderConnections(schema, providers) {
|
|
|
472
473
|
return schema;
|
|
473
474
|
return { ...schema, configuredProviders: providers };
|
|
474
475
|
}
|
|
475
|
-
export
|
|
476
|
+
export function getCommandSchemaForAgents(name) {
|
|
476
477
|
const schema = getCommandSchema(name);
|
|
477
478
|
if (!schema)
|
|
478
|
-
return undefined;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
return withProviderConnections(schema, providers);
|
|
479
|
+
return Effect.succeed(undefined);
|
|
480
|
+
return configuredProviders().pipe(Effect.map((providers) => Array.isArray(schema)
|
|
481
|
+
? schema.map((item) => withProviderConnections(item, providers))
|
|
482
|
+
: withProviderConnections(schema, providers)));
|
|
483
483
|
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type DoomainEffect } from './effect.js';
|
|
2
|
+
import { type DoomainErrorCode } from './errors.js';
|
|
1
3
|
export declare function getConfigDir(): string;
|
|
2
4
|
export declare function getConfigFile(): string;
|
|
3
5
|
export declare const CONFIG_DIR: string;
|
|
@@ -35,9 +37,9 @@ export interface DoomainConfig {
|
|
|
35
37
|
domain?: string;
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
|
-
export declare function loadConfig():
|
|
39
|
-
export declare function saveConfig(config: DoomainConfig):
|
|
40
|
-
export declare function updateConfig(updater: (config: DoomainConfig) => DoomainConfig):
|
|
41
|
-
export declare function clearConfig():
|
|
40
|
+
export declare function loadConfig(): DoomainEffect<DoomainConfig, never>;
|
|
41
|
+
export declare function saveConfig(config: DoomainConfig, errorCode?: DoomainErrorCode): DoomainEffect<void>;
|
|
42
|
+
export declare function updateConfig(updater: (config: DoomainConfig) => DoomainConfig, errorCode?: DoomainErrorCode): DoomainEffect<DoomainConfig>;
|
|
43
|
+
export declare function clearConfig(): DoomainEffect<boolean, never>;
|
|
42
44
|
export declare function getConfigPath(): string;
|
|
43
45
|
export declare function maskSecret(value?: string): string | undefined;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
+
import { Effect } from 'effect';
|
|
5
|
+
import { trySync } from './effect.js';
|
|
6
|
+
import { toDoomainError } from './errors.js';
|
|
4
7
|
export function getConfigDir() {
|
|
5
8
|
return process.env.DOOMAIN_CONFIG_DIR || join(homedir(), '.doomain');
|
|
6
9
|
}
|
|
@@ -9,33 +12,32 @@ export function getConfigFile() {
|
|
|
9
12
|
}
|
|
10
13
|
export const CONFIG_DIR = getConfigDir();
|
|
11
14
|
export const CONFIG_FILE = getConfigFile();
|
|
12
|
-
export
|
|
13
|
-
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
15
|
+
export function loadConfig() {
|
|
16
|
+
return Effect.tryPromise(() => readFile(getConfigFile(), 'utf8')).pipe(Effect.flatMap((data) => Effect.try(() => JSON.parse(data))), Effect.catchAll(() => Effect.succeed({})));
|
|
17
|
+
}
|
|
18
|
+
export function saveConfig(config, errorCode = 'CONFIG_NOT_FOUND') {
|
|
19
|
+
return Effect.gen(function* () {
|
|
20
|
+
const configFile = getConfigFile();
|
|
21
|
+
yield* Effect.tryPromise({
|
|
22
|
+
try: () => mkdir(dirname(configFile), { recursive: true }),
|
|
23
|
+
catch: (cause) => toDoomainError(cause, errorCode),
|
|
24
|
+
});
|
|
25
|
+
yield* Effect.tryPromise({
|
|
26
|
+
try: () => writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 }),
|
|
27
|
+
catch: (cause) => toDoomainError(cause, errorCode),
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function updateConfig(updater, errorCode = 'CONFIG_NOT_FOUND') {
|
|
32
|
+
return Effect.gen(function* () {
|
|
33
|
+
const current = yield* loadConfig();
|
|
34
|
+
const next = yield* trySync(() => updater(current), errorCode);
|
|
35
|
+
yield* saveConfig(next, errorCode);
|
|
36
|
+
return next;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function clearConfig() {
|
|
40
|
+
return Effect.tryPromise(() => unlink(getConfigFile())).pipe(Effect.as(true), Effect.catchAll(() => Effect.succeed(false)));
|
|
39
41
|
}
|
|
40
42
|
export function getConfigPath() {
|
|
41
43
|
return getConfigFile();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type DnsResolverObservation } from './dns-propagation.js';
|
|
2
2
|
import { type ResolvedDnsTarget } from './domain-provider.js';
|
|
3
|
+
import { type DoomainEffect } from './effect.js';
|
|
4
|
+
import { type DoomainErrorCode } from './errors.js';
|
|
3
5
|
import type { DnsProvider, DnsRecord, DnsRecordInput, DnsRecordType } from './providers/types.js';
|
|
4
6
|
export type DnsDiagnosisStatus = 'consistent' | 'local_or_vpn_cache_stale' | 'provider_not_updated' | 'public_propagation_pending' | 'system_resolver_unavailable';
|
|
5
7
|
export interface DiagnoseDnsInput {
|
|
@@ -44,10 +46,11 @@ export interface DiagnoseDnsResult {
|
|
|
44
46
|
interface DiagnoseDnsDependencies {
|
|
45
47
|
createProvider: (provider: string, opts: {
|
|
46
48
|
account?: string;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
transportErrorCode?: DoomainErrorCode;
|
|
50
|
+
}) => DoomainEffect<DnsProvider>;
|
|
51
|
+
observeDns: (fqdn: string, target: Pick<DnsRecordInput, 'type' | 'value'>, elapsedMs: number) => DoomainEffect<DnsResolverObservation[]>;
|
|
52
|
+
resolveTarget: (input: Pick<DiagnoseDnsInput, 'account' | 'domain' | 'provider'>) => DoomainEffect<ResolvedDnsTarget>;
|
|
53
|
+
macOsResolvers?: () => DoomainEffect<MacOsResolverMetadata[], never>;
|
|
51
54
|
}
|
|
52
|
-
export declare function diagnoseDns(input: DiagnoseDnsInput, dependencies?: DiagnoseDnsDependencies):
|
|
55
|
+
export declare function diagnoseDns(input: DiagnoseDnsInput, dependencies?: DiagnoseDnsDependencies): DoomainEffect<DiagnoseDnsResult>;
|
|
53
56
|
export {};
|
package/dist/lib/diagnose-dns.js
CHANGED
|
@@ -2,46 +2,45 @@ import { execFile } from 'node:child_process';
|
|
|
2
2
|
import { getServers } from 'node:dns';
|
|
3
3
|
import { networkInterfaces, platform } from 'node:os';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
|
+
import { Effect } from 'effect';
|
|
5
6
|
import { classifyDnsPropagation, isNegativeDnsObservation, observeDnsRecord, } from './dns-propagation.js';
|
|
6
7
|
import { desiredSlotPostcondition, dnsRecordNamesEqual, inferAddressRecordType, normalizeAddressRecordTarget, normalizeDnsValue, } from './dns-records.js';
|
|
7
8
|
import { resolveProviderTarget } from './domain-provider.js';
|
|
9
|
+
import { trySync } from './effect.js';
|
|
8
10
|
import { DoomainError } from './errors.js';
|
|
9
11
|
import { createProvider } from './providers/registry.js';
|
|
10
12
|
const execFileAsync = promisify(execFile);
|
|
11
|
-
|
|
13
|
+
function readMacOsResolvers() {
|
|
12
14
|
if (platform() !== 'darwin')
|
|
13
|
-
return [];
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
];
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
15
|
+
return Effect.succeed([]);
|
|
16
|
+
return Effect.tryPromise(() => execFileAsync('scutil', ['--dns'], { maxBuffer: 1024 * 1024 })).pipe(Effect.map(({ stdout }) => 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
|
+
})), Effect.catchAll(() => Effect.succeed([])));
|
|
39
35
|
}
|
|
40
36
|
const defaultDependencies = {
|
|
41
37
|
createProvider,
|
|
42
38
|
macOsResolvers: readMacOsResolvers,
|
|
43
39
|
observeDns: (fqdn, target, elapsedMs) => observeDnsRecord(fqdn, target, undefined, elapsedMs),
|
|
44
|
-
resolveTarget: (input) => resolveProviderTarget(input, {
|
|
40
|
+
resolveTarget: (input) => resolveProviderTarget(input, {
|
|
41
|
+
tolerateProviderAccountErrors: true,
|
|
42
|
+
transportErrorCode: 'DNS_DIAGNOSE_FAILED',
|
|
43
|
+
}),
|
|
45
44
|
};
|
|
46
45
|
function recordConflicts(records) {
|
|
47
46
|
const conflicts = [];
|
|
@@ -88,69 +87,68 @@ function diagnosisResolutionError(error, input) {
|
|
|
88
87
|
suggestedCommands: ['doomain providers connect', retry],
|
|
89
88
|
});
|
|
90
89
|
}
|
|
91
|
-
export
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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,
|
|
90
|
+
export function diagnoseDns(input, dependencies = defaultDependencies) {
|
|
91
|
+
return Effect.gen(function* () {
|
|
92
|
+
const recordType = yield* trySync(() => input.recordType ?? (input.target ? inferAddressRecordType(input.target.trim()) : undefined) ?? 'A', 'INVALID_INPUT');
|
|
93
|
+
const requestedTarget = yield* trySync(() => (input.target === undefined ? undefined : normalizeAddressRecordTarget(recordType, input.target)), 'INVALID_INPUT');
|
|
94
|
+
const resolved = yield* dependencies
|
|
95
|
+
.resolveTarget(input)
|
|
96
|
+
.pipe(Effect.mapError((error) => diagnosisResolutionError(error, input)));
|
|
97
|
+
const provider = yield* dependencies.createProvider(resolved.provider, {
|
|
98
|
+
account: resolved.account,
|
|
99
|
+
transportErrorCode: 'DNS_DIAGNOSE_FAILED',
|
|
121
100
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
101
|
+
const zone = yield* provider.getZone(resolved.target.zoneDomain);
|
|
102
|
+
if (!zone) {
|
|
103
|
+
return yield* Effect.fail(diagnosisResolutionError(new DoomainError('PROVIDER_ZONE_NOT_FOUND', `${provider.name} does not have a DNS zone for ${resolved.target.zoneDomain}.`), input));
|
|
104
|
+
}
|
|
105
|
+
const allRecords = yield* provider.listRecords(zone);
|
|
106
|
+
const providerRecords = allRecords.filter((record) => dnsRecordNamesEqual(record.name, resolved.target.recordName));
|
|
107
|
+
const recordsOfType = providerRecords.filter((record) => record.type === recordType);
|
|
108
|
+
const expected = requestedTarget ?? (recordsOfType.length === 1 ? recordsOfType[0].value : undefined);
|
|
109
|
+
const queryTarget = { type: recordType, value: expected ?? recordsOfType[0]?.value ?? '' };
|
|
110
|
+
const observations = yield* dependencies.observeDns(resolved.target.fullDomain, queryTarget, 0);
|
|
111
|
+
const macOsResolvers = dependencies.macOsResolvers ? yield* dependencies.macOsResolvers() : undefined;
|
|
112
|
+
let status;
|
|
113
|
+
if (expected) {
|
|
114
|
+
const postcondition = desiredSlotPostcondition(allRecords, {
|
|
115
|
+
name: resolved.target.recordName,
|
|
116
|
+
type: recordType,
|
|
117
|
+
value: expected,
|
|
118
|
+
});
|
|
119
|
+
status = postcondition.reconciled ? diagnosisStatus(classifyDnsPropagation(observations)) : 'provider_not_updated';
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const providerValues = new Set(recordsOfType.map((record) => normalizeDnsValue(record.value)));
|
|
123
|
+
const compared = observations.map((observation) => ({
|
|
124
|
+
...observation,
|
|
125
|
+
matches: providerValues.size === 0
|
|
126
|
+
? observation.answers.length === 0 && isNegativeDnsObservation(observation)
|
|
127
|
+
: observation.answers.length === providerValues.size &&
|
|
128
|
+
observation.answers.every((answer) => providerValues.has(normalizeDnsValue(answer.value))),
|
|
129
|
+
}));
|
|
130
|
+
observations.splice(0, observations.length, ...compared);
|
|
131
|
+
status = diagnosisStatus(classifyDnsPropagation(observations));
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
account: resolved.account,
|
|
135
|
+
accountInferred: resolved.accountInferred,
|
|
136
|
+
conflicts: recordConflicts(providerRecords),
|
|
137
|
+
domain: resolved.target.fullDomain,
|
|
138
|
+
...(expected === undefined ? {} : { expected }),
|
|
139
|
+
interfaces: activeInterfaceNames(),
|
|
140
|
+
isDefaultAccount: resolved.isDefaultAccount,
|
|
141
|
+
...(macOsResolvers && macOsResolvers.length > 0 ? { macOsResolvers } : {}),
|
|
142
|
+
observations,
|
|
143
|
+
platform: platform(),
|
|
144
|
+
provider: resolved.provider,
|
|
145
|
+
providerInferred: resolved.providerInferred,
|
|
146
|
+
providerRecords,
|
|
147
|
+
recordType,
|
|
148
|
+
status,
|
|
149
|
+
systemResolverServers: getServers(),
|
|
150
|
+
warnings: resolved.warnings,
|
|
151
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
156
154
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DoomainEffect } from './effect.js';
|
|
1
2
|
import type { DnsRecordInput, DnsRecordType } from './providers/types.js';
|
|
2
3
|
export interface DnsAnswer {
|
|
3
4
|
ttl?: number | null;
|
|
@@ -32,14 +33,14 @@ export interface DnsResolverSpec {
|
|
|
32
33
|
export declare const defaultDnsResolvers: DnsResolverSpec[];
|
|
33
34
|
export declare function isNegativeDnsObservation(observation: DnsResolverObservation): boolean;
|
|
34
35
|
type ResolveTarget = Pick<DnsRecordInput, 'type' | 'value'>;
|
|
35
|
-
export declare function observeDnsRecord(fqdn: string, target: ResolveTarget, specs?: DnsResolverSpec[], elapsedMs?: number):
|
|
36
|
+
export declare function observeDnsRecord(fqdn: string, target: ResolveTarget, specs?: DnsResolverSpec[], elapsedMs?: number): DoomainEffect<DnsResolverObservation[], never>;
|
|
36
37
|
export declare function classifyDnsPropagation(observations: DnsResolverObservation[]): DnsPropagationStatus;
|
|
37
38
|
export declare function waitForDnsPropagation(input: {
|
|
38
39
|
fqdn: string;
|
|
39
40
|
record: ResolveTarget;
|
|
40
41
|
timeoutSeconds: number;
|
|
41
|
-
observe?: (fqdn: string, target: ResolveTarget, elapsedMs: number) =>
|
|
42
|
+
observe?: (fqdn: string, target: ResolveTarget, elapsedMs: number) => DoomainEffect<DnsResolverObservation[]>;
|
|
42
43
|
now?: () => number;
|
|
43
|
-
sleep?: (milliseconds: number) =>
|
|
44
|
-
}):
|
|
44
|
+
sleep?: (milliseconds: number) => DoomainEffect<void, never>;
|
|
45
|
+
}): DoomainEffect<DnsPropagationResult>;
|
|
45
46
|
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getServers, Resolver, resolve4, resolve6, resolveCname } from 'node:dns/promises';
|
|
2
|
+
import { Effect } from 'effect';
|
|
2
3
|
import { normalizeDnsValue } from './dns-records.js';
|
|
4
|
+
import { DoomainError } from './errors.js';
|
|
3
5
|
export const defaultDnsResolvers = [
|
|
4
6
|
{ kind: 'system', name: 'system' },
|
|
5
7
|
{ kind: 'public', name: 'cloudflare', servers: ['1.1.1.1', '1.0.0.1'] },
|
|
@@ -16,30 +18,51 @@ function resolverFor(spec) {
|
|
|
16
18
|
resolver.setServers(spec.servers);
|
|
17
19
|
return resolver;
|
|
18
20
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.`);
|
|
21
|
+
function resolverErrorCode(error) {
|
|
22
|
+
const cause = error instanceof DoomainError ? error.details : error;
|
|
23
|
+
return cause && typeof cause === 'object' && 'code' in cause && typeof cause.code === 'string'
|
|
24
|
+
? cause.code
|
|
25
|
+
: undefined;
|
|
37
26
|
}
|
|
38
|
-
|
|
39
|
-
return
|
|
27
|
+
function queryResolver(fqdn, type, resolver) {
|
|
28
|
+
return Effect.gen(function* () {
|
|
29
|
+
if (type === 'A') {
|
|
30
|
+
const answers = yield* Effect.tryPromise({
|
|
31
|
+
try: () => (resolver ? resolver.resolve4(fqdn, { ttl: true }) : resolve4(fqdn, { ttl: true })),
|
|
32
|
+
catch: (cause) => new DoomainError('DNS_DIAGNOSE_FAILED', String(cause), cause),
|
|
33
|
+
});
|
|
34
|
+
return answers.map((answer) => ({ ttl: answer.ttl, value: answer.address }));
|
|
35
|
+
}
|
|
36
|
+
if (type === 'AAAA') {
|
|
37
|
+
const answers = yield* Effect.tryPromise({
|
|
38
|
+
try: () => (resolver ? resolver.resolve6(fqdn, { ttl: true }) : resolve6(fqdn, { ttl: true })),
|
|
39
|
+
catch: (cause) => new DoomainError('DNS_DIAGNOSE_FAILED', String(cause), cause),
|
|
40
|
+
});
|
|
41
|
+
return answers.map((answer) => ({ ttl: answer.ttl, value: answer.address }));
|
|
42
|
+
}
|
|
43
|
+
if (type === 'CNAME') {
|
|
44
|
+
const answers = yield* Effect.tryPromise({
|
|
45
|
+
try: () => (resolver ? resolver.resolveCname(fqdn) : resolveCname(fqdn)),
|
|
46
|
+
catch: (cause) => new DoomainError('DNS_DIAGNOSE_FAILED', String(cause), cause),
|
|
47
|
+
});
|
|
48
|
+
return answers.map((value) => ({
|
|
49
|
+
ttl: null,
|
|
50
|
+
ttlUnavailableReason: 'resolver_api_does_not_expose_cname_ttl',
|
|
51
|
+
value,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
return yield* Effect.fail(new DoomainError('DNS_DIAGNOSE_FAILED', `Resolver observation is not supported for ${type} records.`));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export function observeDnsRecord(fqdn, target, specs = defaultDnsResolvers, elapsedMs = 0) {
|
|
58
|
+
return Effect.all(specs.map((spec) => Effect.gen(function* () {
|
|
40
59
|
const servers = spec.servers ?? getServers();
|
|
41
|
-
try
|
|
42
|
-
|
|
60
|
+
const result = yield* Effect.try({
|
|
61
|
+
try: () => resolverFor(spec),
|
|
62
|
+
catch: (cause) => new DoomainError('DNS_DIAGNOSE_FAILED', String(cause), cause),
|
|
63
|
+
}).pipe(Effect.flatMap((resolver) => queryResolver(fqdn, target.type, resolver)), Effect.either);
|
|
64
|
+
if (result._tag === 'Right') {
|
|
65
|
+
const answers = result.right;
|
|
43
66
|
return {
|
|
44
67
|
answers,
|
|
45
68
|
elapsedMs,
|
|
@@ -52,10 +75,9 @@ export async function observeDnsRecord(fqdn, target, specs = defaultDnsResolvers
|
|
|
52
75
|
type: target.type,
|
|
53
76
|
};
|
|
54
77
|
}
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
: undefined;
|
|
78
|
+
else {
|
|
79
|
+
const error = result.left;
|
|
80
|
+
const errorCode = resolverErrorCode(error);
|
|
59
81
|
return {
|
|
60
82
|
answers: [],
|
|
61
83
|
elapsedMs,
|
|
@@ -69,7 +91,7 @@ export async function observeDnsRecord(fqdn, target, specs = defaultDnsResolvers
|
|
|
69
91
|
type: target.type,
|
|
70
92
|
};
|
|
71
93
|
}
|
|
72
|
-
}));
|
|
94
|
+
})), { concurrency: 'unbounded' });
|
|
73
95
|
}
|
|
74
96
|
export function classifyDnsPropagation(observations) {
|
|
75
97
|
const publicResults = observations.filter((observation) => observation.kind === 'public' && (!observation.error || isNegativeDnsObservation(observation)));
|
|
@@ -83,32 +105,36 @@ export function classifyDnsPropagation(observations) {
|
|
|
83
105
|
return 'local_or_vpn_cache_stale';
|
|
84
106
|
return 'propagated';
|
|
85
107
|
}
|
|
86
|
-
const sleep = (milliseconds) =>
|
|
87
|
-
export
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const sleep = (milliseconds) => Effect.sleep(milliseconds);
|
|
109
|
+
export function waitForDnsPropagation(input) {
|
|
110
|
+
return Effect.gen(function* () {
|
|
111
|
+
const now = input.now ?? Date.now;
|
|
112
|
+
const wait = input.sleep ?? sleep;
|
|
113
|
+
const started = now();
|
|
114
|
+
const deadline = started + input.timeoutSeconds * 1000;
|
|
115
|
+
let observations = [];
|
|
116
|
+
while (true) {
|
|
117
|
+
const elapsedMs = now() - started;
|
|
118
|
+
observations = input.observe
|
|
119
|
+
? yield* input.observe(input.fqdn, input.record, elapsedMs)
|
|
120
|
+
: yield* observeDnsRecord(input.fqdn, input.record, defaultDnsResolvers, elapsedMs);
|
|
121
|
+
const status = classifyDnsPropagation(observations);
|
|
122
|
+
if (status === 'propagated' ||
|
|
123
|
+
status === 'local_or_vpn_cache_stale' ||
|
|
124
|
+
status === 'system_resolver_unavailable') {
|
|
125
|
+
return { elapsedMs, expected: input.record.value, observations, status };
|
|
126
|
+
}
|
|
127
|
+
const remaining = deadline - now();
|
|
128
|
+
if (remaining <= 0) {
|
|
129
|
+
return {
|
|
130
|
+
elapsedMs: now() - started,
|
|
131
|
+
expected: input.record.value,
|
|
132
|
+
observations,
|
|
133
|
+
status,
|
|
134
|
+
timeoutReason: 'public_resolvers_did_not_match_before_timeout',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
yield* wait(Math.min(5000, remaining));
|
|
111
138
|
}
|
|
112
|
-
|
|
113
|
-
}
|
|
139
|
+
});
|
|
114
140
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type DnsRecordSelector } from './dns-records.js';
|
|
2
|
+
import type { DoomainEffect } from './effect.js';
|
|
2
3
|
import type { DnsProvider, DnsRecord, DnsRecordInput, DnsZone } from './providers/types.js';
|
|
3
4
|
interface RetryDependencies {
|
|
4
5
|
now?: () => number;
|
|
5
|
-
sleep?: (milliseconds: number) =>
|
|
6
|
+
sleep?: (milliseconds: number) => DoomainEffect<void, never>;
|
|
6
7
|
}
|
|
7
8
|
export interface ReconciliationResult {
|
|
8
9
|
appliedChanges: number;
|
|
@@ -18,7 +19,7 @@ export declare function reconcileDesiredRecord(input: {
|
|
|
18
19
|
zone: DnsZone;
|
|
19
20
|
progress?: (message: string) => void;
|
|
20
21
|
dependencies?: RetryDependencies;
|
|
21
|
-
}):
|
|
22
|
+
}): DoomainEffect<ReconciliationResult>;
|
|
22
23
|
export declare function reconcileRecordRemoval(input: {
|
|
23
24
|
intervalMs?: number;
|
|
24
25
|
provider: DnsProvider;
|
|
@@ -27,5 +28,5 @@ export declare function reconcileRecordRemoval(input: {
|
|
|
27
28
|
zone: DnsZone;
|
|
28
29
|
progress?: (message: string) => void;
|
|
29
30
|
dependencies?: RetryDependencies;
|
|
30
|
-
}):
|
|
31
|
+
}): DoomainEffect<ReconciliationResult>;
|
|
31
32
|
export {};
|