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,53 +1,58 @@
1
+ import { Effect } from 'effect';
1
2
  import { desiredSlotPostcondition, recordMatchesSelector } from './dns-records.js';
2
3
  import { DoomainError } from './errors.js';
3
- const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
4
- export async function reconcileDesiredRecord(input) {
5
- const now = input.dependencies?.now ?? Date.now;
6
- const wait = input.dependencies?.sleep ?? sleep;
7
- const deadline = now() + (input.timeoutMs ?? 30_000);
8
- let attempts = 0;
9
- const appliedChanges = 0;
10
- while (true) {
11
- attempts += 1;
12
- const records = await input.provider.listRecords(input.zone);
13
- const state = desiredSlotPostcondition(records, input.desired);
14
- if (state.reconciled)
15
- return { appliedChanges, attempts, observed: state.observed, reconciled: true };
16
- if (now() >= deadline) {
17
- throw new DoomainError('DNS_RECONCILIATION_INCOMPLETE', `The DNS provider accepted the change, but the ${input.desired.type} ${input.desired.name} slot is not reconciled yet.`, {
18
- attempts,
19
- expected: input.desired,
20
- observed: state.observed,
21
- recovery: 'Retry the command. Do not treat the DNS change as complete until reconciled is true.',
22
- });
4
+ const sleep = (milliseconds) => Effect.sleep(milliseconds);
5
+ export function reconcileDesiredRecord(input) {
6
+ return Effect.gen(function* () {
7
+ const now = input.dependencies?.now ?? Date.now;
8
+ const wait = input.dependencies?.sleep ?? sleep;
9
+ const deadline = now() + (input.timeoutMs ?? 30_000);
10
+ let attempts = 0;
11
+ const appliedChanges = 0;
12
+ while (true) {
13
+ attempts += 1;
14
+ const records = yield* input.provider.listRecords(input.zone);
15
+ const state = desiredSlotPostcondition(records, input.desired);
16
+ if (state.reconciled)
17
+ return { appliedChanges, attempts, observed: state.observed, reconciled: true };
18
+ if (now() >= deadline) {
19
+ return yield* Effect.fail(new DoomainError('DNS_RECONCILIATION_INCOMPLETE', `The DNS provider accepted the change, but the ${input.desired.type} ${input.desired.name} slot is not reconciled yet.`, {
20
+ attempts,
21
+ expected: input.desired,
22
+ observed: state.observed,
23
+ recovery: 'Retry the command. Do not treat the DNS change as complete until reconciled is true.',
24
+ }));
25
+ }
26
+ input.progress?.('Waiting for the DNS provider to publish the accepted change');
27
+ const remaining = deadline - now();
28
+ yield* wait(Math.min(input.intervalMs ?? 1000, Math.max(0, remaining)));
23
29
  }
24
- input.progress?.('Waiting for the DNS provider to publish the accepted change');
25
- const remaining = deadline - now();
26
- await wait(Math.min(input.intervalMs ?? 1000, Math.max(0, remaining)));
27
- }
30
+ });
28
31
  }
29
- export async function reconcileRecordRemoval(input) {
30
- const now = input.dependencies?.now ?? Date.now;
31
- const wait = input.dependencies?.sleep ?? sleep;
32
- const deadline = now() + (input.timeoutMs ?? 30_000);
33
- let attempts = 0;
34
- const appliedChanges = 0;
35
- while (true) {
36
- attempts += 1;
37
- const records = await input.provider.listRecords(input.zone);
38
- const observed = records.filter((record) => recordMatchesSelector(record, input.selector));
39
- if (observed.length === 0)
40
- return { appliedChanges, attempts, observed, reconciled: true };
41
- if (now() >= deadline) {
42
- throw new DoomainError('DNS_RECONCILIATION_INCOMPLETE', `The DNS provider accepted the deletion, but ${observed.length} matching record${observed.length === 1 ? '' : 's'} remain.`, {
43
- attempts,
44
- observed,
45
- selector: input.selector,
46
- recovery: 'Retry the command. Do not treat the DNS deletion as complete until reconciled is true.',
47
- });
32
+ export function reconcileRecordRemoval(input) {
33
+ return Effect.gen(function* () {
34
+ const now = input.dependencies?.now ?? Date.now;
35
+ const wait = input.dependencies?.sleep ?? sleep;
36
+ const deadline = now() + (input.timeoutMs ?? 30_000);
37
+ let attempts = 0;
38
+ const appliedChanges = 0;
39
+ while (true) {
40
+ attempts += 1;
41
+ const records = yield* input.provider.listRecords(input.zone);
42
+ const observed = records.filter((record) => recordMatchesSelector(record, input.selector));
43
+ if (observed.length === 0)
44
+ return { appliedChanges, attempts, observed, reconciled: true };
45
+ if (now() >= deadline) {
46
+ return yield* Effect.fail(new DoomainError('DNS_RECONCILIATION_INCOMPLETE', `The DNS provider accepted the deletion, but ${observed.length} matching record${observed.length === 1 ? '' : 's'} remain.`, {
47
+ attempts,
48
+ observed,
49
+ selector: input.selector,
50
+ recovery: 'Retry the command. Do not treat the DNS deletion as complete until reconciled is true.',
51
+ }));
52
+ }
53
+ input.progress?.('Waiting for the DNS provider to publish the accepted deletion');
54
+ const remaining = deadline - now();
55
+ yield* wait(Math.min(input.intervalMs ?? 1000, Math.max(0, remaining)));
48
56
  }
49
- input.progress?.('Waiting for the DNS provider to publish the accepted deletion');
50
- const remaining = deadline - now();
51
- await wait(Math.min(input.intervalMs ?? 1000, Math.max(0, remaining)));
52
- }
57
+ });
53
58
  }
@@ -1,3 +1,4 @@
1
+ import { type DoomainEffect } from './effect.js';
1
2
  import { type DoomainErrorCode } from './errors.js';
2
3
  export interface FindDomainProviderInput {
3
4
  account?: string;
@@ -33,6 +34,7 @@ export interface ResolveProviderTargetInput extends FindDomainProviderInput {
33
34
  }
34
35
  export interface ResolveProviderTargetOptions {
35
36
  tolerateProviderAccountErrors?: boolean;
37
+ transportErrorCode?: DoomainErrorCode;
36
38
  }
37
39
  export interface ResolvedDnsTarget {
38
40
  account: string;
@@ -48,6 +50,8 @@ export interface ResolvedDnsTarget {
48
50
  };
49
51
  warnings: ProviderSearchWarning[];
50
52
  }
51
- export declare function resolveProviderTarget(input: ResolveProviderTargetInput, options?: ResolveProviderTargetOptions): Promise<ResolvedDnsTarget>;
53
+ export declare function resolveProviderTarget(input: ResolveProviderTargetInput, options?: ResolveProviderTargetOptions): DoomainEffect<ResolvedDnsTarget>;
52
54
  /** Find the configured DNS provider account with the longest zone match for a domain. */
55
+ export declare function findDomainProviderEffect(input: FindDomainProviderInput): DoomainEffect<DomainProviderResult>;
56
+ /** Promise facade retained for the package's public programmatic API. */
53
57
  export declare function findDomainProvider(input: FindDomainProviderInput): Promise<DomainProviderResult>;
@@ -1,4 +1,6 @@
1
+ import { Effect } from 'effect';
1
2
  import { loadConfig } from './config.js';
3
+ import { runDoomainEffect, trySync } from './effect.js';
2
4
  import { DoomainError } from './errors.js';
3
5
  import { DEFAULT_PROVIDER_ACCOUNT, isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount, } from './providers/core/config.js';
4
6
  import { createProvider, getProviderDefinition, listProviderDefinitions } from './providers/registry.js';
@@ -55,46 +57,43 @@ function searchError(error) {
55
57
  message: error instanceof Error ? error.message : String(error),
56
58
  };
57
59
  }
58
- async function loadProviderZones(definition, account) {
59
- const provider = await createProvider(definition.id, { account: account.account });
60
- const zones = await provider.listZones();
61
- return {
62
- candidates: zones.map((zone) => ({
63
- account: account.account,
64
- isDefaultAccount: account.isDefaultAccount,
65
- provider: definition.id,
66
- providerName: definition.displayName,
67
- zone,
68
- })),
69
- search: {
70
- account: account.account,
71
- displayName: definition.displayName,
72
- id: definition.id,
73
- isDefaultAccount: account.isDefaultAccount,
74
- zones: zones.map((zone) => zone.name),
75
- },
76
- };
77
- }
78
- async function loadProviderZonesSafely(definition, account) {
79
- try {
80
- return await loadProviderZones(definition, account);
81
- }
82
- catch (error) {
60
+ function loadProviderZones(definition, account, transportErrorCode) {
61
+ return Effect.gen(function* () {
62
+ const provider = yield* createProvider(definition.id, { account: account.account, transportErrorCode });
63
+ const zones = yield* provider.listZones();
83
64
  return {
84
- candidates: [],
65
+ candidates: zones.map((zone) => ({
66
+ account: account.account,
67
+ isDefaultAccount: account.isDefaultAccount,
68
+ provider: definition.id,
69
+ providerName: definition.displayName,
70
+ zone,
71
+ })),
85
72
  search: {
86
73
  account: account.account,
87
74
  displayName: definition.displayName,
88
- error: searchError(error),
89
75
  id: definition.id,
90
76
  isDefaultAccount: account.isDefaultAccount,
91
- zones: [],
77
+ zones: zones.map((zone) => zone.name),
92
78
  },
93
79
  };
94
- }
80
+ });
95
81
  }
96
- async function providerConnectionDetails() {
97
- return (await listProviderStatuses({ verify: false })).map((provider) => ({
82
+ function loadProviderZonesSafely(definition, account, transportErrorCode) {
83
+ return loadProviderZones(definition, account, transportErrorCode).pipe(Effect.catchAll((error) => Effect.succeed({
84
+ candidates: [],
85
+ search: {
86
+ account: account.account,
87
+ displayName: definition.displayName,
88
+ error: searchError(error),
89
+ id: definition.id,
90
+ isDefaultAccount: account.isDefaultAccount,
91
+ zones: [],
92
+ },
93
+ })));
94
+ }
95
+ function providerConnectionDetails() {
96
+ return listProviderStatuses({ verify: false }).pipe(Effect.map((providers) => providers.map((provider) => ({
98
97
  configured: provider.configured,
99
98
  account: provider.account,
100
99
  default: provider.default,
@@ -102,7 +101,7 @@ async function providerConnectionDetails() {
102
101
  docsUrl: provider.docsUrl,
103
102
  id: provider.id,
104
103
  isDefaultAccount: provider.isDefaultAccount,
105
- }));
104
+ }))));
106
105
  }
107
106
  function searchWarnings(searches) {
108
107
  return searches.flatMap((search) => search.error
@@ -117,89 +116,99 @@ function searchWarnings(searches) {
117
116
  ]
118
117
  : []);
119
118
  }
120
- async function loadConfiguredProviderZones(providerId, accountInput, tolerateProviderAccountErrors = false) {
121
- const config = await loadConfig();
122
- const account = accountInput ? normalizeProviderAccount(accountInput) : undefined;
123
- if (providerId) {
124
- const definition = getProviderDefinition(providerId);
125
- const accounts = account
126
- ? [explicitAccountRef(definition.id, account)]
127
- : listConfiguredProviderAccounts(config, definition);
128
- const selectedAccounts = accounts.length > 0 ? accounts : [defaultAccountRef(definition.id)];
129
- const tolerateAccountErrors = tolerateProviderAccountErrors && !account && selectedAccounts.length > 1;
130
- const results = await Promise.all(selectedAccounts.map((ref) => tolerateAccountErrors ? loadProviderZonesSafely(definition, ref) : loadProviderZones(definition, ref)));
119
+ function loadConfiguredProviderZones(providerId, accountInput, tolerateProviderAccountErrors = false, transportErrorCode = 'DOMAIN_PROVIDER_DISCOVERY_FAILED') {
120
+ return Effect.gen(function* () {
121
+ const config = yield* loadConfig();
122
+ const account = accountInput
123
+ ? yield* trySync(() => normalizeProviderAccount(accountInput), 'INVALID_INPUT')
124
+ : undefined;
125
+ if (providerId) {
126
+ const definition = yield* trySync(() => getProviderDefinition(providerId), 'PROVIDER_NOT_FOUND');
127
+ const accounts = account
128
+ ? [explicitAccountRef(definition.id, account)]
129
+ : yield* trySync(() => listConfiguredProviderAccounts(config, definition), 'INVALID_INPUT');
130
+ const selectedAccounts = accounts.length > 0 ? accounts : [defaultAccountRef(definition.id)];
131
+ const tolerateAccountErrors = tolerateProviderAccountErrors && !account && selectedAccounts.length > 1;
132
+ const results = yield* Effect.all(selectedAccounts.map((ref) => tolerateAccountErrors
133
+ ? loadProviderZonesSafely(definition, ref, transportErrorCode)
134
+ : loadProviderZones(definition, ref, transportErrorCode)), { concurrency: 'unbounded' });
135
+ return {
136
+ accountInferred: account === undefined,
137
+ candidates: results.flatMap((result) => result.candidates),
138
+ providerInferred: false,
139
+ searched: results.map((result) => result.search),
140
+ };
141
+ }
142
+ const providerAccounts = yield* trySync(() => listProviderDefinitions().flatMap((definition) => listConfiguredProviderAccounts(config, definition)
143
+ .filter((ref) => !account || ref.account === account)
144
+ .map((ref) => ({ definition, ref }))), 'INVALID_INPUT');
145
+ if (providerAccounts.length === 0) {
146
+ const message = account
147
+ ? `No DNS provider account named ${account} is configured. Run \`doomain providers connect <provider> --account ${account}\` first.`
148
+ : 'No DNS provider is configured. Run `doomain providers connect` first.';
149
+ return yield* Effect.fail(new DoomainError('CONFIG_NOT_FOUND', message, {
150
+ account,
151
+ configuredProviders: yield* providerConnectionDetails(),
152
+ recovery: 'Connect the DNS provider that owns this domain, then retry `doomain link <domain> --json`.',
153
+ suggestedCommands: account
154
+ ? [`doomain providers connect <provider> --account ${account}`, 'doomain link <domain> --json']
155
+ : ['doomain providers connect', 'doomain link <domain> --json'],
156
+ }));
157
+ }
158
+ const results = yield* Effect.all(providerAccounts.map(({ definition, ref }) => tolerateProviderAccountErrors
159
+ ? loadProviderZonesSafely(definition, ref, transportErrorCode)
160
+ : loadProviderZones(definition, ref, transportErrorCode)), { concurrency: 'unbounded' });
131
161
  return {
132
162
  accountInferred: account === undefined,
133
163
  candidates: results.flatMap((result) => result.candidates),
134
- providerInferred: false,
164
+ providerInferred: true,
135
165
  searched: results.map((result) => result.search),
136
166
  };
137
- }
138
- const providerAccounts = listProviderDefinitions().flatMap((definition) => listConfiguredProviderAccounts(config, definition)
139
- .filter((ref) => !account || ref.account === account)
140
- .map((ref) => ({ definition, ref })));
141
- if (providerAccounts.length === 0) {
142
- const message = account
143
- ? `No DNS provider account named ${account} is configured. Run \`doomain providers connect <provider> --account ${account}\` first.`
144
- : 'No DNS provider is configured. Run `doomain providers connect` first.';
145
- throw new DoomainError('CONFIG_NOT_FOUND', message, {
146
- account,
147
- configuredProviders: await providerConnectionDetails(),
148
- recovery: 'Connect the DNS provider that owns this domain, then retry `doomain link <domain> --json`.',
149
- suggestedCommands: account
150
- ? [`doomain providers connect <provider> --account ${account}`, 'doomain link <domain> --json']
151
- : ['doomain providers connect', 'doomain link <domain> --json'],
152
- });
153
- }
154
- const results = await Promise.all(providerAccounts.map(({ definition, ref }) => tolerateProviderAccountErrors ? loadProviderZonesSafely(definition, ref) : loadProviderZones(definition, ref)));
155
- return {
156
- accountInferred: account === undefined,
157
- candidates: results.flatMap((result) => result.candidates),
158
- providerInferred: true,
159
- searched: results.map((result) => result.search),
160
- };
167
+ });
161
168
  }
162
- export async function resolveProviderTarget(input, options = {}) {
163
- const requested = resolveRequestedDomain(input);
164
- const zones = await loadConfiguredProviderZones(input.provider, input.account, options.tolerateProviderAccountErrors);
165
- const matches = zones.candidates
166
- .filter((candidate) => zoneMatchesDomain(requested.fullDomain, candidate.zone.name, requested.forceExactZone))
167
- .sort((a, b) => b.zone.name.length - a.zone.name.length);
168
- if (matches.length === 0) {
169
- const account = input.account ? normalizeProviderAccount(input.account) : undefined;
170
- const providerMessage = input.provider
171
- ? `${getProviderDefinition(input.provider).displayName}${account ? ` account ${account}` : ''} does not have a matching DNS zone for ${requested.fullDomain}.`
172
- : `No configured DNS provider has a matching DNS zone for ${requested.fullDomain}.`;
173
- throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', providerMessage, {
174
- account,
175
- configuredProviders: await providerConnectionDetails(),
176
- domain: requested.fullDomain,
177
- recovery: 'Retry with --provider <id> --account <alias> only if another configured provider account owns this zone. Otherwise connect the DNS provider account that owns this domain.',
178
- searchedZones: zones.searched,
179
- suggestedCommands: [
180
- `doomain link ${requested.fullDomain} --provider <id> --account <alias> --json`,
181
- 'doomain providers connect',
182
- ],
183
- });
184
- }
185
- const bestLength = matches[0].zone.name.length;
186
- const bestMatches = matches.filter((candidate) => candidate.zone.name.length === bestLength);
187
- const uniqueBestMatches = bestMatches.filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider &&
188
- item.account === candidate.account &&
189
- item.zone.name === candidate.zone.name) === index);
190
- if (uniqueBestMatches.length > 1) {
191
- throw new DoomainError('PROVIDER_ZONE_AMBIGUOUS', `Multiple DNS provider accounts have a matching DNS zone for ${requested.fullDomain}. Pass --provider and --account to choose one.`, { candidates: candidateDetails(uniqueBestMatches), domain: requested.fullDomain });
192
- }
193
- const selected = uniqueBestMatches[0];
194
- return {
195
- account: selected.account,
196
- accountInferred: zones.accountInferred,
197
- isDefaultAccount: selected.isDefaultAccount,
198
- provider: selected.provider,
199
- providerInferred: zones.providerInferred,
200
- target: targetFromZone(requested.fullDomain, selected.zone.name),
201
- warnings: searchWarnings(zones.searched),
202
- };
169
+ export function resolveProviderTarget(input, options = {}) {
170
+ return Effect.gen(function* () {
171
+ const requested = yield* trySync(() => resolveRequestedDomain(input), 'INVALID_INPUT');
172
+ const zones = yield* loadConfiguredProviderZones(input.provider, input.account, options.tolerateProviderAccountErrors, options.transportErrorCode);
173
+ const matches = zones.candidates
174
+ .filter((candidate) => zoneMatchesDomain(requested.fullDomain, candidate.zone.name, requested.forceExactZone))
175
+ .sort((a, b) => b.zone.name.length - a.zone.name.length);
176
+ if (matches.length === 0) {
177
+ const account = input.account ? normalizeProviderAccount(input.account) : undefined;
178
+ const providerMessage = input.provider
179
+ ? `${getProviderDefinition(input.provider).displayName}${account ? ` account ${account}` : ''} does not have a matching DNS zone for ${requested.fullDomain}.`
180
+ : `No configured DNS provider has a matching DNS zone for ${requested.fullDomain}.`;
181
+ return yield* Effect.fail(new DoomainError('PROVIDER_ZONE_NOT_FOUND', providerMessage, {
182
+ account,
183
+ configuredProviders: yield* providerConnectionDetails(),
184
+ domain: requested.fullDomain,
185
+ recovery: 'Retry with --provider <id> --account <alias> only if another configured provider account owns this zone. Otherwise connect the DNS provider account that owns this domain.',
186
+ searchedZones: zones.searched,
187
+ suggestedCommands: [
188
+ `doomain link ${requested.fullDomain} --provider <id> --account <alias> --json`,
189
+ 'doomain providers connect',
190
+ ],
191
+ }));
192
+ }
193
+ const bestLength = matches[0].zone.name.length;
194
+ const bestMatches = matches.filter((candidate) => candidate.zone.name.length === bestLength);
195
+ const uniqueBestMatches = bestMatches.filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider &&
196
+ item.account === candidate.account &&
197
+ item.zone.name === candidate.zone.name) === index);
198
+ if (uniqueBestMatches.length > 1) {
199
+ return yield* Effect.fail(new DoomainError('PROVIDER_ZONE_AMBIGUOUS', `Multiple DNS provider accounts have a matching DNS zone for ${requested.fullDomain}. Pass --provider and --account to choose one.`, { candidates: candidateDetails(uniqueBestMatches), domain: requested.fullDomain }));
200
+ }
201
+ const selected = uniqueBestMatches[0];
202
+ return {
203
+ account: selected.account,
204
+ accountInferred: zones.accountInferred,
205
+ isDefaultAccount: selected.isDefaultAccount,
206
+ provider: selected.provider,
207
+ providerInferred: zones.providerInferred,
208
+ target: targetFromZone(requested.fullDomain, selected.zone.name),
209
+ warnings: searchWarnings(zones.searched),
210
+ };
211
+ });
203
212
  }
204
213
  function discoveryError(error, domain) {
205
214
  if (error.code !== 'CONFIG_NOT_FOUND' && error.code !== 'PROVIDER_ZONE_NOT_FOUND')
@@ -212,26 +221,22 @@ function discoveryError(error, domain) {
212
221
  });
213
222
  }
214
223
  /** Find the configured DNS provider account with the longest zone match for a domain. */
215
- export async function findDomainProvider(input) {
216
- try {
217
- const resolved = await resolveProviderTarget(input, { tolerateProviderAccountErrors: true });
218
- return {
219
- account: resolved.account,
220
- accountInferred: resolved.accountInferred,
221
- complete: resolved.warnings.length === 0,
222
- domain: resolved.target.fullDomain,
223
- isApex: resolved.target.isApex,
224
- isDefaultAccount: resolved.isDefaultAccount,
225
- provider: resolved.provider,
226
- providerInferred: resolved.providerInferred,
227
- recordName: resolved.target.recordName,
228
- warnings: resolved.warnings,
229
- zoneDomain: resolved.target.zoneDomain,
230
- };
231
- }
232
- catch (error) {
233
- if (error instanceof DoomainError)
234
- throw discoveryError(error, input.domain);
235
- throw error;
236
- }
224
+ export function findDomainProviderEffect(input) {
225
+ return resolveProviderTarget(input, { tolerateProviderAccountErrors: true }).pipe(Effect.map((resolved) => ({
226
+ account: resolved.account,
227
+ accountInferred: resolved.accountInferred,
228
+ complete: resolved.warnings.length === 0,
229
+ domain: resolved.target.fullDomain,
230
+ isApex: resolved.target.isApex,
231
+ isDefaultAccount: resolved.isDefaultAccount,
232
+ provider: resolved.provider,
233
+ providerInferred: resolved.providerInferred,
234
+ recordName: resolved.target.recordName,
235
+ warnings: resolved.warnings,
236
+ zoneDomain: resolved.target.zoneDomain,
237
+ })), Effect.mapError((error) => discoveryError(error, input.domain)));
238
+ }
239
+ /** Promise facade retained for the package's public programmatic API. */
240
+ export function findDomainProvider(input) {
241
+ return runDoomainEffect(findDomainProviderEffect(input));
237
242
  }
@@ -0,0 +1,6 @@
1
+ import { Effect } from 'effect';
2
+ import { type DoomainError, type DoomainErrorCode } from './errors.js';
3
+ export type DoomainEffect<A, E = DoomainError> = Effect.Effect<A, E>;
4
+ export declare function trySync<A>(evaluate: () => A, fallbackCode: DoomainErrorCode): DoomainEffect<A>;
5
+ export declare function tryPromise<A>(evaluate: () => PromiseLike<A>, fallbackCode: DoomainErrorCode): DoomainEffect<A>;
6
+ export declare function runDoomainEffect<A>(effect: DoomainEffect<A>): Promise<A>;
@@ -0,0 +1,20 @@
1
+ import { Effect } from 'effect';
2
+ import { toDoomainError } from './errors.js';
3
+ export function trySync(evaluate, fallbackCode) {
4
+ return Effect.try({
5
+ try: evaluate,
6
+ catch: (error) => toDoomainError(error, fallbackCode),
7
+ });
8
+ }
9
+ export function tryPromise(evaluate, fallbackCode) {
10
+ return Effect.tryPromise({
11
+ try: evaluate,
12
+ catch: (error) => toDoomainError(error, fallbackCode),
13
+ });
14
+ }
15
+ export async function runDoomainEffect(effect) {
16
+ const result = await Effect.runPromise(Effect.either(effect));
17
+ if (result._tag === 'Left')
18
+ throw result.left;
19
+ return result.right;
20
+ }
@@ -1,3 +1,4 @@
1
+ import { type DoomainEffect } from './effect.js';
1
2
  import type { DnsConflict, DnsRecordInput } from './providers/types.js';
2
3
  export interface LinkDomainInput {
3
4
  provider?: string;
@@ -61,5 +62,5 @@ export interface LinkDomainResult extends LinkDomainPlan {
61
62
  }
62
63
  export declare function withProviderRecordOptions(provider: string, record: DnsRecordInput): DnsRecordInput;
63
64
  export declare function verificationRecords(raw: unknown, zoneDomain: string): DnsRecordInput[];
64
- export declare function createLinkPlan(input: LinkDomainInput): Promise<LinkDomainPlan>;
65
- export declare function linkDomain(input: LinkDomainInput): Promise<LinkDomainResult>;
65
+ export declare function createLinkPlan(input: LinkDomainInput): DoomainEffect<LinkDomainPlan>;
66
+ export declare function linkDomain(input: LinkDomainInput): DoomainEffect<LinkDomainResult>;