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.
Files changed (77) hide show
  1. package/dist/commands/auth/clerk.js +3 -2
  2. package/dist/commands/auth/logout/clerk.js +3 -2
  3. package/dist/commands/auth/logout/vercel.js +3 -2
  4. package/dist/commands/auth/vercel.js +5 -4
  5. package/dist/commands/clerk/domains/add.js +3 -2
  6. package/dist/commands/dns/diagnose.js +3 -2
  7. package/dist/commands/dns/point.js +7 -3
  8. package/dist/commands/dns/remove.js +3 -2
  9. package/dist/commands/domains/find.js +3 -2
  10. package/dist/commands/domains/list.js +46 -33
  11. package/dist/commands/link.js +4 -3
  12. package/dist/commands/projects/list.js +5 -2
  13. package/dist/commands/providers/connect.js +14 -23
  14. package/dist/commands/providers/disconnect.js +3 -2
  15. package/dist/commands/providers/status.js +2 -1
  16. package/dist/commands/providers/verify.js +3 -2
  17. package/dist/commands/schema.js +2 -1
  18. package/dist/commands/update.js +2 -1
  19. package/dist/commands/verify.js +5 -2
  20. package/dist/commands/wizard.js +45 -46
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/lib/clerk-domain.d.ts +2 -1
  24. package/dist/lib/clerk-domain.js +122 -110
  25. package/dist/lib/clerk.d.ts +9 -5
  26. package/dist/lib/clerk.js +45 -36
  27. package/dist/lib/command-schema.d.ts +2 -1
  28. package/dist/lib/command-schema.js +9 -9
  29. package/dist/lib/config.d.ts +6 -4
  30. package/dist/lib/config.js +29 -27
  31. package/dist/lib/diagnose-dns.d.ts +8 -5
  32. package/dist/lib/diagnose-dns.js +90 -92
  33. package/dist/lib/dns-propagation.d.ts +5 -4
  34. package/dist/lib/dns-propagation.js +80 -54
  35. package/dist/lib/dns-reconciliation.d.ts +4 -3
  36. package/dist/lib/dns-reconciliation.js +52 -47
  37. package/dist/lib/domain-provider.d.ts +5 -1
  38. package/dist/lib/domain-provider.js +136 -131
  39. package/dist/lib/effect.d.ts +6 -0
  40. package/dist/lib/effect.js +20 -0
  41. package/dist/lib/link-domain.d.ts +3 -2
  42. package/dist/lib/link-domain.js +239 -218
  43. package/dist/lib/point-domain.d.ts +7 -4
  44. package/dist/lib/point-domain.js +92 -90
  45. package/dist/lib/providers/cloudflare/index.d.ts +9 -8
  46. package/dist/lib/providers/cloudflare/index.js +93 -87
  47. package/dist/lib/providers/core/config.d.ts +4 -1
  48. package/dist/lib/providers/core/config.js +17 -13
  49. package/dist/lib/providers/core/http.d.ts +4 -1
  50. package/dist/lib/providers/core/http.js +28 -18
  51. package/dist/lib/providers/core/pagination.d.ts +3 -2
  52. package/dist/lib/providers/core/pagination.js +17 -14
  53. package/dist/lib/providers/core/planner.d.ts +5 -4
  54. package/dist/lib/providers/core/planner.js +18 -15
  55. package/dist/lib/providers/core/types.d.ts +11 -8
  56. package/dist/lib/providers/hostinger/index.d.ts +9 -8
  57. package/dist/lib/providers/hostinger/index.js +74 -72
  58. package/dist/lib/providers/namecheap/index.d.ts +10 -8
  59. package/dist/lib/providers/namecheap/index.js +134 -109
  60. package/dist/lib/providers/registry.d.ts +3 -3
  61. package/dist/lib/providers/registry.js +7 -3
  62. package/dist/lib/providers/spaceship/index.d.ts +9 -8
  63. package/dist/lib/providers/spaceship/index.js +48 -47
  64. package/dist/lib/providers/status.d.ts +2 -1
  65. package/dist/lib/providers/status.js +41 -34
  66. package/dist/lib/public-ip.d.ts +2 -0
  67. package/dist/lib/public-ip.js +14 -0
  68. package/dist/lib/remove-domain.d.ts +6 -3
  69. package/dist/lib/remove-domain.js +83 -77
  70. package/dist/lib/self-update.d.ts +3 -2
  71. package/dist/lib/self-update.js +15 -23
  72. package/dist/lib/vercel-auth.d.ts +2 -1
  73. package/dist/lib/vercel-auth.js +21 -24
  74. package/dist/lib/vercel.d.ts +16 -12
  75. package/dist/lib/vercel.js +174 -114
  76. package/oclif.manifest.json +1 -1
  77. 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): Promise<CommandSchema[] | CommandSchema | undefined>;
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
- async function configuredProviders() {
453
- return (await listProviderStatuses({ verify: false })).map((provider) => ({
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 async function getCommandSchemaForAgents(name) {
476
+ export function getCommandSchemaForAgents(name) {
476
477
  const schema = getCommandSchema(name);
477
478
  if (!schema)
478
- return undefined;
479
- const providers = await configuredProviders();
480
- if (Array.isArray(schema))
481
- return schema.map((item) => withProviderConnections(item, providers));
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
  }
@@ -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(): Promise<DoomainConfig>;
39
- export declare function saveConfig(config: DoomainConfig): Promise<void>;
40
- export declare function updateConfig(updater: (config: DoomainConfig) => DoomainConfig): Promise<DoomainConfig>;
41
- export declare function clearConfig(): Promise<boolean>;
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;
@@ -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 async function loadConfig() {
13
- try {
14
- const data = await readFile(getConfigFile(), 'utf8');
15
- return JSON.parse(data);
16
- }
17
- catch {
18
- return {};
19
- }
20
- }
21
- export async function saveConfig(config) {
22
- const configFile = getConfigFile();
23
- await mkdir(dirname(configFile), { recursive: true });
24
- await writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
25
- }
26
- export async function updateConfig(updater) {
27
- const next = updater(await loadConfig());
28
- await saveConfig(next);
29
- return next;
30
- }
31
- export async function clearConfig() {
32
- try {
33
- await unlink(getConfigFile());
34
- return true;
35
- }
36
- catch {
37
- return false;
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
- }) => Promise<DnsProvider>;
48
- observeDns: (fqdn: string, target: Pick<DnsRecordInput, 'type' | 'value'>, elapsedMs: number) => Promise<DnsResolverObservation[]>;
49
- resolveTarget: (input: Pick<DiagnoseDnsInput, 'account' | 'domain' | 'provider'>) => Promise<ResolvedDnsTarget>;
50
- macOsResolvers?: () => Promise<MacOsResolverMetadata[]>;
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): Promise<DiagnoseDnsResult>;
55
+ export declare function diagnoseDns(input: DiagnoseDnsInput, dependencies?: DiagnoseDnsDependencies): DoomainEffect<DiagnoseDnsResult>;
53
56
  export {};
@@ -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
- async function readMacOsResolvers() {
13
+ function readMacOsResolvers() {
12
14
  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
- }
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, { tolerateProviderAccountErrors: true }),
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 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,
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
- 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
- };
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): Promise<DnsResolverObservation[]>;
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) => Promise<DnsResolverObservation[]>;
42
+ observe?: (fqdn: string, target: ResolveTarget, elapsedMs: number) => DoomainEffect<DnsResolverObservation[]>;
42
43
  now?: () => number;
43
- sleep?: (milliseconds: number) => Promise<void>;
44
- }): Promise<DnsPropagationResult>;
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
- 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.`);
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
- export async function observeDnsRecord(fqdn, target, specs = defaultDnsResolvers, elapsedMs = 0) {
39
- return Promise.all(specs.map(async (spec) => {
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
- const answers = await queryResolver(fqdn, target.type, resolverFor(spec));
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
- catch (error) {
56
- const errorCode = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
57
- ? error.code
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) => 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
- };
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
- await wait(Math.min(5000, remaining));
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) => Promise<void>;
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
- }): Promise<ReconciliationResult>;
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
- }): Promise<ReconciliationResult>;
31
+ }): DoomainEffect<ReconciliationResult>;
31
32
  export {};