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,4 +1,7 @@
1
+ import { Effect } from 'effect';
1
2
  import { XMLParser } from 'fast-xml-parser';
3
+ import { trySync } from '../../effect.js';
4
+ import { toDoomainError } from '../../errors.js';
2
5
  import { normalizeDomain } from '../../validate.js';
3
6
  import { ProviderError } from '../core/errors.js';
4
7
  import { assertNoConflicts, planDnsChanges } from '../core/planner.js';
@@ -131,6 +134,7 @@ export class NamecheapProvider {
131
134
  apiUser;
132
135
  baseUrl;
133
136
  clientIp;
137
+ transportErrorCode;
134
138
  username;
135
139
  constructor(context) {
136
140
  this.apiUser = context.credentials.apiUser;
@@ -138,131 +142,152 @@ export class NamecheapProvider {
138
142
  this.username = context.credentials.username || context.credentials.apiUser;
139
143
  this.clientIp = context.credentials.clientIp;
140
144
  this.baseUrl = bool(context.credentials.sandbox) ? NAMECHEAP_SANDBOX_URL : NAMECHEAP_PRODUCTION_URL;
145
+ this.transportErrorCode = context.transportErrorCode ?? 'PROVIDER_API_ERROR';
141
146
  }
142
- async verifyCredentials() {
143
- await this.listZones();
144
- return { ok: true };
147
+ verifyCredentials() {
148
+ return this.listZones().pipe(Effect.as({ ok: true }));
145
149
  }
146
- async listZones() {
147
- const zones = [];
148
- let currentPage = 1;
149
- let totalItems = Number.POSITIVE_INFINITY;
150
- while (zones.length < totalItems) {
151
- const response = await this.request('namecheap.domains.getList', { Page: String(currentPage), PageSize: '100' });
152
- const commandResponse = response.ApiResponse.CommandResponse;
153
- const result = commandResponse.DomainGetListResult;
154
- const domains = asArray(result.Domain);
155
- for (const domain of domains) {
156
- const zone = toZone(domain);
157
- if (zone)
158
- zones.push(zone);
150
+ listZones() {
151
+ return Effect.gen(this, function* () {
152
+ const zones = [];
153
+ let currentPage = 1;
154
+ let totalItems = Number.POSITIVE_INFINITY;
155
+ while (zones.length < totalItems) {
156
+ const response = yield* this.request('namecheap.domains.getList', {
157
+ Page: String(currentPage),
158
+ PageSize: '100',
159
+ });
160
+ const commandResponse = response.ApiResponse.CommandResponse;
161
+ const result = commandResponse.DomainGetListResult;
162
+ const domains = asArray(result.Domain);
163
+ for (const domain of domains) {
164
+ const zone = toZone(domain);
165
+ if (zone)
166
+ zones.push(zone);
167
+ }
168
+ totalItems = pagingTotal(commandResponse, result, zones.length);
169
+ if (domains.length === 0)
170
+ break;
171
+ currentPage += 1;
159
172
  }
160
- totalItems = pagingTotal(commandResponse, result, zones.length);
161
- if (domains.length === 0)
162
- break;
163
- currentPage += 1;
164
- }
165
- return zones;
166
- }
167
- async getZone(domain) {
168
- const normalized = normalizeDomain(domain);
169
- const zones = await this.listZones();
170
- return zones.find((zone) => zone.name === normalized) ?? null;
173
+ return zones;
174
+ });
171
175
  }
172
- async listRecords(zone) {
173
- const { sld, tld } = splitDomain(zone.name);
174
- const response = await this.request('namecheap.domains.dns.getHosts', { SLD: sld, TLD: tld });
175
- const hosts = asArray(response.ApiResponse.CommandResponse.DomainDNSGetHostsResult.host);
176
- return hosts.flatMap((host) => {
177
- const record = toDnsRecord(host);
178
- return record ? [record] : [];
176
+ getZone(domain) {
177
+ return Effect.gen(this, function* () {
178
+ const normalized = yield* trySync(() => normalizeDomain(domain), 'INVALID_INPUT');
179
+ const zones = yield* this.listZones();
180
+ return zones.find((zone) => zone.name === normalized) ?? null;
179
181
  });
180
182
  }
181
- async planChanges(zone, desired, opts = {}) {
182
- return planDnsChanges({
183
- desired,
184
- existing: await this.listRecords(zone),
185
- force: opts.force,
186
- providerId: this.id,
187
- zone,
183
+ listRecords(zone) {
184
+ return Effect.gen(this, function* () {
185
+ const { sld, tld } = yield* trySync(() => splitDomain(zone.name), 'PROVIDER_ZONE_NOT_FOUND');
186
+ const response = yield* this.request('namecheap.domains.dns.getHosts', { SLD: sld, TLD: tld });
187
+ const hosts = asArray(response.ApiResponse.CommandResponse.DomainDNSGetHostsResult.host);
188
+ return hosts.flatMap((host) => {
189
+ const record = toDnsRecord(host);
190
+ return record ? [record] : [];
191
+ });
188
192
  });
189
193
  }
190
- async applyChanges(zone, plan) {
191
- assertNoConflicts(this.id, plan);
192
- const finalRecords = [...plan.existing];
193
- const applied = [];
194
- const skipped = [];
195
- for (const change of plan.changes) {
196
- if (change.action === 'skip') {
197
- skipped.push(change.record);
198
- continue;
199
- }
200
- if (change.action === 'delete') {
201
- const index = finalRecords.findIndex((record) => sameRecord(record, change.existing));
202
- if (index !== -1)
203
- finalRecords.splice(index, 1);
204
- }
205
- else if (change.action === 'update') {
206
- const index = finalRecords.findIndex((record) => sameRecord(record, change.existing));
207
- if (index !== -1)
208
- finalRecords[index] = { ...change.existing, ...change.record };
209
- }
210
- else {
211
- finalRecords.push(inputToRecord(change.record));
194
+ planChanges(zone, desired, opts = {}) {
195
+ return this.listRecords(zone).pipe(Effect.map((existing) => planDnsChanges({ desired, existing, force: opts.force, providerId: this.id, zone })));
196
+ }
197
+ applyChanges(zone, plan) {
198
+ return Effect.gen(this, function* () {
199
+ yield* assertNoConflicts(this.id, plan);
200
+ const finalRecords = [...plan.existing];
201
+ const applied = [];
202
+ const skipped = [];
203
+ for (const change of plan.changes) {
204
+ if (change.action === 'skip') {
205
+ skipped.push(change.record);
206
+ continue;
207
+ }
208
+ if (change.action === 'delete') {
209
+ const index = finalRecords.findIndex((record) => sameRecord(record, change.existing));
210
+ if (index !== -1)
211
+ finalRecords.splice(index, 1);
212
+ }
213
+ else if (change.action === 'update') {
214
+ const index = finalRecords.findIndex((record) => sameRecord(record, change.existing));
215
+ if (index !== -1)
216
+ finalRecords[index] = { ...change.existing, ...change.record };
217
+ }
218
+ else {
219
+ finalRecords.push(inputToRecord(change.record));
220
+ }
221
+ applied.push(change);
212
222
  }
213
- applied.push(change);
214
- }
215
- if (applied.length > 0)
216
- await this.setHosts(zone, finalRecords);
217
- return { applied, skipped };
223
+ if (applied.length > 0)
224
+ yield* this.setHosts(zone, finalRecords);
225
+ return { applied, skipped };
226
+ });
218
227
  }
219
- async upsertRecord(zone, record) {
220
- const plan = await this.planChanges(zone, [record], { force: true });
221
- await this.applyChanges(zone, plan);
222
- return { ...record, ttl: record.ttl ?? capabilities.defaultTtl };
228
+ upsertRecord(zone, record) {
229
+ return Effect.gen(this, function* () {
230
+ const plan = yield* this.planChanges(zone, [record], { force: true });
231
+ yield* this.applyChanges(zone, plan);
232
+ return { ...record, ttl: record.ttl ?? capabilities.defaultTtl };
233
+ });
223
234
  }
224
- async deleteRecord(zone, record) {
225
- const records = (await this.listRecords(zone)).filter((item) => !sameRecord(item, record));
226
- await this.setHosts(zone, records);
235
+ deleteRecord(zone, record) {
236
+ return Effect.gen(this, function* () {
237
+ const records = (yield* this.listRecords(zone)).filter((item) => !sameRecord(item, record));
238
+ yield* this.setHosts(zone, records);
239
+ });
227
240
  }
228
- async setHosts(zone, records) {
229
- const { sld, tld } = splitDomain(zone.name);
230
- const response = await this.request('namecheap.domains.dns.setHosts', { SLD: sld, TLD: tld, ...hostParams(records) }, { method: 'POST' });
231
- const result = response.ApiResponse.CommandResponse.DomainDNSSetHostsResult;
232
- if (!bool(result?.IsSuccess)) {
233
- throw new ProviderError('namecheap', 'PROVIDER_API_ERROR', 'Namecheap did not confirm DNS host records were updated.', result);
234
- }
241
+ setHosts(zone, records) {
242
+ return Effect.gen(this, function* () {
243
+ const { sld, tld } = yield* trySync(() => splitDomain(zone.name), 'PROVIDER_ZONE_NOT_FOUND');
244
+ const response = yield* this.request('namecheap.domains.dns.setHosts', { SLD: sld, TLD: tld, ...hostParams(records) }, { method: 'POST' });
245
+ const result = response.ApiResponse.CommandResponse.DomainDNSSetHostsResult;
246
+ if (!bool(result?.IsSuccess)) {
247
+ return yield* Effect.fail(new ProviderError('namecheap', 'PROVIDER_API_ERROR', 'Namecheap did not confirm DNS host records were updated.', result));
248
+ }
249
+ });
235
250
  }
236
- async request(command, params, opts = {}) {
251
+ request(command, params, opts = {}) {
237
252
  if (!this.clientIp) {
238
- throw new ProviderError('namecheap', 'MISSING_CREDENTIALS', 'Namecheap requires a whitelisted IPv4 ClientIp credential.');
253
+ return Effect.fail(new ProviderError('namecheap', 'MISSING_CREDENTIALS', 'Namecheap requires a whitelisted IPv4 ClientIp credential.'));
239
254
  }
240
- const query = new URLSearchParams({
241
- ApiKey: this.apiKey,
242
- ApiUser: this.apiUser,
243
- ClientIp: this.clientIp,
244
- Command: command,
245
- UserName: this.username,
246
- ...params,
247
- });
248
- const method = opts.method ?? 'GET';
249
- const response = await fetch(method === 'POST' ? this.baseUrl : `${this.baseUrl}?${query.toString()}`, {
250
- ...(method === 'POST'
251
- ? { body: query.toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, method }
252
- : {}),
255
+ return Effect.gen(this, function* () {
256
+ const query = new URLSearchParams({
257
+ ApiKey: this.apiKey,
258
+ ApiUser: this.apiUser,
259
+ ClientIp: this.clientIp,
260
+ Command: command,
261
+ UserName: this.username,
262
+ ...params,
263
+ });
264
+ const method = opts.method ?? 'GET';
265
+ const response = yield* Effect.tryPromise({
266
+ try: (signal) => fetch(method === 'POST' ? this.baseUrl : `${this.baseUrl}?${query.toString()}`, {
267
+ ...(method === 'POST'
268
+ ? { body: query.toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, method }
269
+ : {}),
270
+ signal,
271
+ }),
272
+ catch: (cause) => toDoomainError(cause, this.transportErrorCode),
273
+ });
274
+ if (!response.ok) {
275
+ return yield* Effect.fail(new ProviderError('namecheap', 'PROVIDER_API_ERROR', `Namecheap API error (${response.status}).`));
276
+ }
277
+ const text = yield* Effect.tryPromise({
278
+ try: () => response.text(),
279
+ catch: (cause) => new ProviderError('namecheap', 'PROVIDER_API_ERROR', 'Invalid Namecheap API response.', cause),
280
+ });
281
+ const data = yield* trySync(() => parser.parse(text), 'PROVIDER_API_ERROR');
282
+ const status = data.ApiResponse?.Status;
283
+ if (status !== 'OK') {
284
+ const message = getErrorMessage(asArray(data.ApiResponse?.Errors?.Error)[0] ?? 'Namecheap API error.');
285
+ const code = providerCodeFromNamecheapError(message);
286
+ const help = namecheapSetupHelp(code);
287
+ return yield* Effect.fail(new ProviderError('namecheap', code, help ? `${message} ${help}` : message, data.ApiResponse?.Errors));
288
+ }
289
+ return data;
253
290
  });
254
- if (!response.ok) {
255
- throw new ProviderError('namecheap', 'PROVIDER_API_ERROR', `Namecheap API error (${response.status}).`);
256
- }
257
- const data = parser.parse(await response.text());
258
- const status = data.ApiResponse?.Status;
259
- if (status !== 'OK') {
260
- const message = getErrorMessage(asArray(data.ApiResponse?.Errors?.Error)[0] ?? 'Namecheap API error.');
261
- const code = providerCodeFromNamecheapError(message);
262
- const help = namecheapSetupHelp(code);
263
- throw new ProviderError('namecheap', code, help ? `${message} ${help}` : message, data.ApiResponse?.Errors);
264
- }
265
- return data;
266
291
  }
267
292
  }
268
293
  export const namecheapProviderDefinition = {
@@ -1,6 +1,6 @@
1
+ import { type DoomainEffect } from '../effect.js';
2
+ import { type ProviderAccountOptions } from './core/config.js';
1
3
  import type { DnsProvider, DnsProviderDefinition } from './types.js';
2
4
  export declare function listProviderDefinitions(): DnsProviderDefinition[];
3
5
  export declare function getProviderDefinition(id: string): DnsProviderDefinition;
4
- export declare function createProvider(id: string, opts?: {
5
- account?: string;
6
- }): Promise<DnsProvider>;
6
+ export declare function createProvider(id: string, opts?: ProviderAccountOptions): DoomainEffect<DnsProvider>;
@@ -1,3 +1,5 @@
1
+ import { Effect } from 'effect';
2
+ import { trySync } from '../effect.js';
1
3
  import { DoomainError } from '../errors.js';
2
4
  import { ensureProviderId } from '../validate.js';
3
5
  import { cloudflareProviderDefinition } from './cloudflare/index.js';
@@ -21,7 +23,9 @@ export function getProviderDefinition(id) {
21
23
  throw new DoomainError('PROVIDER_NOT_FOUND', `Unsupported DNS provider: ${id}`);
22
24
  return definition;
23
25
  }
24
- export async function createProvider(id, opts = {}) {
25
- const definition = getProviderDefinition(id);
26
- return definition.create(await createProviderContext(definition, opts));
26
+ export function createProvider(id, opts = {}) {
27
+ return Effect.gen(function* () {
28
+ const definition = yield* trySync(() => getProviderDefinition(id), 'PROVIDER_NOT_FOUND');
29
+ return definition.create(yield* createProviderContext(definition, opts));
30
+ });
27
31
  }
@@ -1,3 +1,4 @@
1
+ import { type DoomainEffect } from '../../effect.js';
1
2
  import type { DnsChangePlan, DnsProvider, DnsProviderDefinition, DnsRecord, DnsRecordInput, DnsZone, ProviderCapabilities, ProviderContext, ProviderHealth } from '../types.js';
2
3
  export declare class SpaceshipProvider implements DnsProvider {
3
4
  readonly capabilities: ProviderCapabilities;
@@ -5,18 +6,18 @@ export declare class SpaceshipProvider implements DnsProvider {
5
6
  readonly name = "Spaceship";
6
7
  private readonly http;
7
8
  constructor(context: ProviderContext);
8
- verifyCredentials(): Promise<ProviderHealth>;
9
- listZones(): Promise<DnsZone[]>;
10
- getZone(domain: string): Promise<DnsZone | null>;
11
- listRecords(zone: DnsZone): Promise<DnsRecord[]>;
9
+ verifyCredentials(): DoomainEffect<ProviderHealth>;
10
+ listZones(): DoomainEffect<DnsZone[]>;
11
+ getZone(domain: string): DoomainEffect<DnsZone | null>;
12
+ listRecords(zone: DnsZone): DoomainEffect<DnsRecord[]>;
12
13
  planChanges(zone: DnsZone, desired: DnsRecordInput[], opts?: {
13
14
  force?: boolean;
14
- }): Promise<DnsChangePlan>;
15
- applyChanges(zone: DnsZone, plan: DnsChangePlan): Promise<{
15
+ }): DoomainEffect<DnsChangePlan>;
16
+ applyChanges(zone: DnsZone, plan: DnsChangePlan): DoomainEffect<{
16
17
  applied: DnsChangePlan['changes'];
17
18
  skipped: DnsRecordInput[];
18
19
  }>;
19
- upsertRecord(zone: DnsZone, record: DnsRecordInput): Promise<DnsRecord>;
20
- deleteRecord(zone: DnsZone, record: DnsRecord): Promise<void>;
20
+ upsertRecord(zone: DnsZone, record: DnsRecordInput): DoomainEffect<DnsRecord>;
21
+ deleteRecord(zone: DnsZone, record: DnsRecord): DoomainEffect<void>;
21
22
  }
22
23
  export declare const spaceshipProviderDefinition: DnsProviderDefinition;
@@ -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 { createProviderHttpClient } from '../core/http.js';
3
5
  import { paginateBySkip } from '../core/pagination.js';
@@ -66,76 +68,75 @@ export class SpaceshipProvider {
66
68
  },
67
69
  providerId: this.id,
68
70
  signal: context.signal,
71
+ transportErrorCode: context.transportErrorCode,
69
72
  });
70
73
  }
71
- async verifyCredentials() {
72
- await this.listZones();
73
- return { ok: true };
74
+ verifyCredentials() {
75
+ return this.listZones().pipe(Effect.as({ ok: true }));
74
76
  }
75
- async listZones() {
76
- const domains = await paginateBySkip({
77
+ listZones() {
78
+ return paginateBySkip({
77
79
  take: 100,
78
80
  fetchPage: ({ skip, take }) => this.http.request('/domains', {
79
81
  query: { orderBy: 'name', skip, take },
80
82
  }),
81
- });
82
- return domains.flatMap((domain) => {
83
+ }).pipe(Effect.map((domains) => domains.flatMap((domain) => {
83
84
  const zone = toZone(domain);
84
85
  return zone ? [zone] : [];
85
- });
86
+ })));
86
87
  }
87
- async getZone(domain) {
88
- const normalized = normalizeDomain(domain);
89
- const zones = await this.listZones();
90
- return zones.find((zone) => zone.name === normalized) ?? null;
88
+ getZone(domain) {
89
+ return Effect.gen(this, function* () {
90
+ const normalized = yield* trySync(() => normalizeDomain(domain), 'INVALID_INPUT');
91
+ const zones = yield* this.listZones();
92
+ return zones.find((zone) => zone.name === normalized) ?? null;
93
+ });
91
94
  }
92
- async listRecords(zone) {
93
- const records = await paginateBySkip({
95
+ listRecords(zone) {
96
+ return paginateBySkip({
94
97
  take: 500,
95
98
  fetchPage: ({ skip, take }) => this.http.request(`/dns/records/${zone.name}`, {
96
99
  query: { skip, take },
97
100
  }),
98
- });
99
- return records.map(toDnsRecord);
101
+ }).pipe(Effect.map((records) => records.map(toDnsRecord)));
100
102
  }
101
- async planChanges(zone, desired, opts = {}) {
102
- return planDnsChanges({
103
- desired,
104
- existing: await this.listRecords(zone),
105
- force: opts.force,
106
- providerId: this.id,
107
- zone,
108
- });
103
+ planChanges(zone, desired, opts = {}) {
104
+ return this.listRecords(zone).pipe(Effect.map((existing) => planDnsChanges({ desired, existing, force: opts.force, providerId: this.id, zone })));
109
105
  }
110
- async applyChanges(zone, plan) {
111
- assertNoConflicts(this.id, plan);
112
- const skipped = plan.changes.flatMap((change) => (change.action === 'skip' ? [change.record] : []));
113
- const applied = plan.changes.filter((change) => change.action !== 'skip');
114
- // Spaceship records do not have stable ids. Delete every old value before creating
115
- // replacements so an API that processes accepted writes out of order cannot leave
116
- // two address records in the same slot without the reconciler noticing and retrying.
117
- for (const change of applied) {
118
- if (change.action === 'delete' || change.action === 'update')
119
- await this.deleteRecord(zone, change.existing);
120
- }
121
- for (const change of applied) {
122
- if (change.action === 'create' || change.action === 'update')
123
- await this.upsertRecord(zone, change.record);
124
- }
125
- return { applied, skipped };
106
+ applyChanges(zone, plan) {
107
+ return Effect.gen(this, function* () {
108
+ yield* assertNoConflicts(this.id, plan);
109
+ const skipped = plan.changes.flatMap((change) => (change.action === 'skip' ? [change.record] : []));
110
+ const applied = plan.changes.filter((change) => change.action !== 'skip');
111
+ // Spaceship records do not have stable ids. Delete every old value before creating
112
+ // replacements so an API that processes accepted writes out of order cannot leave
113
+ // two address records in the same slot without the reconciler noticing and retrying.
114
+ for (const change of applied) {
115
+ if (change.action === 'delete' || change.action === 'update')
116
+ yield* this.deleteRecord(zone, change.existing);
117
+ }
118
+ for (const change of applied) {
119
+ if (change.action === 'create' || change.action === 'update')
120
+ yield* this.upsertRecord(zone, change.record);
121
+ }
122
+ return { applied, skipped };
123
+ });
126
124
  }
127
- async upsertRecord(zone, record) {
128
- await this.http.request(`/dns/records/${zone.name}`, {
125
+ upsertRecord(zone, record) {
126
+ return this.http
127
+ .request(`/dns/records/${zone.name}`, {
129
128
  body: { force: true, items: [toSpaceshipItem(record)] },
130
129
  method: 'PUT',
131
- });
132
- return { ...record, ttl: record.ttl ?? capabilities.defaultTtl };
130
+ })
131
+ .pipe(Effect.as({ ...record, ttl: record.ttl ?? capabilities.defaultTtl }));
133
132
  }
134
- async deleteRecord(zone, record) {
135
- await this.http.request(`/dns/records/${zone.name}`, {
133
+ deleteRecord(zone, record) {
134
+ return this.http
135
+ .request(`/dns/records/${zone.name}`, {
136
136
  body: [toSpaceshipItem(record)],
137
137
  method: 'DELETE',
138
- });
138
+ })
139
+ .pipe(Effect.asVoid);
139
140
  }
140
141
  }
141
142
  export const spaceshipProviderDefinition = {
@@ -1,4 +1,5 @@
1
1
  import { type DoomainConfig } from '../config.js';
2
+ import { type DoomainEffect } from '../effect.js';
2
3
  import type { DnsProviderDefinition } from './types.js';
3
4
  export interface ProviderStatus {
4
5
  account: string;
@@ -20,4 +21,4 @@ export declare function isProviderConfigured(definition: DnsProviderDefinition,
20
21
  }): boolean;
21
22
  export declare function listProviderStatuses(opts?: {
22
23
  verify?: boolean;
23
- }): Promise<ProviderStatus[]>;
24
+ }): DoomainEffect<ProviderStatus[]>;
@@ -1,44 +1,51 @@
1
+ import { Effect } from 'effect';
1
2
  import { loadConfig } from '../config.js';
3
+ import { trySync } from '../effect.js';
2
4
  import { DEFAULT_PROVIDER_ACCOUNT, isProviderAccountConfigured, listConfiguredProviderAccounts, normalizeProviderAccount, } from './core/config.js';
3
5
  import { createProvider, listProviderDefinitions } from './registry.js';
4
6
  export function isProviderConfigured(definition, config, opts = {}) {
5
7
  return isProviderAccountConfigured(definition, config, opts);
6
8
  }
7
- export async function listProviderStatuses(opts = {}) {
8
- const config = await loadConfig();
9
- const statuses = [];
10
- for (const definition of listProviderDefinitions()) {
11
- const accounts = listConfiguredProviderAccounts(config, definition);
12
- const refs = accounts.length > 0
13
- ? accounts
14
- : [{ account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId: definition.id }];
15
- for (const ref of refs) {
16
- const account = normalizeProviderAccount(ref.account);
17
- const configured = accounts.some((item) => item.account === account);
18
- const status = {
19
- account,
20
- accountLabel: ref.isDefaultAccount ? `${definition.id}/default` : `${definition.id}/${account}`,
21
- configured,
22
- default: config.defaults?.provider === definition.id,
23
- displayName: definition.displayName,
24
- docsUrl: definition.docsUrl,
25
- id: definition.id,
26
- isDefaultAccount: ref.isDefaultAccount,
27
- isPreferredProvider: config.defaults?.provider === definition.id,
28
- };
29
- if (configured && opts.verify) {
30
- try {
31
- const zones = await (await createProvider(definition.id, { account })).listZones();
32
- status.domainCount = zones.length;
33
- status.verified = true;
34
- }
35
- catch (error) {
36
- status.error = error instanceof Error ? error.message : String(error);
37
- status.verified = false;
9
+ export function listProviderStatuses(opts = {}) {
10
+ return Effect.gen(function* () {
11
+ const config = yield* loadConfig();
12
+ const statuses = [];
13
+ for (const definition of listProviderDefinitions()) {
14
+ const accounts = yield* trySync(() => listConfiguredProviderAccounts(config, definition), 'INVALID_INPUT');
15
+ const refs = accounts.length > 0
16
+ ? accounts
17
+ : [{ account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId: definition.id }];
18
+ for (const ref of refs) {
19
+ const account = yield* trySync(() => normalizeProviderAccount(ref.account), 'INVALID_INPUT');
20
+ const configured = accounts.some((item) => item.account === account);
21
+ const status = {
22
+ account,
23
+ accountLabel: ref.isDefaultAccount ? `${definition.id}/default` : `${definition.id}/${account}`,
24
+ configured,
25
+ default: config.defaults?.provider === definition.id,
26
+ displayName: definition.displayName,
27
+ docsUrl: definition.docsUrl,
28
+ id: definition.id,
29
+ isDefaultAccount: ref.isDefaultAccount,
30
+ isPreferredProvider: config.defaults?.provider === definition.id,
31
+ };
32
+ if (configured && opts.verify) {
33
+ const verified = yield* Effect.gen(function* () {
34
+ const provider = yield* createProvider(definition.id, { account });
35
+ return yield* provider.listZones();
36
+ }).pipe(Effect.either);
37
+ if (verified._tag === 'Right') {
38
+ status.domainCount = verified.right.length;
39
+ status.verified = true;
40
+ }
41
+ else {
42
+ status.error = verified.left.message;
43
+ status.verified = false;
44
+ }
38
45
  }
46
+ statuses.push(status);
39
47
  }
40
- statuses.push(status);
41
48
  }
42
- }
43
- return statuses;
49
+ return statuses;
50
+ });
44
51
  }
@@ -0,0 +1,2 @@
1
+ import type { DoomainEffect } from './effect.js';
2
+ export declare function fetchPublicIp(): DoomainEffect<string | undefined, never>;
@@ -0,0 +1,14 @@
1
+ import { Effect } from 'effect';
2
+ export function fetchPublicIp() {
3
+ return Effect.acquireUseRelease(Effect.sync(() => {
4
+ const controller = new AbortController();
5
+ const timeout = setTimeout(() => controller.abort(), 2000);
6
+ return { controller, timeout };
7
+ }), ({ controller }) => Effect.tryPromise(async () => {
8
+ const response = await fetch('https://api.ipify.org', { signal: controller.signal });
9
+ if (!response.ok)
10
+ return undefined;
11
+ const ip = (await response.text()).trim();
12
+ return ip || undefined;
13
+ }).pipe(Effect.catchAll(() => Effect.succeed(undefined))), ({ timeout }) => Effect.sync(() => clearTimeout(timeout)));
14
+ }
@@ -1,5 +1,7 @@
1
1
  import { type DnsRecordSelector } from './dns-records.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, DnsRecordType } from './providers/types.js';
4
6
  export interface RemoveDomainInput {
5
7
  account?: string;
@@ -31,8 +33,9 @@ export interface RemoveDomainResult {
31
33
  interface RemoveDomainDependencies {
32
34
  createProvider: (provider: string, opts: {
33
35
  account?: string;
34
- }) => Promise<DnsProvider>;
35
- resolveTarget: (input: Pick<RemoveDomainInput, 'account' | 'domain' | 'provider'>) => Promise<ResolvedDnsTarget>;
36
+ transportErrorCode?: DoomainErrorCode;
37
+ }) => DoomainEffect<DnsProvider>;
38
+ resolveTarget: (input: Pick<RemoveDomainInput, 'account' | 'domain' | 'provider'>) => DoomainEffect<ResolvedDnsTarget>;
36
39
  }
37
- export declare function removeDomain(input: RemoveDomainInput, dependencies?: RemoveDomainDependencies): Promise<RemoveDomainResult>;
40
+ export declare function removeDomain(input: RemoveDomainInput, dependencies?: RemoveDomainDependencies): DoomainEffect<RemoveDomainResult>;
38
41
  export {};