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.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/auth/clerk.js +3 -2
  3. package/dist/commands/auth/logout/clerk.js +3 -2
  4. package/dist/commands/auth/logout/vercel.js +3 -2
  5. package/dist/commands/auth/vercel.js +5 -4
  6. package/dist/commands/clerk/domains/add.js +3 -2
  7. package/dist/commands/dns/diagnose.js +3 -2
  8. package/dist/commands/dns/point.js +7 -3
  9. package/dist/commands/dns/remove.js +3 -2
  10. package/dist/commands/domains/find.js +3 -2
  11. package/dist/commands/domains/list.js +46 -33
  12. package/dist/commands/link.js +4 -3
  13. package/dist/commands/projects/list.js +5 -2
  14. package/dist/commands/providers/connect.js +14 -23
  15. package/dist/commands/providers/disconnect.js +3 -2
  16. package/dist/commands/providers/status.js +2 -1
  17. package/dist/commands/providers/verify.js +3 -2
  18. package/dist/commands/schema.js +2 -1
  19. package/dist/commands/update.js +2 -1
  20. package/dist/commands/verify.js +5 -2
  21. package/dist/commands/version.d.ts +5 -0
  22. package/dist/commands/version.js +8 -0
  23. package/dist/commands/wizard.js +45 -46
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/lib/clerk-domain.d.ts +2 -1
  27. package/dist/lib/clerk-domain.js +122 -110
  28. package/dist/lib/clerk.d.ts +9 -5
  29. package/dist/lib/clerk.js +45 -36
  30. package/dist/lib/command-schema.d.ts +2 -1
  31. package/dist/lib/command-schema.js +16 -9
  32. package/dist/lib/config.d.ts +6 -4
  33. package/dist/lib/config.js +29 -27
  34. package/dist/lib/diagnose-dns.d.ts +8 -5
  35. package/dist/lib/diagnose-dns.js +90 -92
  36. package/dist/lib/dns-propagation.d.ts +5 -4
  37. package/dist/lib/dns-propagation.js +80 -54
  38. package/dist/lib/dns-reconciliation.d.ts +4 -3
  39. package/dist/lib/dns-reconciliation.js +52 -47
  40. package/dist/lib/domain-provider.d.ts +5 -1
  41. package/dist/lib/domain-provider.js +136 -131
  42. package/dist/lib/effect.d.ts +6 -0
  43. package/dist/lib/effect.js +20 -0
  44. package/dist/lib/link-domain.d.ts +3 -2
  45. package/dist/lib/link-domain.js +239 -218
  46. package/dist/lib/point-domain.d.ts +7 -4
  47. package/dist/lib/point-domain.js +92 -90
  48. package/dist/lib/providers/cloudflare/index.d.ts +9 -8
  49. package/dist/lib/providers/cloudflare/index.js +93 -87
  50. package/dist/lib/providers/core/config.d.ts +4 -1
  51. package/dist/lib/providers/core/config.js +17 -13
  52. package/dist/lib/providers/core/http.d.ts +4 -1
  53. package/dist/lib/providers/core/http.js +28 -18
  54. package/dist/lib/providers/core/pagination.d.ts +3 -2
  55. package/dist/lib/providers/core/pagination.js +17 -14
  56. package/dist/lib/providers/core/planner.d.ts +5 -4
  57. package/dist/lib/providers/core/planner.js +18 -15
  58. package/dist/lib/providers/core/types.d.ts +11 -8
  59. package/dist/lib/providers/hostinger/index.d.ts +9 -8
  60. package/dist/lib/providers/hostinger/index.js +74 -72
  61. package/dist/lib/providers/namecheap/index.d.ts +10 -8
  62. package/dist/lib/providers/namecheap/index.js +134 -109
  63. package/dist/lib/providers/registry.d.ts +3 -3
  64. package/dist/lib/providers/registry.js +7 -3
  65. package/dist/lib/providers/spaceship/index.d.ts +9 -8
  66. package/dist/lib/providers/spaceship/index.js +48 -47
  67. package/dist/lib/providers/status.d.ts +2 -1
  68. package/dist/lib/providers/status.js +41 -34
  69. package/dist/lib/public-ip.d.ts +2 -0
  70. package/dist/lib/public-ip.js +14 -0
  71. package/dist/lib/remove-domain.d.ts +6 -3
  72. package/dist/lib/remove-domain.js +83 -77
  73. package/dist/lib/self-update.d.ts +3 -2
  74. package/dist/lib/self-update.js +15 -23
  75. package/dist/lib/vercel-auth.d.ts +2 -1
  76. package/dist/lib/vercel-auth.js +21 -24
  77. package/dist/lib/vercel.d.ts +16 -12
  78. package/dist/lib/vercel.js +174 -114
  79. package/oclif.manifest.json +21 -1
  80. package/package.json +8 -1
