doomain 0.1.0
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/LICENSE +21 -0
- package/README.md +293 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +8 -0
- package/dist/commands/auth/logout/vercel.d.ts +9 -0
- package/dist/commands/auth/logout/vercel.js +35 -0
- package/dist/commands/auth/vercel.d.ts +10 -0
- package/dist/commands/auth/vercel.js +69 -0
- package/dist/commands/domains/list.d.ts +10 -0
- package/dist/commands/domains/list.js +36 -0
- package/dist/commands/link.d.ts +21 -0
- package/dist/commands/link.js +63 -0
- package/dist/commands/projects/list.d.ts +9 -0
- package/dist/commands/projects/list.js +26 -0
- package/dist/commands/providers/add.d.ts +1 -0
- package/dist/commands/providers/add.js +1 -0
- package/dist/commands/providers/connect.d.ts +15 -0
- package/dist/commands/providers/connect.js +183 -0
- package/dist/commands/providers/disconnect.d.ts +13 -0
- package/dist/commands/providers/disconnect.js +52 -0
- package/dist/commands/providers/list.d.ts +8 -0
- package/dist/commands/providers/list.js +25 -0
- package/dist/commands/providers/status.d.ts +9 -0
- package/dist/commands/providers/status.js +31 -0
- package/dist/commands/providers/verify.d.ts +11 -0
- package/dist/commands/providers/verify.js +27 -0
- package/dist/commands/schema.d.ts +11 -0
- package/dist/commands/schema.js +29 -0
- package/dist/commands/verify.d.ts +12 -0
- package/dist/commands/verify.js +36 -0
- package/dist/commands/wizard.d.ts +9 -0
- package/dist/commands/wizard.js +322 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/command-schema.d.ts +14 -0
- package/dist/lib/command-schema.js +99 -0
- package/dist/lib/config.d.ts +34 -0
- package/dist/lib/config.js +49 -0
- package/dist/lib/errors.d.ts +7 -0
- package/dist/lib/errors.js +17 -0
- package/dist/lib/flags.d.ts +6 -0
- package/dist/lib/flags.js +10 -0
- package/dist/lib/link-domain.d.ts +47 -0
- package/dist/lib/link-domain.js +336 -0
- package/dist/lib/local-vercel.d.ts +6 -0
- package/dist/lib/local-vercel.js +27 -0
- package/dist/lib/output.d.ts +36 -0
- package/dist/lib/output.js +51 -0
- package/dist/lib/providers/cloudflare/index.d.ts +26 -0
- package/dist/lib/providers/cloudflare/index.js +219 -0
- package/dist/lib/providers/core/config.d.ts +5 -0
- package/dist/lib/providers/core/config.js +33 -0
- package/dist/lib/providers/core/errors.d.ts +6 -0
- package/dist/lib/providers/core/errors.js +18 -0
- package/dist/lib/providers/core/http.d.ts +17 -0
- package/dist/lib/providers/core/http.js +43 -0
- package/dist/lib/providers/core/pagination.d.ts +12 -0
- package/dist/lib/providers/core/pagination.js +15 -0
- package/dist/lib/providers/core/planner.d.ts +19 -0
- package/dist/lib/providers/core/planner.js +79 -0
- package/dist/lib/providers/core/types.d.ts +123 -0
- package/dist/lib/providers/core/types.js +1 -0
- package/dist/lib/providers/namecheap/index.d.ts +28 -0
- package/dist/lib/providers/namecheap/index.js +289 -0
- package/dist/lib/providers/registry.d.ts +4 -0
- package/dist/lib/providers/registry.js +21 -0
- package/dist/lib/providers/spaceship/index.d.ts +22 -0
- package/dist/lib/providers/spaceship/index.js +148 -0
- package/dist/lib/providers/status.d.ts +16 -0
- package/dist/lib/providers/status.js +33 -0
- package/dist/lib/providers/types.d.ts +1 -0
- package/dist/lib/providers/types.js +1 -0
- package/dist/lib/validate.d.ts +15 -0
- package/dist/lib/validate.js +62 -0
- package/dist/lib/vercel.d.ts +32 -0
- package/dist/lib/vercel.js +131 -0
- package/oclif.manifest.json +633 -0
- package/package.json +86 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { resolve4, resolveCname, resolveTxt } from 'node:dns/promises';
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
|
+
import { DoomainError } from './errors.js';
|
|
4
|
+
import { detectLocalVercelProject } from './local-vercel.js';
|
|
5
|
+
import { createProvider, getProviderDefinition, listProviderDefinitions } from './providers/registry.js';
|
|
6
|
+
import { isProviderConfigured } from './providers/status.js';
|
|
7
|
+
import { normalizeDomain, normalizeSubdomain } from './validate.js';
|
|
8
|
+
import { createVercelClient, resolveVercelConfig, VERCEL_APEX_A_RECORD, VERCEL_CNAME_RECORD } from './vercel.js';
|
|
9
|
+
function cleanDnsValue(value) {
|
|
10
|
+
return value.toLowerCase().replace(/\.$/, '');
|
|
11
|
+
}
|
|
12
|
+
function recordFqdn(record, zoneDomain) {
|
|
13
|
+
return record.name === '@' ? zoneDomain : `${record.name}.${zoneDomain}`;
|
|
14
|
+
}
|
|
15
|
+
async function resolveConfiguredDomain(domain) {
|
|
16
|
+
const config = await loadConfig();
|
|
17
|
+
const resolved = domain ?? process.env.DOOMAIN_DOMAIN ?? config.defaults?.domain;
|
|
18
|
+
if (!resolved)
|
|
19
|
+
throw new DoomainError('MISSING_ARGUMENT', 'Domain is required. Use --domain or set a default domain.');
|
|
20
|
+
return resolved;
|
|
21
|
+
}
|
|
22
|
+
function resolveProject(project) {
|
|
23
|
+
if (project)
|
|
24
|
+
return { project, localProjectDetected: false };
|
|
25
|
+
const localProject = detectLocalVercelProject();
|
|
26
|
+
if (localProject)
|
|
27
|
+
return { project: localProject.projectId, localProjectDetected: true };
|
|
28
|
+
throw new DoomainError('VERCEL_PROJECT_NOT_LINKED', 'No linked Vercel project found. Run inside a Vercel project or pass --project.');
|
|
29
|
+
}
|
|
30
|
+
async function resolveZone(provider, zoneDomain) {
|
|
31
|
+
const zone = await provider.getZone(zoneDomain);
|
|
32
|
+
if (!zone) {
|
|
33
|
+
throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', `${provider.name} does not have a DNS zone for ${zoneDomain}.`);
|
|
34
|
+
}
|
|
35
|
+
return zone;
|
|
36
|
+
}
|
|
37
|
+
function resolveRequestedDomain(opts) {
|
|
38
|
+
if (opts.apex && opts.subdomain) {
|
|
39
|
+
throw new DoomainError('INVALID_INPUT', 'Use either --apex or --subdomain, not both.');
|
|
40
|
+
}
|
|
41
|
+
const domain = normalizeDomain(opts.domain);
|
|
42
|
+
if (opts.apex)
|
|
43
|
+
return { forceExactZone: true, fullDomain: domain };
|
|
44
|
+
if (!opts.subdomain)
|
|
45
|
+
return { forceExactZone: false, fullDomain: domain };
|
|
46
|
+
return { forceExactZone: false, fullDomain: `${normalizeSubdomain(opts.subdomain)}.${domain}` };
|
|
47
|
+
}
|
|
48
|
+
function zoneMatchesDomain(fullDomain, zoneDomain, forceExactZone) {
|
|
49
|
+
if (fullDomain === zoneDomain)
|
|
50
|
+
return true;
|
|
51
|
+
if (forceExactZone)
|
|
52
|
+
return false;
|
|
53
|
+
return fullDomain.endsWith(`.${zoneDomain}`);
|
|
54
|
+
}
|
|
55
|
+
function targetFromZone(fullDomain, zoneDomain) {
|
|
56
|
+
if (fullDomain === zoneDomain) {
|
|
57
|
+
return { fullDomain, isApex: true, recordName: '@', zoneDomain };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
fullDomain,
|
|
61
|
+
isApex: false,
|
|
62
|
+
recordName: fullDomain.slice(0, -(zoneDomain.length + 1)),
|
|
63
|
+
zoneDomain,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function candidateDetails(candidates) {
|
|
67
|
+
return candidates.map((candidate) => ({
|
|
68
|
+
provider: candidate.provider,
|
|
69
|
+
providerName: candidate.providerName,
|
|
70
|
+
zoneDomain: candidate.zone.name,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
async function loadProviderZones(definition) {
|
|
74
|
+
const provider = await createProvider(definition.id);
|
|
75
|
+
const zones = await provider.listZones();
|
|
76
|
+
return {
|
|
77
|
+
candidates: zones.map((zone) => ({ provider: definition.id, providerName: definition.displayName, zone })),
|
|
78
|
+
search: { displayName: definition.displayName, id: definition.id, zones: zones.map((zone) => zone.name) },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function loadConfiguredProviderZones(providerId) {
|
|
82
|
+
if (providerId) {
|
|
83
|
+
const definition = getProviderDefinition(providerId);
|
|
84
|
+
const result = await loadProviderZones(definition);
|
|
85
|
+
return { candidates: result.candidates, providerInferred: false, searched: [result.search] };
|
|
86
|
+
}
|
|
87
|
+
const config = await loadConfig();
|
|
88
|
+
const definitions = listProviderDefinitions().filter((definition) => isProviderConfigured(definition, config));
|
|
89
|
+
if (definitions.length === 0) {
|
|
90
|
+
throw new DoomainError('CONFIG_NOT_FOUND', 'No DNS provider is configured. Run `doomain providers connect` first.');
|
|
91
|
+
}
|
|
92
|
+
const results = await Promise.all(definitions.map(async (definition) => {
|
|
93
|
+
try {
|
|
94
|
+
return await loadProviderZones(definition);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
candidates: [],
|
|
99
|
+
search: {
|
|
100
|
+
displayName: definition.displayName,
|
|
101
|
+
error: error instanceof Error ? error.message : String(error),
|
|
102
|
+
id: definition.id,
|
|
103
|
+
zones: [],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}));
|
|
108
|
+
return {
|
|
109
|
+
candidates: results.flatMap((result) => result.candidates),
|
|
110
|
+
providerInferred: true,
|
|
111
|
+
searched: results.map((result) => result.search),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function resolveProviderTarget(input) {
|
|
115
|
+
const requested = resolveRequestedDomain({
|
|
116
|
+
apex: input.apex,
|
|
117
|
+
domain: await resolveConfiguredDomain(input.domain),
|
|
118
|
+
subdomain: input.subdomain,
|
|
119
|
+
});
|
|
120
|
+
const zones = await loadConfiguredProviderZones(input.provider);
|
|
121
|
+
const matches = zones.candidates
|
|
122
|
+
.filter((candidate) => zoneMatchesDomain(requested.fullDomain, candidate.zone.name, requested.forceExactZone))
|
|
123
|
+
.sort((a, b) => b.zone.name.length - a.zone.name.length);
|
|
124
|
+
if (matches.length === 0) {
|
|
125
|
+
const providerMessage = input.provider
|
|
126
|
+
? `${getProviderDefinition(input.provider).displayName} does not have a matching DNS zone for ${requested.fullDomain}.`
|
|
127
|
+
: `No configured DNS provider has a matching DNS zone for ${requested.fullDomain}.`;
|
|
128
|
+
throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', providerMessage, { domain: requested.fullDomain, providers: zones.searched });
|
|
129
|
+
}
|
|
130
|
+
const bestLength = matches[0].zone.name.length;
|
|
131
|
+
const bestMatches = matches.filter((candidate) => candidate.zone.name.length === bestLength);
|
|
132
|
+
const uniqueBestMatches = bestMatches.filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider && item.zone.name === candidate.zone.name) === index);
|
|
133
|
+
if (uniqueBestMatches.length > 1) {
|
|
134
|
+
throw new DoomainError('PROVIDER_ZONE_AMBIGUOUS', `Multiple DNS providers have a matching DNS zone for ${requested.fullDomain}. Pass --provider to choose one.`, { candidates: candidateDetails(uniqueBestMatches), domain: requested.fullDomain });
|
|
135
|
+
}
|
|
136
|
+
const selected = uniqueBestMatches[0];
|
|
137
|
+
return {
|
|
138
|
+
provider: selected.provider,
|
|
139
|
+
providerInferred: zones.providerInferred,
|
|
140
|
+
target: targetFromZone(requested.fullDomain, selected.zone.name),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function withProviderRecordOptions(provider, record) {
|
|
144
|
+
if (provider !== 'cloudflare' || !['A', 'AAAA', 'CNAME'].includes(record.type))
|
|
145
|
+
return record;
|
|
146
|
+
return { ...record, proxied: false };
|
|
147
|
+
}
|
|
148
|
+
function planBaseRecord(opts) {
|
|
149
|
+
const record = opts.isApex
|
|
150
|
+
? { type: 'A', name: '@', value: VERCEL_APEX_A_RECORD, ttl: 3600 }
|
|
151
|
+
: { type: 'CNAME', name: opts.recordName, value: opts.cname ?? VERCEL_CNAME_RECORD, ttl: 3600 };
|
|
152
|
+
return withProviderRecordOptions(opts.provider, record);
|
|
153
|
+
}
|
|
154
|
+
function planVerificationRecords(provider, raw, zoneDomain) {
|
|
155
|
+
return verificationRecords(raw, zoneDomain).map((record) => withProviderRecordOptions(provider, record));
|
|
156
|
+
}
|
|
157
|
+
function cleanVerificationName(name, zoneDomain) {
|
|
158
|
+
const cleaned = name.trim().toLowerCase().replace(/\.$/, '');
|
|
159
|
+
const zone = zoneDomain.toLowerCase().replace(/\.$/, '');
|
|
160
|
+
if (cleaned === zone)
|
|
161
|
+
return '@';
|
|
162
|
+
if (cleaned.endsWith(`.${zone}`))
|
|
163
|
+
return cleaned.slice(0, -(zone.length + 1)) || '@';
|
|
164
|
+
return cleaned;
|
|
165
|
+
}
|
|
166
|
+
function collectVerificationRecords(raw, seen = new Set()) {
|
|
167
|
+
if (!raw || typeof raw !== 'object' || seen.has(raw))
|
|
168
|
+
return [];
|
|
169
|
+
seen.add(raw);
|
|
170
|
+
if (Array.isArray(raw))
|
|
171
|
+
return raw.flatMap((item) => collectVerificationRecords(item, seen));
|
|
172
|
+
const object = raw;
|
|
173
|
+
const verification = object.verification;
|
|
174
|
+
const records = Array.isArray(verification) ? verification : [];
|
|
175
|
+
const nested = Object.entries(object).flatMap(([key, value]) => (key === 'verification' ? [] : collectVerificationRecords(value, seen)));
|
|
176
|
+
return [...records, ...nested];
|
|
177
|
+
}
|
|
178
|
+
function uniqueRecords(records) {
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
const unique = [];
|
|
181
|
+
for (const record of records) {
|
|
182
|
+
const key = `${record.type}:${record.name}:${record.value}:${record.proxied ?? ''}`;
|
|
183
|
+
if (seen.has(key))
|
|
184
|
+
continue;
|
|
185
|
+
seen.add(key);
|
|
186
|
+
unique.push(record);
|
|
187
|
+
}
|
|
188
|
+
return unique;
|
|
189
|
+
}
|
|
190
|
+
export function verificationRecords(raw, zoneDomain) {
|
|
191
|
+
const verification = collectVerificationRecords(raw);
|
|
192
|
+
return verification.flatMap((record) => {
|
|
193
|
+
if (record.type !== 'TXT' || !record.value)
|
|
194
|
+
return [];
|
|
195
|
+
const name = record.domain ?? record.name;
|
|
196
|
+
if (!name)
|
|
197
|
+
return [];
|
|
198
|
+
return [{ type: 'TXT', name: cleanVerificationName(name, zoneDomain), value: record.value, ttl: 3600 }];
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async function wait(milliseconds) {
|
|
202
|
+
await new Promise((resolve) => {
|
|
203
|
+
setTimeout(resolve, milliseconds);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async function isRecordPropagated(record, zoneDomain) {
|
|
207
|
+
const fqdn = recordFqdn(record, zoneDomain);
|
|
208
|
+
try {
|
|
209
|
+
if (record.type === 'A') {
|
|
210
|
+
const values = await resolve4(fqdn);
|
|
211
|
+
return values.includes(record.value);
|
|
212
|
+
}
|
|
213
|
+
if (record.type === 'CNAME') {
|
|
214
|
+
const values = await resolveCname(fqdn);
|
|
215
|
+
return values.map(cleanDnsValue).includes(cleanDnsValue(record.value));
|
|
216
|
+
}
|
|
217
|
+
if (record.type === 'TXT') {
|
|
218
|
+
const values = (await resolveTxt(fqdn)).map((chunks) => chunks.join(''));
|
|
219
|
+
return values.includes(record.value);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
function errorMessage(error) {
|
|
228
|
+
if (error instanceof Error)
|
|
229
|
+
return error.message;
|
|
230
|
+
return String(error);
|
|
231
|
+
}
|
|
232
|
+
async function areRecordsPropagated(records, zoneDomain) {
|
|
233
|
+
const results = await Promise.all(records.map((record) => isRecordPropagated(record, zoneDomain)));
|
|
234
|
+
return results.every(Boolean);
|
|
235
|
+
}
|
|
236
|
+
async function waitForVercelDomainReady(opts) {
|
|
237
|
+
const vercel = createVercelClient(await resolveVercelConfig());
|
|
238
|
+
const timeoutSeconds = opts.input.timeoutSeconds ?? 300;
|
|
239
|
+
const startedAt = Date.now();
|
|
240
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
241
|
+
let lastError;
|
|
242
|
+
let propagated = false;
|
|
243
|
+
let attempt = 1;
|
|
244
|
+
while (Date.now() <= deadline) {
|
|
245
|
+
const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
|
|
246
|
+
reportProgress(opts.input, 'vercel:verify', `Verifying domain in Vercel (attempt ${attempt}, ${elapsedSeconds}s elapsed)`);
|
|
247
|
+
try {
|
|
248
|
+
const current = await vercel.getProjectDomain(opts.project, opts.domain);
|
|
249
|
+
if (current.verified === true)
|
|
250
|
+
return { propagated: true, verified: true };
|
|
251
|
+
const result = await vercel.verifyProjectDomain(opts.project, opts.domain);
|
|
252
|
+
if (result.verified !== false)
|
|
253
|
+
return { propagated: true, verified: true };
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
// Vercel returns an error while DNS is still propagating.
|
|
257
|
+
lastError = error;
|
|
258
|
+
}
|
|
259
|
+
propagated = await areRecordsPropagated(opts.records, opts.zoneDomain);
|
|
260
|
+
reportProgress(opts.input, 'dns:wait', propagated
|
|
261
|
+
? 'DNS is visible publicly; Vercel verification is still pending'
|
|
262
|
+
: 'DNS records were saved; public DNS is still catching up');
|
|
263
|
+
attempt += 1;
|
|
264
|
+
await wait(5000);
|
|
265
|
+
}
|
|
266
|
+
if (lastError) {
|
|
267
|
+
throw new DoomainError('DOMAIN_VERIFY_FAILED', `Vercel did not verify ${opts.domain} within ${timeoutSeconds} seconds. Last Vercel response: ${errorMessage(lastError)}`);
|
|
268
|
+
}
|
|
269
|
+
return { propagated, verified: false };
|
|
270
|
+
}
|
|
271
|
+
function reportProgress(input, stage, message) {
|
|
272
|
+
input.progress?.({ message, stage });
|
|
273
|
+
}
|
|
274
|
+
export async function createLinkPlan(input) {
|
|
275
|
+
const project = resolveProject(input.project);
|
|
276
|
+
const resolved = await resolveProviderTarget(input);
|
|
277
|
+
const { provider, providerInferred, target } = resolved;
|
|
278
|
+
const record = planBaseRecord({ isApex: target.isApex, provider, recordName: target.recordName });
|
|
279
|
+
return {
|
|
280
|
+
provider,
|
|
281
|
+
providerInferred,
|
|
282
|
+
project: project.project,
|
|
283
|
+
recordName: target.recordName,
|
|
284
|
+
zoneDomain: target.zoneDomain,
|
|
285
|
+
domain: target.fullDomain,
|
|
286
|
+
isApex: target.isApex,
|
|
287
|
+
records: [record],
|
|
288
|
+
localProjectDetected: project.localProjectDetected,
|
|
289
|
+
actions: ['vercel:addProjectDomain', 'dns:upsertRecord', 'dns:waitPropagation', 'vercel:verifyProjectDomain'],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
export async function linkDomain(input) {
|
|
293
|
+
const plan = await createLinkPlan(input);
|
|
294
|
+
if (input.dryRun) {
|
|
295
|
+
return {
|
|
296
|
+
...plan,
|
|
297
|
+
dryRun: true,
|
|
298
|
+
dns: { updated: false, propagated: false, skipped: [] },
|
|
299
|
+
vercel: { added: false, alreadyAdded: false, verified: false },
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const vercel = createVercelClient(await resolveVercelConfig());
|
|
303
|
+
const provider = await createProvider(plan.provider);
|
|
304
|
+
reportProgress(input, 'dns:resolve-zone', `Finding ${provider.name} DNS zone`);
|
|
305
|
+
const zone = await resolveZone(provider, plan.zoneDomain);
|
|
306
|
+
reportProgress(input, 'vercel:add-domain', 'Adding domain to Vercel');
|
|
307
|
+
const addResult = await vercel.addDomainToProject(plan.project, plan.domain);
|
|
308
|
+
reportProgress(input, 'vercel:get-target', 'Reading Vercel DNS target');
|
|
309
|
+
const cname = plan.isApex ? undefined : await vercel.getRecommendedCname(plan.domain);
|
|
310
|
+
reportProgress(input, 'vercel:get-domain', 'Reading Vercel verification records');
|
|
311
|
+
const projectDomain = await vercel.getProjectDomain(plan.project, plan.domain);
|
|
312
|
+
const baseRecord = planBaseRecord({ isApex: plan.isApex, provider: plan.provider, recordName: plan.recordName, cname });
|
|
313
|
+
const verificationDnsRecords = uniqueRecords([
|
|
314
|
+
...planVerificationRecords(plan.provider, addResult.raw, plan.zoneDomain),
|
|
315
|
+
...planVerificationRecords(plan.provider, projectDomain, plan.zoneDomain),
|
|
316
|
+
]);
|
|
317
|
+
const records = [baseRecord, ...verificationDnsRecords];
|
|
318
|
+
reportProgress(input, 'dns:plan', `Reading ${provider.name} DNS records`);
|
|
319
|
+
const dnsPlan = await provider.planChanges(zone, records, { force: input.force });
|
|
320
|
+
reportProgress(input, 'dns:apply', `Updating DNS records in ${provider.name}`);
|
|
321
|
+
const dnsResult = await provider.applyChanges(zone, dnsPlan, { force: input.force });
|
|
322
|
+
const shouldWait = input.wait ?? true;
|
|
323
|
+
if (shouldWait) {
|
|
324
|
+
reportProgress(input, 'dns:wait', verificationDnsRecords.length > 0 ? 'DNS records saved; asking Vercel to verify ownership' : 'DNS records saved; asking Vercel to verify');
|
|
325
|
+
}
|
|
326
|
+
const waitResult = shouldWait
|
|
327
|
+
? await waitForVercelDomainReady({ domain: plan.domain, input, project: plan.project, records, zoneDomain: plan.zoneDomain })
|
|
328
|
+
: { propagated: false, verified: false };
|
|
329
|
+
return {
|
|
330
|
+
...plan,
|
|
331
|
+
records,
|
|
332
|
+
dryRun: false,
|
|
333
|
+
dns: { updated: dnsResult.applied.length > 0, propagated: waitResult.propagated, skipped: dnsResult.skipped },
|
|
334
|
+
vercel: { added: !addResult.alreadyAdded, alreadyAdded: addResult.alreadyAdded, verified: waitResult.verified },
|
|
335
|
+
};
|
|
336
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, parse } from 'node:path';
|
|
3
|
+
export function detectLocalVercelProject(start = process.cwd()) {
|
|
4
|
+
let current = start;
|
|
5
|
+
const root = parse(start).root;
|
|
6
|
+
while (true) {
|
|
7
|
+
const projectPath = join(current, '.vercel', 'project.json');
|
|
8
|
+
if (existsSync(projectPath)) {
|
|
9
|
+
try {
|
|
10
|
+
const data = JSON.parse(readFileSync(projectPath, 'utf8'));
|
|
11
|
+
if (data.projectId) {
|
|
12
|
+
return {
|
|
13
|
+
projectId: data.projectId,
|
|
14
|
+
orgId: data.orgId,
|
|
15
|
+
root: current,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (current === root)
|
|
24
|
+
return null;
|
|
25
|
+
current = dirname(current);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type DoomainErrorCode } from './errors.js';
|
|
2
|
+
export interface JsonSuccess<T> {
|
|
3
|
+
ok: true;
|
|
4
|
+
data: T;
|
|
5
|
+
}
|
|
6
|
+
export interface JsonFailure {
|
|
7
|
+
ok: false;
|
|
8
|
+
error: {
|
|
9
|
+
code: DoomainErrorCode;
|
|
10
|
+
message: string;
|
|
11
|
+
details?: unknown;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export type JsonEnvelope<T> = JsonSuccess<T> | JsonFailure;
|
|
15
|
+
export interface OutputContext {
|
|
16
|
+
readonly json: boolean;
|
|
17
|
+
info(message: string): void;
|
|
18
|
+
success(message: string): void;
|
|
19
|
+
warn(message: string): void;
|
|
20
|
+
error(message: string): void;
|
|
21
|
+
intro(message: string): void;
|
|
22
|
+
outro(message: string): void;
|
|
23
|
+
spinner(): {
|
|
24
|
+
error(message?: string): void;
|
|
25
|
+
message(message?: string): void;
|
|
26
|
+
start(message: string): void;
|
|
27
|
+
stop(message?: string): void;
|
|
28
|
+
};
|
|
29
|
+
result<T>(data: T): void;
|
|
30
|
+
}
|
|
31
|
+
export declare function shouldUseJson(flag?: boolean): boolean;
|
|
32
|
+
export declare function writeJson<T>(data: JsonEnvelope<T>): void;
|
|
33
|
+
export declare function createOutput(opts?: {
|
|
34
|
+
json?: boolean;
|
|
35
|
+
}): OutputContext;
|
|
36
|
+
export declare function outputError(json: boolean, error: unknown, fallbackCode: DoomainErrorCode): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { DoomainError, toDoomainError } from './errors.js';
|
|
3
|
+
const noop = () => { };
|
|
4
|
+
export function shouldUseJson(flag) {
|
|
5
|
+
return Boolean(flag) || !process.stdout.isTTY;
|
|
6
|
+
}
|
|
7
|
+
export function writeJson(data) {
|
|
8
|
+
process.stdout.write(`${JSON.stringify(data)}\n`);
|
|
9
|
+
}
|
|
10
|
+
export function createOutput(opts = {}) {
|
|
11
|
+
const json = shouldUseJson(opts.json);
|
|
12
|
+
if (json) {
|
|
13
|
+
return {
|
|
14
|
+
json: true,
|
|
15
|
+
info: noop,
|
|
16
|
+
success: noop,
|
|
17
|
+
warn: noop,
|
|
18
|
+
error: noop,
|
|
19
|
+
intro: noop,
|
|
20
|
+
outro: noop,
|
|
21
|
+
spinner: () => ({ error: noop, message: noop, start: noop, stop: noop }),
|
|
22
|
+
result: (data) => writeJson({ ok: true, data }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
json: false,
|
|
27
|
+
info: (message) => p.log.info(message),
|
|
28
|
+
success: (message) => p.log.success(message),
|
|
29
|
+
warn: (message) => p.log.warning(message),
|
|
30
|
+
error: (message) => p.log.error(message),
|
|
31
|
+
intro: (message) => p.intro(message),
|
|
32
|
+
outro: (message) => p.outro(message),
|
|
33
|
+
spinner: () => p.spinner(),
|
|
34
|
+
result: noop,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function outputError(json, error, fallbackCode) {
|
|
38
|
+
const doomainError = error instanceof DoomainError ? error : toDoomainError(error, fallbackCode);
|
|
39
|
+
if (json) {
|
|
40
|
+
writeJson({
|
|
41
|
+
ok: false,
|
|
42
|
+
error: {
|
|
43
|
+
code: doomainError.code,
|
|
44
|
+
message: doomainError.message,
|
|
45
|
+
...(doomainError.details === undefined ? {} : { details: doomainError.details }),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
p.log.error(doomainError.message);
|
|
51
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DnsChange, DnsChangePlan, DnsProvider, DnsProviderDefinition, DnsRecord, DnsRecordInput, DnsZone, ProviderCapabilities, ProviderContext, ProviderHealth } from '../types.js';
|
|
2
|
+
export declare class CloudflareProvider implements DnsProvider {
|
|
3
|
+
readonly capabilities: ProviderCapabilities;
|
|
4
|
+
readonly id = "cloudflare";
|
|
5
|
+
readonly name = "Cloudflare";
|
|
6
|
+
private readonly accountId;
|
|
7
|
+
private readonly http;
|
|
8
|
+
constructor(context: ProviderContext);
|
|
9
|
+
verifyCredentials(): Promise<ProviderHealth>;
|
|
10
|
+
listZones(): Promise<DnsZone[]>;
|
|
11
|
+
getZone(domain: string): Promise<DnsZone | null>;
|
|
12
|
+
listRecords(zone: DnsZone): Promise<DnsRecord[]>;
|
|
13
|
+
planChanges(zone: DnsZone, desired: DnsRecordInput[], opts?: {
|
|
14
|
+
force?: boolean;
|
|
15
|
+
}): Promise<DnsChangePlan>;
|
|
16
|
+
applyChanges(zone: DnsZone, plan: DnsChangePlan): Promise<{
|
|
17
|
+
applied: DnsChange[];
|
|
18
|
+
skipped: DnsRecordInput[];
|
|
19
|
+
}>;
|
|
20
|
+
upsertRecord(zone: DnsZone, record: DnsRecordInput): Promise<DnsRecord>;
|
|
21
|
+
deleteRecord(zone: DnsZone, record: DnsRecord): Promise<void>;
|
|
22
|
+
private createRecord;
|
|
23
|
+
private updateRecord;
|
|
24
|
+
private request;
|
|
25
|
+
}
|
|
26
|
+
export declare const cloudflareProviderDefinition: DnsProviderDefinition;
|