doomain 0.1.2 → 0.1.4

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.
@@ -4,6 +4,15 @@ import { domainFlag, jsonFlag, providerFlag } from '../../lib/flags.js';
4
4
  import { createOutput, outputError } from '../../lib/output.js';
5
5
  import { createProvider } from '../../lib/providers/registry.js';
6
6
  import { normalizeDomain } from '../../lib/validate.js';
7
+ async function resolveZones(provider, domain) {
8
+ if (!domain)
9
+ return provider.listZones();
10
+ const normalized = normalizeDomain(domain);
11
+ const zone = await provider.getZone(normalized);
12
+ if (!zone)
13
+ throw new Error(`${provider.name} does not have a DNS zone for ${normalized}.`);
14
+ return [zone];
15
+ }
7
16
  export default class DomainsList extends Command {
8
17
  static description = 'List DNS zones and records for a provider.';
9
18
  static flags = {
@@ -17,7 +26,7 @@ export default class DomainsList extends Command {
17
26
  try {
18
27
  const config = await loadConfig();
19
28
  const provider = await createProvider(flags.provider ?? process.env.DOOMAIN_PROVIDER ?? config.defaults?.provider ?? 'spaceship');
20
- const zones = flags.domain ? [{ id: normalizeDomain(flags.domain), name: normalizeDomain(flags.domain) }] : await provider.listZones();
29
+ const zones = await resolveZones(provider, flags.domain);
21
30
  const results = [];
22
31
  for (const zone of zones) {
23
32
  const records = await provider.listRecords(zone);
@@ -12,7 +12,6 @@ import { listGlobalVercelTokens } from '../lib/vercel-auth.js';
12
12
  import { createVercelClient } from '../lib/vercel.js';
13
13
  const PERSONAL_ACCOUNT = '__personal__';
14
14
  const NEW_TOKEN = '__new_token__';
15
- const SAVED_TOKEN = '__saved_token__';
16
15
  function cancelIfNeeded(value) {
17
16
  if (p.isCancel(value)) {
18
17
  p.cancel('Cancelled');
@@ -111,23 +110,18 @@ function teamLabel(team) {
111
110
  function globalTokenLabel(token) {
112
111
  return token.source === 'environment' ? `Use ${token.label}` : `Use ${token.label} token`;
113
112
  }
114
- async function promptVercelToken(globalTokens, savedToken) {
113
+ async function promptVercelToken(globalTokens) {
115
114
  if (globalTokens.length > 0) {
116
115
  const selected = await p.select({
117
116
  message: 'Vercel token',
118
117
  options: [
119
118
  ...globalTokens.map((token, index) => ({ label: globalTokenLabel(token), value: String(index), hint: maskSecret(token.token) })),
120
- ...(savedToken && !globalTokens.some((token) => token.token === savedToken)
121
- ? [{ label: 'Use saved Doomain token', value: SAVED_TOKEN, hint: maskSecret(savedToken) }]
122
- : []),
123
119
  { label: 'Enter a new token', value: NEW_TOKEN },
124
120
  ],
125
121
  });
126
122
  const resolved = cancelIfNeeded(selected);
127
123
  if (resolved === null)
128
124
  return null;
129
- if (resolved === SAVED_TOKEN)
130
- return savedToken ?? null;
131
125
  if (resolved !== NEW_TOKEN)
132
126
  return globalTokens[Number(resolved)]?.token ?? null;
133
127
  }
@@ -156,13 +150,13 @@ export default class Wizard extends Command {
156
150
  const config = await loadConfig();
157
151
  const providerDefinitions = listProviderDefinitions();
158
152
  const localProject = detectLocalVercelProject();
159
- const globalVercelTokens = await listGlobalVercelTokens();
160
153
  let vercelToken = config.vercel?.token;
161
154
  let vercelTeamId = process.env.VERCEL_TEAM_ID || localProject?.orgId || config.vercel?.teamId;
162
155
  const defaultProvider = process.env.DOOMAIN_PROVIDER || config.defaults?.provider;
163
156
  const defaultDomain = process.env.DOOMAIN_DOMAIN || config.defaults?.domain;
164
- if (globalVercelTokens.length > 0 || !vercelToken) {
165
- vercelToken = (await promptVercelToken(globalVercelTokens, vercelToken)) ?? undefined;
157
+ if (!vercelToken) {
158
+ const globalVercelTokens = await listGlobalVercelTokens();
159
+ vercelToken = (await promptVercelToken(globalVercelTokens)) ?? undefined;
166
160
  if (!vercelToken)
167
161
  return;
168
162
  }
@@ -187,6 +187,20 @@ function uniqueRecords(records) {
187
187
  }
188
188
  return unique;
189
189
  }
190
+ function recordKey(record) {
191
+ return `${record.type}:${record.name}:${record.value}:${record.proxied ?? ''}`;
192
+ }
193
+ function mergeRecords(records, nextRecords) {
194
+ const existing = new Set(records.map(recordKey));
195
+ const added = nextRecords.filter((record) => !existing.has(recordKey(record)));
196
+ return { records: [...records, ...added], added };
197
+ }
198
+ function errorDetails(error) {
199
+ return error instanceof DoomainError ? error.details : undefined;
200
+ }
201
+ function isDomainConfigReady(config) {
202
+ return config?.misconfigured !== true;
203
+ }
190
204
  export function verificationRecords(raw, zoneDomain) {
191
205
  const verification = collectVerificationRecords(raw);
192
206
  return verification.flatMap((record) => {
@@ -239,32 +253,58 @@ async function waitForVercelDomainReady(opts) {
239
253
  const startedAt = Date.now();
240
254
  const deadline = Date.now() + timeoutSeconds * 1000;
241
255
  let lastError;
256
+ let lastConfig;
242
257
  let propagated = false;
243
258
  let attempt = 1;
259
+ let records = opts.records;
260
+ async function applyVerificationRecords(raw) {
261
+ const nextRecords = planVerificationRecords(opts.providerId, raw, opts.zoneDomain);
262
+ const merged = mergeRecords(records, nextRecords);
263
+ if (merged.added.length === 0)
264
+ return;
265
+ reportProgress(opts.input, 'dns:plan', `Found ${merged.added.length} new Vercel ownership record${merged.added.length === 1 ? '' : 's'}`);
266
+ const dnsPlan = await opts.provider.planChanges(opts.zone, merged.added, { force: opts.force });
267
+ reportProgress(opts.input, 'dns:apply', `Updating ownership records in ${opts.provider.name}`);
268
+ await opts.provider.applyChanges(opts.zone, dnsPlan, { force: opts.force });
269
+ records = merged.records;
270
+ }
271
+ async function isVercelReady(raw) {
272
+ await applyVerificationRecords(raw);
273
+ if (raw.verified !== true)
274
+ return false;
275
+ lastConfig = await vercel.getDomainConfig(opts.domain);
276
+ return isDomainConfigReady(lastConfig);
277
+ }
244
278
  while (Date.now() <= deadline) {
245
279
  const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
246
280
  reportProgress(opts.input, 'vercel:verify', `Verifying domain in Vercel (attempt ${attempt}, ${elapsedSeconds}s elapsed)`);
247
281
  try {
248
282
  const current = await vercel.getProjectDomain(opts.project, opts.domain);
249
- if (current.verified === true)
283
+ if (await isVercelReady(current))
250
284
  return { propagated: true, verified: true };
251
285
  const result = await vercel.verifyProjectDomain(opts.project, opts.domain);
252
- if (result.verified !== false)
286
+ if (await isVercelReady(result))
253
287
  return { propagated: true, verified: true };
254
288
  }
255
289
  catch (error) {
256
290
  // Vercel returns an error while DNS is still propagating.
257
291
  lastError = error;
292
+ await applyVerificationRecords(errorDetails(error));
258
293
  }
259
- propagated = await areRecordsPropagated(opts.records, opts.zoneDomain);
294
+ propagated = await areRecordsPropagated(records, opts.zoneDomain);
260
295
  reportProgress(opts.input, 'dns:wait', propagated
261
296
  ? 'DNS is visible publicly; Vercel verification is still pending'
262
297
  : 'DNS records were saved; public DNS is still catching up');
263
298
  attempt += 1;
299
+ if (Date.now() > deadline)
300
+ break;
264
301
  await wait(5000);
265
302
  }
266
303
  if (lastError) {
267
- throw new DoomainError('DOMAIN_VERIFY_FAILED', `Vercel did not verify ${opts.domain} within ${timeoutSeconds} seconds. Last Vercel response: ${errorMessage(lastError)}`);
304
+ throw new DoomainError('DOMAIN_VERIFY_FAILED', `Vercel did not verify ${opts.domain} within ${timeoutSeconds} seconds. Last Vercel response: ${errorMessage(lastError)}`, { domainConfig: lastConfig, error: errorDetails(lastError) });
305
+ }
306
+ if (lastConfig?.misconfigured === true) {
307
+ throw new DoomainError('DOMAIN_VERIFY_FAILED', `Vercel verified ${opts.domain}, but its DNS configuration is still invalid after ${timeoutSeconds} seconds.`, { domainConfig: lastConfig });
268
308
  }
269
309
  return { propagated, verified: false };
270
310
  }
@@ -324,7 +364,17 @@ export async function linkDomain(input) {
324
364
  reportProgress(input, 'dns:wait', verificationDnsRecords.length > 0 ? 'DNS records saved; asking Vercel to verify ownership' : 'DNS records saved; asking Vercel to verify');
325
365
  }
326
366
  const waitResult = shouldWait
327
- ? await waitForVercelDomainReady({ domain: plan.domain, input, project: plan.project, records, zoneDomain: plan.zoneDomain })
367
+ ? await waitForVercelDomainReady({
368
+ domain: plan.domain,
369
+ force: input.force,
370
+ input,
371
+ project: plan.project,
372
+ provider,
373
+ providerId: plan.provider,
374
+ records,
375
+ zone,
376
+ zoneDomain: plan.zoneDomain,
377
+ })
328
378
  : { propagated: false, verified: false };
329
379
  return {
330
380
  ...plan,
@@ -300,41 +300,6 @@
300
300
  "list.js"
301
301
  ]
302
302
  },
303
- "projects:list": {
304
- "aliases": [],
305
- "args": {},
306
- "description": "List Vercel projects.",
307
- "flags": {
308
- "json": {
309
- "description": "Output a single JSON object and never prompt.",
310
- "name": "json",
311
- "allowNo": false,
312
- "type": "boolean"
313
- },
314
- "search": {
315
- "description": "Filter projects by search term.",
316
- "name": "search",
317
- "hasDynamicHelp": false,
318
- "multiple": false,
319
- "type": "option"
320
- }
321
- },
322
- "hasDynamicHelp": false,
323
- "hiddenAliases": [],
324
- "id": "projects:list",
325
- "pluginAlias": "doomain",
326
- "pluginName": "doomain",
327
- "pluginType": "core",
328
- "strict": true,
329
- "enableJsonFlag": false,
330
- "isESM": true,
331
- "relativePath": [
332
- "dist",
333
- "commands",
334
- "projects",
335
- "list.js"
336
- ]
337
- },
338
303
  "providers:add": {
339
304
  "aliases": [],
340
305
  "args": {
@@ -595,6 +560,41 @@
595
560
  "verify.js"
596
561
  ]
597
562
  },
563
+ "projects:list": {
564
+ "aliases": [],
565
+ "args": {},
566
+ "description": "List Vercel projects.",
567
+ "flags": {
568
+ "json": {
569
+ "description": "Output a single JSON object and never prompt.",
570
+ "name": "json",
571
+ "allowNo": false,
572
+ "type": "boolean"
573
+ },
574
+ "search": {
575
+ "description": "Filter projects by search term.",
576
+ "name": "search",
577
+ "hasDynamicHelp": false,
578
+ "multiple": false,
579
+ "type": "option"
580
+ }
581
+ },
582
+ "hasDynamicHelp": false,
583
+ "hiddenAliases": [],
584
+ "id": "projects:list",
585
+ "pluginAlias": "doomain",
586
+ "pluginName": "doomain",
587
+ "pluginType": "core",
588
+ "strict": true,
589
+ "enableJsonFlag": false,
590
+ "isESM": true,
591
+ "relativePath": [
592
+ "dist",
593
+ "commands",
594
+ "projects",
595
+ "list.js"
596
+ ]
597
+ },
598
598
  "auth:logout:vercel": {
599
599
  "aliases": [],
600
600
  "args": {},
@@ -629,5 +629,5 @@
629
629
  ]
630
630
  }
631
631
  },
632
- "version": "0.1.2"
632
+ "version": "0.1.4"
633
633
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "doomain",
3
3
  "description": "Link your vercel project and domain in seconds",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
5
5
  "author": "Crafter Station",
6
6
  "packageManager": "bun@1.3.13",
7
7
  "bin": {