@@ -1,13 +1,15 @@
1
+ import { Effect } from 'effect';
1
2
  import { waitForDnsPropagation } from './dns-propagation.js';
2
3
  import { reconcileDesiredRecord } from './dns-reconciliation.js';
3
4
  import { inferAddressRecordType, normalizeAddressRecordTarget } from './dns-records.js';
4
5
  import { resolveProviderTarget } from './domain-provider.js';
6
+ import { tryPromise, trySync } from './effect.js';
5
7
  import { DoomainError } from './errors.js';
6
8
  import { withProviderRecordOptions } from './link-domain.js';
7
9
  import { createProvider } from './providers/registry.js';
8
10
  const defaultDependencies = {
9
11
  createProvider,
10
- resolveTarget: resolveProviderTarget,
12
+ resolveTarget: (input) => resolveProviderTarget(input, { transportErrorCode: 'DNS_POINT_FAILED' }),
11
13
  };
12
14
  export function createPointRecord(input) {
13
15
  const recordType = input.recordType ?? inferAddressRecordType(input.target.trim());
@@ -46,6 +48,9 @@ function dnsTargetConflictError(warning) {
46
48
  });
47
49
  }
48
50
  function providerResolutionError(error, input) {
51
+ if (error.code === 'PROVIDER_API_ERROR' && error.details instanceof Error) {
52
+ return new DoomainError('DNS_POINT_FAILED', error.details.message);
53
+ }
49
54
  if (error.code !== 'CONFIG_NOT_FOUND' && error.code !== 'PROVIDER_ZONE_NOT_FOUND')
50
55
  return error;
51
56
  const details = error.details && typeof error.details === 'object' ? error.details : {};
@@ -75,104 +80,101 @@ function validateTtl(provider, ttl) {
75
80
  throw new DoomainError('INVALID_INPUT', `${provider.name} requires a TTL no greater than ${maxTtl} seconds.`);
76
81
  }
77
82
  }
