doomain 0.1.22 → 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/README.md +1 -1
- 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/version.d.ts +5 -0
- package/dist/commands/version.js +8 -0
- 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 +16 -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 +21 -1
- package/package.json +8 -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.' },
|
|
@@ -218,6 +219,13 @@ export const commandSchemas = [
|
|
|
218
219
|
safeForAgents: true,
|
|
219
220
|
flags: [{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' }],
|
|
220
221
|
},
|
|
222
|
+
{
|
|
223
|
+
name: 'version',
|
|
224
|
+
description: 'Display the installed doomain version.',
|
|
225
|
+
examples: ['doomain version'],
|
|
226
|
+
safeForAgents: true,
|
|
227
|
+
flags: [],
|
|
228
|
+
},
|
|
221
229
|
{
|
|
222
230
|
name: 'update',
|
|
223
231
|
description: 'Install the latest doomain version from npm without using the existing npm cache.',
|
|
@@ -442,8 +450,8 @@ export function getCommandSchema(name) {
|
|
|
442
450
|
return commandSchemas;
|
|
443
451
|
return commandSchemas.find((schema) => schema.name === name);
|
|
444
452
|
}
|
|
445
|
-
|
|
446
|
-
return
|
|
453
|
+
function configuredProviders() {
|
|
454
|
+
return listProviderStatuses({ verify: false }).pipe(Effect.map((providers) => providers.map((provider) => ({
|
|
447
455
|
account: provider.account,
|
|
448
456
|
accountLabel: provider.accountLabel,
|
|
449
457
|
configured: provider.configured,
|
|
@@ -453,7 +461,7 @@ async function configuredProviders() {
|
|
|
453
461
|
id: provider.id,
|
|
454
462
|
isDefaultAccount: provider.isDefaultAccount,
|
|
455
463
|
isPreferredProvider: provider.isPreferredProvider,
|
|
456
|
-
}));
|
|
464
|
+
}))));
|
|
457
465
|
}
|
|
458
466
|
function withProviderConnections(schema, providers) {
|
|
459
467
|
if (schema.name !== 'link' &&
|
|
@@ -465,12 +473,11 @@ function withProviderConnections(schema, providers) {
|
|
|
465
473
|
return schema;
|
|
466
474
|
return { ...schema, configuredProviders: providers };
|
|
467
475
|
}
|
|
468
|
-
export
|
|
476
|
+
export function getCommandSchemaForAgents(name) {
|
|
469
477
|
const schema = getCommandSchema(name);
|
|
470
478
|
if (!schema)
|
|
471
|
-
return undefined;
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
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)));
|
|
476
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 {};
|