doomain 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/commands/auth/clerk.js +3 -2
- package/dist/commands/auth/logout/clerk.js +3 -2
- package/dist/commands/auth/logout/vercel.js +3 -2
- package/dist/commands/auth/vercel.js +5 -4
- package/dist/commands/clerk/domains/add.js +3 -2
- package/dist/commands/dns/diagnose.js +3 -2
- package/dist/commands/dns/point.js +7 -3
- package/dist/commands/dns/remove.js +3 -2
- package/dist/commands/domains/find.js +3 -2
- package/dist/commands/domains/list.js +46 -33
- package/dist/commands/link.js +4 -3
- package/dist/commands/projects/list.js +5 -2
- package/dist/commands/providers/connect.js +14 -23
- package/dist/commands/providers/disconnect.js +3 -2
- package/dist/commands/providers/status.js +2 -1
- package/dist/commands/providers/verify.js +3 -2
- package/dist/commands/schema.js +2 -1
- package/dist/commands/update.js +2 -1
- package/dist/commands/verify.js +5 -2
- package/dist/commands/version.d.ts +5 -0
- package/dist/commands/version.js +8 -0
- package/dist/commands/wizard.js +45 -46
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/clerk-domain.d.ts +2 -1
- package/dist/lib/clerk-domain.js +122 -110
- package/dist/lib/clerk.d.ts +9 -5
- package/dist/lib/clerk.js +45 -36
- package/dist/lib/command-schema.d.ts +2 -1
- package/dist/lib/command-schema.js +16 -9
- package/dist/lib/config.d.ts +6 -4
- package/dist/lib/config.js +29 -27
- package/dist/lib/diagnose-dns.d.ts +8 -5
- package/dist/lib/diagnose-dns.js +90 -92
- package/dist/lib/dns-propagation.d.ts +5 -4
- package/dist/lib/dns-propagation.js +80 -54
- package/dist/lib/dns-reconciliation.d.ts +4 -3
- package/dist/lib/dns-reconciliation.js +52 -47
- package/dist/lib/domain-provider.d.ts +5 -1
- package/dist/lib/domain-provider.js +136 -131
- package/dist/lib/effect.d.ts +6 -0
- package/dist/lib/effect.js +20 -0
- package/dist/lib/link-domain.d.ts +3 -2
- package/dist/lib/link-domain.js +239 -218
- package/dist/lib/point-domain.d.ts +7 -4
- package/dist/lib/point-domain.js +92 -90
- package/dist/lib/providers/cloudflare/index.d.ts +9 -8
- package/dist/lib/providers/cloudflare/index.js +93 -87
- package/dist/lib/providers/core/config.d.ts +4 -1
- package/dist/lib/providers/core/config.js +17 -13
- package/dist/lib/providers/core/http.d.ts +4 -1
- package/dist/lib/providers/core/http.js +28 -18
- package/dist/lib/providers/core/pagination.d.ts +3 -2
- package/dist/lib/providers/core/pagination.js +17 -14
- package/dist/lib/providers/core/planner.d.ts +5 -4
- package/dist/lib/providers/core/planner.js +18 -15
- package/dist/lib/providers/core/types.d.ts +11 -8
- package/dist/lib/providers/hostinger/index.d.ts +9 -8
- package/dist/lib/providers/hostinger/index.js +74 -72
- package/dist/lib/providers/namecheap/index.d.ts +10 -8
- package/dist/lib/providers/namecheap/index.js +134 -109
- package/dist/lib/providers/registry.d.ts +3 -3
- package/dist/lib/providers/registry.js +7 -3
- package/dist/lib/providers/spaceship/index.d.ts +9 -8
- package/dist/lib/providers/spaceship/index.js +48 -47
- package/dist/lib/providers/status.d.ts +2 -1
- package/dist/lib/providers/status.js +41 -34
- package/dist/lib/public-ip.d.ts +2 -0
- package/dist/lib/public-ip.js +14 -0
- package/dist/lib/remove-domain.d.ts +6 -3
- package/dist/lib/remove-domain.js +83 -77
- package/dist/lib/self-update.d.ts +3 -2
- package/dist/lib/self-update.js +15 -23
- package/dist/lib/vercel-auth.d.ts +2 -1
- package/dist/lib/vercel-auth.js +21 -24
- package/dist/lib/vercel.d.ts +16 -12
- package/dist/lib/vercel.js +174 -114
- package/oclif.manifest.json +21 -1
- package/package.json +8 -1
package/dist/lib/point-domain.js
CHANGED
|
@@ -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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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:
|
|
166
|
+
dryRun: false,
|
|
109
167
|
isDefaultAccount: resolved.isDefaultAccount,
|
|
110
168
|
provider: resolved.provider,
|
|
111
169
|
providerInferred: resolved.providerInferred,
|
|
112
|
-
propagated
|
|
113
|
-
propagation
|
|
114
|
-
|
|
115
|
-
|
|
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:
|
|
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():
|
|
10
|
-
listZones():
|
|
11
|
-
getZone(domain: string):
|
|
12
|
-
listRecords(zone: DnsZone):
|
|
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
|
-
}):
|
|
16
|
-
applyChanges(zone: DnsZone, plan: DnsChangePlan):
|
|
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):
|
|
21
|
-
deleteRecord(zone: DnsZone, record: DnsRecord):
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
return { ok: true };
|
|
102
|
+
verifyCredentials() {
|
|
103
|
+
return this.listZones().pipe(Effect.as({ ok: true }));
|
|
102
104
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
return zones;
|
|
121
|
+
return zones;
|
|
122
|
+
});
|
|
119
123
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return this.updateRecord(zone, existing, record);
|
|
174
|
-
|
|
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
|
-
|
|
180
|
+
deleteRecord(zone, record) {
|
|
177
181
|
if (!record.id)
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
194
|
+
updateRecord(zone, existing, record) {
|
|
190
195
|
if (!existing.id)
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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):
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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):
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
}):
|
|
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>):
|
|
13
|
+
export declare function paginateBySkip<T>(input: SkipPaginationInput<T>): DoomainEffect<T[]>;
|