78
- export async function pointDomain(input, dependencies = defaultDependencies) {
79
- let resolved;
80
- try {
81
- resolved = await dependencies.resolveTarget(input);
82
- }
83
- catch (error) {
84
- if (error instanceof DoomainError)
85
- throw providerResolutionError(error, input);
86
- throw error;
87
- }
88
- const record = createPointRecord({
89
- provider: resolved.provider,
90
- recordName: resolved.target.recordName,
91
- recordType: input.recordType,
92
- target: input.target,
93
- ttl: input.ttl,
94
- });
95
- const provider = await dependencies.createProvider(resolved.provider, { account: resolved.account });
96
- validateTtl(provider, record.ttl);
97
- if (!provider.capabilities.recordTypes.includes(record.type)) {
98
- throw new DoomainError('PROVIDER_UNSUPPORTED_RECORD', `${provider.name} does not support ${record.type} records.`);
99
- }
100
- if (resolved.target.isApex && record.type === 'CNAME' && !provider.capabilities.supportsApexCname) {
101
- throw new DoomainError('PROVIDER_UNSUPPORTED_RECORD', `${provider.name} does not support CNAME records at the zone apex. Use an A or AAAA target instead.`);
102
- }
103
- if (input.dryRun) {
83
+ export function pointDomain(input, dependencies = defaultDependencies) {
84
+ return Effect.gen(function* () {
85
+ const resolved = yield* dependencies
86
+ .resolveTarget(input)
87
+ .pipe(Effect.mapError((error) => providerResolutionError(error, input)));
88
+ const record = yield* trySync(() => createPointRecord({
89
+ provider: resolved.provider,
90
+ recordName: resolved.target.recordName,
91
+ recordType: input.recordType,
92
+ target: input.target,
93
+ ttl: input.ttl,
94
+ }), 'INVALID_INPUT');
95
+ const provider = yield* dependencies.createProvider(resolved.provider, {
96
+ account: resolved.account,
97
+ transportErrorCode: 'DNS_POINT_FAILED',
98
+ });
99
+ yield* trySync(() => validateTtl(provider, record.ttl), 'INVALID_INPUT');
100
+ if (!provider.capabilities.recordTypes.includes(record.type)) {
101
+ return yield* Effect.fail(new DoomainError('PROVIDER_UNSUPPORTED_RECORD', `${provider.name} does not support ${record.type} records.`));
102
+ }
103
+ if (resolved.target.isApex && record.type === 'CNAME' && !provider.capabilities.supportsApexCname) {
104
+ return yield* Effect.fail(new DoomainError('PROVIDER_UNSUPPORTED_RECORD', `${provider.name} does not support CNAME records at the zone apex. Use an A or AAAA target instead.`));
105
+ }
106
+ if (input.dryRun) {
107
+ return {
108
+ account: resolved.account,
109
+ accountInferred: resolved.accountInferred,
110
+ domain: resolved.target.fullDomain,
111
+ dryRun: true,
112
+ isDefaultAccount: resolved.isDefaultAccount,
113
+ provider: resolved.provider,
114
+ providerInferred: resolved.providerInferred,
115
+ propagated: false,
116
+ propagation: { elapsedMs: 0, expected: record.value, observations: [], status: 'not_checked' },
117
+ reconciled: false,
118
+ reconciliationAttempts: 0,
119
+ record,
120
+ skipped: [],
121
+ updated: false,
122
+ zoneDomain: resolved.target.zoneDomain,
123
+ };
124
+ }
125
+ const zone = yield* provider.getZone(resolved.target.zoneDomain);
126
+ if (!zone) {
127
+ return yield* Effect.fail(new DoomainError('PROVIDER_ZONE_NOT_FOUND', `${provider.name} does not have a DNS zone for ${resolved.target.zoneDomain}.`));
128
+ }
129
+ input.progress?.(`Planning DNS change in ${provider.name}`);
130
+ let force = Boolean(input.force);
131
+ let plan = yield* provider.planChanges(zone, [record], { force });
132
+ if (!force && plan.conflicts.length > 0) {
133
+ const warning = conflictWarning(resolved, provider, record, plan.conflicts);
134
+ force = input.confirmDnsOverride
135
+ ? yield* tryPromise(() => input.confirmDnsOverride?.(warning) ?? Promise.resolve(false), 'DNS_POINT_FAILED')
136
+ : false;
137
+ if (!force)
138
+ return yield* Effect.fail(dnsTargetConflictError(warning));
139
+ plan = yield* provider.planChanges(zone, [record], { force: true });
140
+ }
141
+ input.progress?.(`Pointing ${resolved.target.fullDomain} to ${record.value}`);
142
+ const result = yield* provider.applyChanges(zone, plan, { force });
143
+ const reconciliation = yield* reconcileDesiredRecord({
144
+ desired: record,
145
+ progress: input.progress,
146
+ provider,
147
+ timeoutMs: (input.reconcileTimeoutSeconds ?? 30) * 1000,
148
+ zone,
149
+ });
150
+ const shouldWait = input.wait ?? true;
151
+ const propagation = shouldWait
152
+ ? yield* waitForDnsPropagation({
153
+ fqdn: recordFqdn(record, resolved.target.zoneDomain),
154
+ observe: dependencies.observeDns,
155
+ record,
156
+ timeoutSeconds: input.timeoutSeconds ?? 300,
157
+ })
158
+ : { elapsedMs: 0, expected: record.value, observations: [], status: 'not_checked' };
159
+ const propagated = propagation.status === 'propagated' ||
160
+ propagation.status === 'local_or_vpn_cache_stale' ||
161
+ propagation.status === 'system_resolver_unavailable';
104
162
  return {
105
163
  account: resolved.account,
106
164
  accountInferred: resolved.accountInferred,
107
165
  domain: resolved.target.fullDomain,
108
- dryRun: true,
166
+ dryRun: false,
109
167
  isDefaultAccount: resolved.isDefaultAccount,
110
168
  provider: resolved.provider,
111
169
  providerInferred: resolved.providerInferred,
112
- propagated: false,
113
- propagation: {
114
- elapsedMs: 0,
115
- expected: record.value,
116
- observations: [],
117
- status: 'not_checked',
118
- },
119
- reconciled: false,
120
- reconciliationAttempts: 0,
170
+ propagated,
171
+ propagation,
172
+ reconciled: reconciliation.reconciled,
173
+ reconciliationAttempts: reconciliation.attempts,
121
174
  record,
122
- skipped: [],
123
- updated: false,
175
+ skipped: result.skipped,
176
+ updated: result.applied.length > 0 || reconciliation.appliedChanges > 0,
124
177
  zoneDomain: resolved.target.zoneDomain,
125
178
  };
126
- }
127
- const zone = await provider.getZone(resolved.target.zoneDomain);
128
- if (!zone)
129
- throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', `${provider.name} does not have a DNS zone for ${resolved.target.zoneDomain}.`);
130
- input.progress?.(`Planning DNS change in ${provider.name}`);
131
- let force = Boolean(input.force);
132
- let plan = await provider.planChanges(zone, [record], { force });
133
- if (!force && plan.conflicts.length > 0) {
134
- const warning = conflictWarning(resolved, provider, record, plan.conflicts);
135
- force = (await input.confirmDnsOverride?.(warning)) === true;
136
- if (!force)
137
- throw dnsTargetConflictError(warning);
138
- plan = await provider.planChanges(zone, [record], { force: true });
139
- }
140
- input.progress?.(`Pointing ${resolved.target.fullDomain} to ${record.value}`);
141
- const result = await provider.applyChanges(zone, plan, { force });
142
- const reconciliation = await reconcileDesiredRecord({
143
- desired: record,
144
- progress: input.progress,
145
- provider,
146
- timeoutMs: (input.reconcileTimeoutSeconds ?? 30) * 1000,
147
- zone,
148
179
  });
149
- const shouldWait = input.wait ?? true;
150
- const propagation = shouldWait
151
- ? await waitForDnsPropagation({
152
- fqdn: recordFqdn(record, resolved.target.zoneDomain),
153
- observe: dependencies.observeDns,
154
- record,
155
- timeoutSeconds: input.timeoutSeconds ?? 300,
156
- })
157
- : { elapsedMs: 0, expected: record.value, observations: [], status: 'not_checked' };
158
- const propagated = propagation.status === 'propagated' ||
159
- propagation.status === 'local_or_vpn_cache_stale' ||
160
- propagation.status === 'system_resolver_unavailable';
161
- return {
162
- account: resolved.account,
163
- accountInferred: resolved.accountInferred,
164
- domain: resolved.target.fullDomain,
165
- dryRun: false,
166
- isDefaultAccount: resolved.isDefaultAccount,
167
- provider: resolved.provider,
168
- providerInferred: resolved.providerInferred,
169
- propagated,
170
- propagation,
171
- reconciled: reconciliation.reconciled,
172
- reconciliationAttempts: reconciliation.attempts,
173
- record,
174
- skipped: result.skipped,
175
- updated: result.applied.length > 0 || reconciliation.appliedChanges > 0,
176
- zoneDomain: resolved.target.zoneDomain,
177
- };
178
180
  }
@@ -1,3 +1,4 @@
1
+ import { type DoomainEffect } from '../../effect.js';
1
2
  import type { DnsChange, DnsChangePlan, DnsProvider, DnsProviderDefinition, DnsRecord, DnsRecordInput, DnsZone, ProviderCapabilities, ProviderContext, ProviderHealth } from '../types.js';
2
3
  export declare class CloudflareProvider implements DnsProvider {
3
4
  readonly capabilities: ProviderCapabilities;
@@ -6,19 +7,19 @@ export declare class CloudflareProvider implements DnsProvider {
6
7
  private readonly accountId;
7
8
  private readonly http;
8
9
  constructor(context: ProviderContext);
9
- verifyCredentials(): Promise<ProviderHealth>;
10
- listZones(): Promise<DnsZone[]>;
11
- getZone(domain: string): Promise<DnsZone | null>;
12
- listRecords(zone: DnsZone): Promise<DnsRecord[]>;
10
+ verifyCredentials(): DoomainEffect<ProviderHealth>;
11
+ listZones(): DoomainEffect<DnsZone[]>;
12
+ getZone(domain: string): DoomainEffect<DnsZone | null>;
13
+ listRecords(zone: DnsZone): DoomainEffect<DnsRecord[]>;
13
14
  planChanges(zone: DnsZone, desired: DnsRecordInput[], opts?: {
14
15
  force?: boolean;
15
- }): Promise<DnsChangePlan>;
16
- applyChanges(zone: DnsZone, plan: DnsChangePlan): Promise<{
16
+ }): DoomainEffect<DnsChangePlan>;
17
+ applyChanges(zone: DnsZone, plan: DnsChangePlan): DoomainEffect<{
17
18
  applied: DnsChange[];
18
19
  skipped: DnsRecordInput[];
19
20
  }>;
20
- upsertRecord(zone: DnsZone, record: DnsRecordInput): Promise<DnsRecord>;
21
- deleteRecord(zone: DnsZone, record: DnsRecord): Promise<void>;
21
+ upsertRecord(zone: DnsZone, record: DnsRecordInput): DoomainEffect<DnsRecord>;
22
+ deleteRecord(zone: DnsZone, record: DnsRecord): DoomainEffect<void>;
22
23
  private createRecord;
23
24
  private updateRecord;
24
25
  private request;
@@ -1,3 +1,5 @@
1
+ import { Effect } from 'effect';
2
+ import { trySync } from '../../effect.js';
1
3
  import { normalizeDomain } from '../../validate.js';
2
4
  import { ProviderError } from '../core/errors.js';
3
5
  import { createProviderHttpClient } from '../core/http.js';
@@ -94,114 +96,118 @@ export class CloudflareProvider {
94
96
  headers: { Authorization: `Bearer ${context.credentials.apiToken}` },
95
97
  providerId: this.id,
96
98
  signal: context.signal,
99
+ transportErrorCode: context.transportErrorCode,
97
100
  });
98
101
  }
99
- async verifyCredentials() {
100
- await this.listZones();
101
- return { ok: true };
102
+ verifyCredentials() {
103
+ return this.listZones().pipe(Effect.as({ ok: true }));
102
104
  }
103
- async listZones() {
104
- const zones = [];
105
- for (let page = 1; page <= 100; page += 1) {
106
- const response = await this.request('/zones', {
107
- query: { 'account.id': this.accountId, direction: 'asc', order: 'name', page, per_page: 50 },
108
- });
109
- for (const zone of response.result ?? []) {
110
- const dnsZone = toZone(zone);
111
- if (dnsZone)
112
- zones.push(dnsZone);
105
+ listZones() {
106
+ return Effect.gen(this, function* () {
107
+ const zones = [];
108
+ for (let page = 1; page <= 100; page += 1) {
109
+ const response = yield* this.request('/zones', {
110
+ query: { 'account.id': this.accountId, direction: 'asc', order: 'name', page, per_page: 50 },
111
+ });
112
+ for (const zone of response.result ?? []) {
113
+ const dnsZone = toZone(zone);
114
+ if (dnsZone)
115
+ zones.push(dnsZone);
116
+ }
117
+ const totalPages = response.result_info?.total_pages ?? page;
118
+ if (page >= totalPages || (response.result ?? []).length === 0)
119
+ break;
113
120
  }
114
- const totalPages = response.result_info?.total_pages ?? page;
115
- if (page >= totalPages || (response.result ?? []).length === 0)
116
- break;
117
- }
118
- return zones;
121
+ return zones;
122
+ });
119
123
  }
120
- async getZone(domain) {
121
- const normalized = normalizeDomain(domain);
122
- const zones = await this.listZones();
123
- return zones.find((zone) => zone.name === normalized) ?? null;
124
+ getZone(domain) {
125
+ return Effect.gen(this, function* () {
126
+ const normalized = yield* trySync(() => normalizeDomain(domain), 'INVALID_INPUT');
127
+ const zones = yield* this.listZones();
128
+ return zones.find((zone) => zone.name === normalized) ?? null;
129
+ });
124
130
  }
125
- async listRecords(zone) {
126
- const records = [];
127
- for (let page = 1; page <= 100; page += 1) {
128
- const response = await this.request(`/zones/${zone.id}/dns_records`, {
129
- query: { page, per_page: 100 },
130
- });
131
- for (const record of response.result ?? []) {
132
- const dnsRecord = toDnsRecord(record, zone);
133
- if (dnsRecord)
134
- records.push(dnsRecord);
131
+ listRecords(zone) {
132
+ return Effect.gen(this, function* () {
133
+ const records = [];
134
+ for (let page = 1; page <= 100; page += 1) {
135
+ const response = yield* this.request(`/zones/${zone.id}/dns_records`, {
136
+ query: { page, per_page: 100 },
137
+ });
138
+ for (const record of response.result ?? []) {
139
+ const dnsRecord = toDnsRecord(record, zone);
140
+ if (dnsRecord)
141
+ records.push(dnsRecord);
142
+ }
143
+ const totalPages = response.result_info?.total_pages ?? page;
144
+ if (page >= totalPages || (response.result ?? []).length === 0)
145
+ break;
135
146
  }
136
- const totalPages = response.result_info?.total_pages ?? page;
137
- if (page >= totalPages || (response.result ?? []).length === 0)
138
- break;
139
- }
140
- return records;
141
- }
142
- async planChanges(zone, desired, opts = {}) {
143
- return planDnsChanges({
144
- desired,
145
- existing: await this.listRecords(zone),
146
- force: opts.force,
147
- providerId: this.id,
148
- zone,
147
+ return records;
149
148
  });
150
149
  }
151
- async applyChanges(zone, plan) {
152
- assertNoConflicts(this.id, plan);
153
- const applied = [];
154
- const skipped = [];
155
- for (const change of plan.changes) {
156
- if (change.action === 'skip') {
157
- skipped.push(change.record);
158
- continue;
150
+ planChanges(zone, desired, opts = {}) {
151
+ return this.listRecords(zone).pipe(Effect.map((existing) => planDnsChanges({ desired, existing, force: opts.force, providerId: this.id, zone })));
152
+ }
153
+ applyChanges(zone, plan) {
154
+ return Effect.gen(this, function* () {
155
+ yield* assertNoConflicts(this.id, plan);
156
+ const applied = [];
157
+ const skipped = [];
158
+ for (const change of plan.changes) {
159
+ if (change.action === 'skip') {
160
+ skipped.push(change.record);
161
+ continue;
162
+ }
163
+ if (change.action === 'delete')
164
+ yield* this.deleteRecord(zone, change.existing);
165
+ else if (change.action === 'update')
166
+ yield* this.updateRecord(zone, change.existing, change.record);
167
+ else
168
+ yield* this.createRecord(zone, change.record);
169
+ applied.push(change);
159
170
  }
160
- if (change.action === 'delete')
161
- await this.deleteRecord(zone, change.existing);
162
- else if (change.action === 'update')
163
- await this.updateRecord(zone, change.existing, change.record);
164
- else
165
- await this.createRecord(zone, change.record);
166
- applied.push(change);
167
- }
168
- return { applied, skipped };
171
+ return { applied, skipped };
172
+ });
169
173
  }
170
- async upsertRecord(zone, record) {
171
- const existing = (await this.listRecords(zone)).find((item) => item.name === record.name && item.type === record.type);
172
- if (existing)
173
- return this.updateRecord(zone, existing, record);
174
- return this.createRecord(zone, record);
174
+ upsertRecord(zone, record) {
175
+ return Effect.gen(this, function* () {
176
+ const existing = (yield* this.listRecords(zone)).find((item) => item.name === record.name && item.type === record.type);
177
+ return yield* existing ? this.updateRecord(zone, existing, record) : this.createRecord(zone, record);
178
+ });
175
179
  }
176
- async deleteRecord(zone, record) {
180
+ deleteRecord(zone, record) {
177
181
  if (!record.id)
178
- throw new ProviderError(this.id, 'PROVIDER_API_ERROR', 'Cloudflare DNS record id is required to delete a record.');
179
- await this.request(`/zones/${zone.id}/dns_records/${record.id}`, { method: 'DELETE' });
182
+ return Effect.fail(new ProviderError(this.id, 'PROVIDER_API_ERROR', 'Cloudflare DNS record id is required to delete a record.'));
183
+ return this.request(`/zones/${zone.id}/dns_records/${record.id}`, { method: 'DELETE' }).pipe(Effect.asVoid);
180
184
  }
181
- async createRecord(zone, record) {
182
- const response = await this.request(`/zones/${zone.id}/dns_records`, {
185
+ createRecord(zone, record) {
186
+ return this.request(`/zones/${zone.id}/dns_records`, {
183
187
  body: toCloudflareRecord(record, zone),
184
188
  method: 'POST',
185
- });
186
- const created = response.result ? toDnsRecord(response.result, zone) : null;
187
- return created ?? { ...record, ttl: record.ttl ?? capabilities.defaultTtl };
189
+ }).pipe(Effect.map((response) => {
190
+ const created = response.result ? toDnsRecord(response.result, zone) : null;
191
+ return created ?? { ...record, ttl: record.ttl ?? capabilities.defaultTtl };
192
+ }));
188
193
  }
189
- async updateRecord(zone, existing, record) {
194
+ updateRecord(zone, existing, record) {
190
195
  if (!existing.id)
191
- throw new ProviderError(this.id, 'PROVIDER_API_ERROR', 'Cloudflare DNS record id is required to update a record.');
192
- const response = await this.request(`/zones/${zone.id}/dns_records/${existing.id}`, {
196
+ return Effect.fail(new ProviderError(this.id, 'PROVIDER_API_ERROR', 'Cloudflare DNS record id is required to update a record.'));
197
+ return this.request(`/zones/${zone.id}/dns_records/${existing.id}`, {
193
198
  body: toCloudflareRecord(record, zone),
194
199
  method: 'PUT',
195
- });
196
- const updated = response.result ? toDnsRecord(response.result, zone) : null;
197
- return updated ?? { ...record, id: existing.id, ttl: record.ttl ?? capabilities.defaultTtl };
200
+ }).pipe(Effect.map((response) => {
201
+ const updated = response.result ? toDnsRecord(response.result, zone) : null;
202
+ return updated ?? { ...record, id: existing.id, ttl: record.ttl ?? capabilities.defaultTtl };
203
+ }));
198
204
  }
199
- async request(path, init = {}) {
200
- const response = await this.http.request(path, init);
201
- if (response.success === false) {
202
- throw new ProviderError(this.id, 'PROVIDER_API_ERROR', cloudflareErrorMessage(response), response.errors);
203
- }
204
- return response;
205
+ request(path, init = {}) {
206
+ return this.http
207
+ .request(path, init)
208
+ .pipe(Effect.flatMap((response) => response.success === false
209
+ ? Effect.fail(new ProviderError(this.id, 'PROVIDER_API_ERROR', cloudflareErrorMessage(response), response.errors))
210
+ : Effect.succeed(response)));
205
211
  }
206
212
  }
207
213
  export const cloudflareProviderDefinition = {
@@ -1,4 +1,6 @@
1
1
  import { type DoomainConfig, type ProviderConfig } from '../../config.js';
2
+ import { type DoomainEffect } from '../../effect.js';
3
+ import { type DoomainErrorCode } from '../../errors.js';
2
4
  import type { CredentialDefinition, DnsProviderDefinition, ProviderContext } from './types.js';
3
5
  export declare const DEFAULT_PROVIDER_ACCOUNT = "default";
4
6
  export interface ProviderAccountRef {
@@ -8,6 +10,7 @@ export interface ProviderAccountRef {
8
10
  }
9
11
  export interface ProviderAccountOptions {
10
12
  account?: string;
13
+ transportErrorCode?: DoomainErrorCode;
11
14
  }
12
15
  export declare function normalizeProviderAccount(account?: string): string;
13
16
  export declare function isDefaultProviderAccount(account?: string): boolean;
@@ -17,4 +20,4 @@ export declare function providerAccountHasCredentials(config: DoomainConfig, pro
17
20
  export declare function withProviderAccountCredentials(current: ProviderConfig | undefined, accountInput: string, credentials: Record<string, string>): ProviderConfig;
18
21
  export declare function isProviderAccountConfigured(definition: DnsProviderDefinition, config: DoomainConfig, opts?: ProviderAccountOptions): boolean;
19
22
  export declare function listConfiguredProviderAccounts(config: DoomainConfig, definition: DnsProviderDefinition): ProviderAccountRef[];
20
- export declare function createProviderContext(definition: DnsProviderDefinition, opts?: ProviderAccountOptions): Promise<ProviderContext>;
23
+ export declare function createProviderContext(definition: DnsProviderDefinition, opts?: ProviderAccountOptions): DoomainEffect<ProviderContext>;
@@ -1,4 +1,6 @@
1
+ import { Effect } from 'effect';
1
2
  import { loadConfig } from '../../config.js';
3
+ import { trySync } from '../../effect.js';
2
4
  import { DoomainError } from '../../errors.js';
3
5
  import { ensureProviderAccount } from '../../validate.js';
4
6
  export const DEFAULT_PROVIDER_ACCOUNT = 'default';
@@ -83,18 +85,20 @@ export function listConfiguredProviderAccounts(config, definition) {
83
85
  }
84
86
  return accounts;
85
87
  }
86
- export async function createProviderContext(definition, opts = {}) {
87
- const config = await loadConfig();
88
- const credentials = {};
89
- const account = normalizeProviderAccount(opts.account);
90
- for (const credential of definition.credentials) {
91
- const value = getProviderCredential(config, definition.id, credential, { account });
92
- if (value)
93
- credentials[credential.key] = value;
94
- else if (credential.required !== false) {
95
- const accountHint = account === DEFAULT_PROVIDER_ACCOUNT ? '' : ` for account ${account}`;
96
- throw new DoomainError('MISSING_CREDENTIALS', `Missing ${definition.displayName} ${credential.label}${accountHint}. Run \`doomain providers connect ${definition.id}${account === DEFAULT_PROVIDER_ACCOUNT ? '' : ` --account ${account}`}\` or set ${credential.env}.`, { account, provider: definition.id });
88
+ export function createProviderContext(definition, opts = {}) {
89
+ return Effect.gen(function* () {
90
+ const config = yield* loadConfig();
91
+ const credentials = {};
92
+ const account = yield* trySync(() => normalizeProviderAccount(opts.account), 'INVALID_INPUT');
93
+ for (const credential of definition.credentials) {
94
+ const value = getProviderCredential(config, definition.id, credential, { account });
95
+ if (value)
96
+ credentials[credential.key] = value;
97
+ else if (credential.required !== false) {
98
+ const accountHint = account === DEFAULT_PROVIDER_ACCOUNT ? '' : ` for account ${account}`;
99
+ return yield* Effect.fail(new DoomainError('MISSING_CREDENTIALS', `Missing ${definition.displayName} ${credential.label}${accountHint}. Run \`doomain providers connect ${definition.id}${account === DEFAULT_PROVIDER_ACCOUNT ? '' : ` --account ${account}`}\` or set ${credential.env}.`, { account, provider: definition.id }));
100
+ }
97
101
  }
98
- }
99
- return { credentials, debug: process.env.DOOMAIN_DEBUG === '1' };
102
+ return { credentials, debug: process.env.DOOMAIN_DEBUG === '1', transportErrorCode: opts.transportErrorCode };
103
+ });
100
104
  }
@@ -1,9 +1,12 @@
1
+ import type { DoomainEffect } from '../../effect.js';
2
+ import { type DoomainErrorCode } from '../../errors.js';
1
3
  export interface ProviderHttpClientOptions {
2
4
  baseUrl: string;
3
5
  errorMessages?: Partial<Record<number, string>>;
4
6
  headers?: Record<string, string>;
5
7
  providerId: string;
6
8
  signal?: AbortSignal;
9
+ transportErrorCode?: DoomainErrorCode;
7
10
  }
8
11
  export interface ProviderRequestOptions extends Omit<RequestInit, 'body'> {
9
12
  body?: unknown;
@@ -12,6 +15,6 @@ export interface ProviderRequestOptions extends Omit<RequestInit, 'body'> {
12
15
  export declare class ProviderHttpClient {
13
16
  private readonly opts;
14
17
  constructor(opts: ProviderHttpClientOptions);
15
- request<T>(path: string, init?: ProviderRequestOptions): Promise<T>;
18
+ request<T>(path: string, init?: ProviderRequestOptions): DoomainEffect<T>;
16
19
  }
17
20
  export declare function createProviderHttpClient(opts: ProviderHttpClientOptions): ProviderHttpClient;
@@ -1,3 +1,5 @@
1
+ import { Effect } from 'effect';
2
+ import { toDoomainError } from '../../errors.js';
1
3
  import { ProviderError, providerCodeFromStatus } from './errors.js';
2
4
  function appendQuery(path, query) {
3
5
  if (!query)
@@ -17,25 +19,33 @@ export class ProviderHttpClient {
17
19
  constructor(opts) {
18
20
  this.opts = opts;
19
21
  }
20
- async request(path, init = {}) {
21
- const { body, headers, query, ...rest } = init;
22
- const response = await fetch(`${this.opts.baseUrl}${appendQuery(path, query)}`, {
23
- ...rest,
24
- body: body === undefined ? undefined : JSON.stringify(body),
25
- headers: {
26
- 'Content-Type': 'application/json',
27
- ...this.opts.headers,
28
- ...headers,
29
- },
30
- signal: init.signal ?? this.opts.signal,
22
+ request(path, init = {}) {
23
+ return Effect.gen(this, function* () {
24
+ const { body, headers, query, ...rest } = init;
25
+ const response = yield* Effect.tryPromise({
26
+ try: (signal) => fetch(`${this.opts.baseUrl}${appendQuery(path, query)}`, {
27
+ ...rest,
28
+ body: body === undefined ? undefined : JSON.stringify(body),
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ ...this.opts.headers,
32
+ ...headers,
33
+ },
34
+ signal: init.signal ?? this.opts.signal ?? signal,
35
+ }),
36
+ catch: (cause) => toDoomainError(cause, this.opts.transportErrorCode ?? 'PROVIDER_API_ERROR'),
37
+ });
38
+ if (!response.ok) {
39
+ const details = yield* Effect.tryPromise(() => response.json()).pipe(Effect.catchAll(() => Effect.succeed(undefined)));
40
+ return yield* Effect.fail(new ProviderError(this.opts.providerId, providerCodeFromStatus(response.status), this.opts.errorMessages?.[response.status] ?? `${this.opts.providerId} API error (${response.status}).`, details));
41
+ }
42
+ if (response.status === 204)
43
+ return undefined;
44
+ return (yield* Effect.tryPromise({
45
+ try: () => response.json(),
46
+ catch: (cause) => toDoomainError(cause, this.opts.transportErrorCode ?? 'PROVIDER_API_ERROR'),
47
+ }));
31
48
  });
32
- if (!response.ok) {
33
- const details = await response.json().catch(() => undefined);
34
- throw new ProviderError(this.opts.providerId, providerCodeFromStatus(response.status), this.opts.errorMessages?.[response.status] ?? `${this.opts.providerId} API error (${response.status}).`, details);
35
- }
36
- if (response.status === 204)
37
- return undefined;
38
- return (await response.json().catch(() => undefined));
39
49
  }
40
50
  }
41
51
  export function createProviderHttpClient(opts) {
@@ -1,12 +1,13 @@
1
+ import type { DoomainEffect } from '../../effect.js';
1
2
  export interface SkipPaginationInput<T> {
2
3
  fetchPage(input: {
3
4
  skip: number;
4
5
  take: number;
5
- }): Promise<{
6
+ }): DoomainEffect<{
6
7
  items: T[];
7
8
  total: number;
8
9
  }>;
9
10
  maxPages?: number;
10
11
  take: number;
11
12
  }
12
- export declare function paginateBySkip<T>(input: SkipPaginationInput<T>): Promise<T[]>;
13
+ export declare function paginateBySkip<T>(input: SkipPaginationInput<T>): DoomainEffect<T[]>;