doomain 0.1.11 → 0.1.13
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/dist/commands/link.js +40 -0
- package/dist/commands/providers/connect.js +39 -12
- package/dist/commands/wizard.js +75 -5
- package/dist/lib/command-schema.js +5 -5
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/flags.js +1 -1
- package/dist/lib/link-domain.d.ts +13 -2
- package/dist/lib/link-domain.js +42 -6
- package/dist/lib/providers/core/config.d.ts +3 -1
- package/dist/lib/providers/core/config.js +24 -0
- package/oclif.manifest.json +7 -7
- package/package.json +1 -1
package/dist/commands/link.js
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
2
3
|
import { DoomainError } from '../lib/errors.js';
|
|
3
4
|
import { accountFlag, apexFlag, domainFlag, jsonFlag, projectFlag, providerFlag, subdomainFlag } from '../lib/flags.js';
|
|
4
5
|
import { linkDomain } from '../lib/link-domain.js';
|
|
5
6
|
import { createOutput, outputError } from '../lib/output.js';
|
|
7
|
+
function recordName(name) {
|
|
8
|
+
return name === '@' ? 'root' : name;
|
|
9
|
+
}
|
|
10
|
+
function recordLine(record) {
|
|
11
|
+
const details = [
|
|
12
|
+
record.priority === undefined ? undefined : `priority ${record.priority}`,
|
|
13
|
+
record.proxied === undefined ? undefined : `proxied ${record.proxied}`,
|
|
14
|
+
record.ttl === undefined ? undefined : `ttl ${record.ttl}`,
|
|
15
|
+
].filter(Boolean);
|
|
16
|
+
return `${record.type} ${recordName(record.name)} -> ${record.value}${details.length > 0 ? ` (${details.join(', ')})` : ''}`;
|
|
17
|
+
}
|
|
18
|
+
function dnsOverrideNote(warning) {
|
|
19
|
+
const account = warning.account === 'default' ? warning.providerName : `${warning.providerName}/${warning.account}`;
|
|
20
|
+
return [
|
|
21
|
+
`${warning.domain} already has DNS records in ${account} (${warning.zoneDomain}) that do not match the Vercel target.`,
|
|
22
|
+
'',
|
|
23
|
+
'Existing:',
|
|
24
|
+
...warning.conflicts.map((conflict) => `- ${recordLine(conflict.existing)}`),
|
|
25
|
+
'',
|
|
26
|
+
'Desired:',
|
|
27
|
+
...warning.desired.map((record) => `- ${recordLine(record)}`),
|
|
28
|
+
].join('\n');
|
|
29
|
+
}
|
|
6
30
|
export default class Link extends Command {
|
|
7
31
|
static description = 'Link a Vercel project to a domain and create DNS records.';
|
|
8
32
|
static examples = [
|
|
@@ -51,6 +75,22 @@ export default class Link extends Command {
|
|
|
51
75
|
const result = await linkDomain({
|
|
52
76
|
...flags,
|
|
53
77
|
domain,
|
|
78
|
+
confirmDnsOverride: out.json
|
|
79
|
+
? undefined
|
|
80
|
+
: async (warning) => {
|
|
81
|
+
spinner?.stop('Existing DNS target found');
|
|
82
|
+
p.note(dnsOverrideNote(warning), 'DNS already points elsewhere');
|
|
83
|
+
const confirmed = await p.confirm({
|
|
84
|
+
message: `Override existing DNS records for ${warning.domain}?`,
|
|
85
|
+
initialValue: false,
|
|
86
|
+
});
|
|
87
|
+
if (confirmed === true) {
|
|
88
|
+
spinner?.start('Continuing domain link');
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
spinner = undefined;
|
|
92
|
+
return false;
|
|
93
|
+
},
|
|
54
94
|
dryRun: false,
|
|
55
95
|
progress: out.json ? undefined : ({ message }) => spinner?.message(message),
|
|
56
96
|
timeoutSeconds: flags.timeout,
|
|
@@ -3,7 +3,7 @@ import * as p from '@clack/prompts';
|
|
|
3
3
|
import { getConfigPath, loadConfig, maskSecret, updateConfig } from '../../lib/config.js';
|
|
4
4
|
import { accountFlag, jsonFlag } from '../../lib/flags.js';
|
|
5
5
|
import { createOutput, outputError } from '../../lib/output.js';
|
|
6
|
-
import { isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount } from '../../lib/providers/core/config.js';
|
|
6
|
+
import { DEFAULT_PROVIDER_ACCOUNT, isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount, providerAccountHasCredentials, withProviderAccountCredentials, } from '../../lib/providers/core/config.js';
|
|
7
7
|
import { getProviderDefinition, listProviderDefinitions } from '../../lib/providers/registry.js';
|
|
8
8
|
function requireString(value, message) {
|
|
9
9
|
if (typeof value === 'string' && value.trim())
|
|
@@ -58,6 +58,34 @@ async function promptCredential(credential, detectedPublicIp) {
|
|
|
58
58
|
}
|
|
59
59
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
60
60
|
}
|
|
61
|
+
function validateProviderAccount(value) {
|
|
62
|
+
try {
|
|
63
|
+
normalizeProviderAccount(value);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return error instanceof Error ? error.message : String(error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function promptProviderAccount() {
|
|
71
|
+
const value = await p.text({ message: 'Profile name', placeholder: DEFAULT_PROVIDER_ACCOUNT, validate: validateProviderAccount });
|
|
72
|
+
if (p.isCancel(value)) {
|
|
73
|
+
p.cancel('Cancelled');
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return normalizeProviderAccount(value);
|
|
77
|
+
}
|
|
78
|
+
async function confirmProviderAccountOverwrite(definition, account) {
|
|
79
|
+
const value = await p.confirm({
|
|
80
|
+
initialValue: false,
|
|
81
|
+
message: `Profile "${account}" already exists for ${definition.displayName}. Overwrite it?`,
|
|
82
|
+
});
|
|
83
|
+
if (p.isCancel(value)) {
|
|
84
|
+
p.cancel('Cancelled');
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
61
89
|
async function promptProvider() {
|
|
62
90
|
const config = await loadConfig();
|
|
63
91
|
const selected = await p.select({
|
|
@@ -108,8 +136,16 @@ export default class ProvidersConnect extends Command {
|
|
|
108
136
|
const definition = args.provider ? getProviderDefinition(args.provider) : await promptProvider();
|
|
109
137
|
if (!definition)
|
|
110
138
|
return;
|
|
111
|
-
const account = normalizeProviderAccount(flags.account);
|
|
139
|
+
const account = flags.account ? normalizeProviderAccount(flags.account) : out.json ? DEFAULT_PROVIDER_ACCOUNT : await promptProviderAccount();
|
|
140
|
+
if (!account)
|
|
141
|
+
return;
|
|
112
142
|
const isDefaultAccount = isDefaultProviderAccount(account);
|
|
143
|
+
const currentConfig = await loadConfig();
|
|
144
|
+
if (!out.json && providerAccountHasCredentials(currentConfig, definition.id, account)) {
|
|
145
|
+
const overwrite = await confirmProviderAccountOverwrite(definition, account);
|
|
146
|
+
if (!overwrite)
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
113
149
|
const passedCredentials = parseCredentialFlags(flags.credential);
|
|
114
150
|
const credentials = {};
|
|
115
151
|
const detectedPublicIp = !out.json && usesClientIp(definition) ? await fetchPublicIp() : undefined;
|
|
@@ -148,7 +184,6 @@ export default class ProvidersConnect extends Command {
|
|
|
148
184
|
spinner = undefined;
|
|
149
185
|
}
|
|
150
186
|
let setDefault = true;
|
|
151
|
-
const currentConfig = await loadConfig();
|
|
152
187
|
if (!out.json && currentConfig.defaults?.provider && currentConfig.defaults.provider !== definition.id) {
|
|
153
188
|
const value = await p.confirm({
|
|
154
189
|
initialValue: true,
|
|
@@ -165,15 +200,7 @@ export default class ProvidersConnect extends Command {
|
|
|
165
200
|
defaults: setDefault ? { ...config.defaults, provider: definition.id } : config.defaults,
|
|
166
201
|
providers: {
|
|
167
202
|
...config.providers,
|
|
168
|
-
[definition.id]:
|
|
169
|
-
? { ...config.providers?.[definition.id], credentials }
|
|
170
|
-
: {
|
|
171
|
-
...config.providers?.[definition.id],
|
|
172
|
-
accounts: {
|
|
173
|
-
...config.providers?.[definition.id]?.accounts,
|
|
174
|
-
[account]: { credentials },
|
|
175
|
-
},
|
|
176
|
-
},
|
|
203
|
+
[definition.id]: withProviderAccountCredentials(config.providers?.[definition.id], account, credentials),
|
|
177
204
|
},
|
|
178
205
|
}));
|
|
179
206
|
out.result({
|
package/dist/commands/wizard.js
CHANGED
|
@@ -6,7 +6,7 @@ import { jsonFlag } from '../lib/flags.js';
|
|
|
6
6
|
import { createLinkPlan, linkDomain } from '../lib/link-domain.js';
|
|
7
7
|
import { detectLocalVercelProject } from '../lib/local-vercel.js';
|
|
8
8
|
import { createOutput, outputError } from '../lib/output.js';
|
|
9
|
-
import { DEFAULT_PROVIDER_ACCOUNT, listConfiguredProviderAccounts } from '../lib/providers/core/config.js';
|
|
9
|
+
import { DEFAULT_PROVIDER_ACCOUNT, isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount, providerAccountHasCredentials, withProviderAccountCredentials, } from '../lib/providers/core/config.js';
|
|
10
10
|
import { createProvider, getProviderDefinition, listProviderDefinitions } from '../lib/providers/registry.js';
|
|
11
11
|
import { listGlobalVercelTokens } from '../lib/vercel-auth.js';
|
|
12
12
|
import { createVercelClient } from '../lib/vercel.js';
|
|
@@ -26,6 +26,28 @@ async function promptRequired(message, opts = {}) {
|
|
|
26
26
|
const resolved = cancelIfNeeded(value);
|
|
27
27
|
return typeof resolved === 'string' ? resolved.trim() : null;
|
|
28
28
|
}
|
|
29
|
+
function validateProviderAccount(value) {
|
|
30
|
+
try {
|
|
31
|
+
normalizeProviderAccount(value);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return error instanceof Error ? error.message : String(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function promptProviderAccount() {
|
|
39
|
+
const value = await p.text({ message: 'Profile name', placeholder: DEFAULT_PROVIDER_ACCOUNT, validate: validateProviderAccount });
|
|
40
|
+
const resolved = cancelIfNeeded(value);
|
|
41
|
+
return typeof resolved === 'string' ? normalizeProviderAccount(resolved) : null;
|
|
42
|
+
}
|
|
43
|
+
async function confirmProviderAccountOverwrite(definition, account) {
|
|
44
|
+
const value = await p.confirm({
|
|
45
|
+
initialValue: false,
|
|
46
|
+
message: `Profile "${account}" already exists for ${definition.displayName}. Overwrite it?`,
|
|
47
|
+
});
|
|
48
|
+
const resolved = cancelIfNeeded(value);
|
|
49
|
+
return resolved === true;
|
|
50
|
+
}
|
|
29
51
|
async function fetchPublicIp() {
|
|
30
52
|
const controller = new AbortController();
|
|
31
53
|
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
@@ -135,9 +157,31 @@ async function promptVercelToken(globalTokens) {
|
|
|
135
157
|
}
|
|
136
158
|
return promptRequired('Vercel token', { password: true });
|
|
137
159
|
}
|
|
160
|
+
function recordName(name) {
|
|
161
|
+
return name === '@' ? 'root' : name;
|
|
162
|
+
}
|
|
163
|
+
function recordLine(record) {
|
|
164
|
+
const details = [
|
|
165
|
+
record.priority === undefined ? undefined : `priority ${record.priority}`,
|
|
166
|
+
record.proxied === undefined ? undefined : `proxied ${record.proxied}`,
|
|
167
|
+
record.ttl === undefined ? undefined : `ttl ${record.ttl}`,
|
|
168
|
+
].filter(Boolean);
|
|
169
|
+
return `${record.type} ${recordName(record.name)} -> ${record.value}${details.length > 0 ? ` (${details.join(', ')})` : ''}`;
|
|
170
|
+
}
|
|
138
171
|
function recordPreview(record, providerName) {
|
|
139
|
-
|
|
140
|
-
|
|
172
|
+
return `DNS: ${recordLine(record)} in ${providerName}`;
|
|
173
|
+
}
|
|
174
|
+
function dnsOverrideNote(warning) {
|
|
175
|
+
const account = warning.account === DEFAULT_PROVIDER_ACCOUNT ? warning.providerName : `${warning.providerName}/${warning.account}`;
|
|
176
|
+
return [
|
|
177
|
+
`${warning.domain} already has DNS records in ${account} (${warning.zoneDomain}) that do not match the Vercel target.`,
|
|
178
|
+
'',
|
|
179
|
+
'Existing:',
|
|
180
|
+
...warning.conflicts.map((conflict) => `- ${recordLine(conflict.existing)}`),
|
|
181
|
+
'',
|
|
182
|
+
'Desired:',
|
|
183
|
+
...warning.desired.map((record) => `- ${recordLine(record)}`),
|
|
184
|
+
].join('\n');
|
|
141
185
|
}
|
|
142
186
|
export default class Wizard extends Command {
|
|
143
187
|
static description = 'Interactive Vercel domain linker.';
|
|
@@ -251,6 +295,15 @@ export default class Wizard extends Command {
|
|
|
251
295
|
if (!selectedDefinition)
|
|
252
296
|
return;
|
|
253
297
|
showProviderSetup(selectedDefinition);
|
|
298
|
+
const account = await promptProviderAccount();
|
|
299
|
+
if (!account)
|
|
300
|
+
return;
|
|
301
|
+
if (providerAccountHasCredentials(config, selectedDefinition.id, account)) {
|
|
302
|
+
const overwrite = await confirmProviderAccountOverwrite(selectedDefinition, account);
|
|
303
|
+
if (!overwrite)
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const providerAccount = { account, isDefaultAccount: isDefaultProviderAccount(account), providerId: selectedDefinition.id };
|
|
254
307
|
const credentials = await promptProviderCredentials(selectedDefinition);
|
|
255
308
|
if (!credentials)
|
|
256
309
|
return;
|
|
@@ -261,11 +314,14 @@ export default class Wizard extends Command {
|
|
|
261
314
|
const zones = await provider.listZones();
|
|
262
315
|
domainSpinner.stop(`Connected ${selectedDefinition.displayName} and loaded ${zones.length} domain${zones.length === 1 ? '' : 's'}`);
|
|
263
316
|
activeSpinner = undefined;
|
|
264
|
-
domainOptions.push(...zones.map((zone) => toProviderDomainOption(selectedDefinition,
|
|
317
|
+
domainOptions.push(...zones.map((zone) => toProviderDomainOption(selectedDefinition, providerAccount, zone)));
|
|
265
318
|
await updateConfig((current) => ({
|
|
266
319
|
...current,
|
|
267
320
|
defaults: { ...current.defaults, provider: selectedDefinition.id },
|
|
268
|
-
providers: {
|
|
321
|
+
providers: {
|
|
322
|
+
...current.providers,
|
|
323
|
+
[selectedDefinition.id]: withProviderAccountCredentials(current.providers?.[selectedDefinition.id], account, credentials),
|
|
324
|
+
},
|
|
269
325
|
vercel: { token: vercelToken, teamId: vercelTeamId },
|
|
270
326
|
}));
|
|
271
327
|
}
|
|
@@ -355,6 +411,20 @@ export default class Wizard extends Command {
|
|
|
355
411
|
subdomain,
|
|
356
412
|
apex,
|
|
357
413
|
project,
|
|
414
|
+
confirmDnsOverride: async (warning) => {
|
|
415
|
+
spinner.stop('Existing DNS target found');
|
|
416
|
+
p.note(dnsOverrideNote(warning), 'DNS already points elsewhere');
|
|
417
|
+
const confirmed = await p.confirm({
|
|
418
|
+
message: `Override existing DNS records for ${warning.domain}?`,
|
|
419
|
+
initialValue: false,
|
|
420
|
+
});
|
|
421
|
+
if (confirmed === true) {
|
|
422
|
+
spinner.start('Continuing domain link');
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
activeSpinner = undefined;
|
|
426
|
+
return false;
|
|
427
|
+
},
|
|
358
428
|
progress: ({ message }) => spinner.message(message),
|
|
359
429
|
wait: true,
|
|
360
430
|
});
|
|
@@ -26,7 +26,7 @@ export const commandSchemas = [
|
|
|
26
26
|
flags: [
|
|
27
27
|
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
28
28
|
{ name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
|
|
29
|
-
{ name: 'account', type: 'string', description: 'DNS provider account alias. Defaults to the provider default account.' },
|
|
29
|
+
{ name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
|
|
30
30
|
{ name: 'domain', type: 'string', description: 'Target domain or base zone, for example app.example.com or example.com.' },
|
|
31
31
|
{ name: 'subdomain', type: 'string', description: 'Subdomain to add.' },
|
|
32
32
|
{ name: 'apex', type: 'boolean', description: 'Use the root/apex domain.' },
|
|
@@ -50,7 +50,7 @@ export const commandSchemas = [
|
|
|
50
50
|
],
|
|
51
51
|
flags: [
|
|
52
52
|
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
53
|
-
{ name: 'account', type: 'string', description: 'DNS provider account alias. Defaults to the provider default account.' },
|
|
53
|
+
{ name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
|
|
54
54
|
{ name: 'credential', type: 'string', description: 'Provider credential as key=value. Can be repeated.' },
|
|
55
55
|
{ name: 'api-key', type: 'string', description: 'Spaceship API key.' },
|
|
56
56
|
{ name: 'api-secret', type: 'string', description: 'Spaceship API secret.' },
|
|
@@ -63,7 +63,7 @@ export const commandSchemas = [
|
|
|
63
63
|
examples: ['doomain providers add', 'doomain providers add namecheap', 'doomain providers add spaceship --account work'],
|
|
64
64
|
flags: [
|
|
65
65
|
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
66
|
-
{ name: 'account', type: 'string', description: 'DNS provider account alias. Defaults to the provider default account.' },
|
|
66
|
+
{ name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
|
|
67
67
|
{ name: 'credential', type: 'string', description: 'Provider credential as key=value. Can be repeated.' },
|
|
68
68
|
{ name: 'no-verify', type: 'boolean', description: 'Save credentials without verifying them first.' },
|
|
69
69
|
],
|
|
@@ -88,7 +88,7 @@ export const commandSchemas = [
|
|
|
88
88
|
],
|
|
89
89
|
flags: [
|
|
90
90
|
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
91
|
-
{ name: 'account', type: 'string', description: 'DNS provider account alias. Omit to remove all accounts for the provider.' },
|
|
91
|
+
{ name: 'account', type: 'string', description: 'DNS provider profile/account alias. Omit to remove all accounts for the provider.' },
|
|
92
92
|
],
|
|
93
93
|
},
|
|
94
94
|
{
|
|
@@ -102,7 +102,7 @@ export const commandSchemas = [
|
|
|
102
102
|
],
|
|
103
103
|
flags: [
|
|
104
104
|
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
105
|
-
{ name: 'account', type: 'string', description: 'DNS provider account alias. Defaults to the provider default account.' },
|
|
105
|
+
{ name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
|
|
106
106
|
],
|
|
107
107
|
},
|
|
108
108
|
{
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'VERCEL_AUTH_FAILED' | 'VERCEL_PROJECT_NOT_LINKED';
|
|
1
|
+
export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'DNS_TARGET_CONFLICT' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_ALREADY_ASSIGNED' | 'DOMAIN_VERIFY_FAILED' | 'INVALID_INPUT' | 'MISSING_ARGUMENT' | 'MISSING_CREDENTIALS' | 'PROVIDER_API_ERROR' | 'PROVIDER_AUTH_FAILED' | 'PROVIDER_PERMISSION_DENIED' | 'PROVIDER_NOT_FOUND' | 'PROVIDER_RATE_LIMITED' | 'PROVIDER_RECORD_CONFLICT' | 'PROVIDER_UNSUPPORTED_RECORD' | 'PROVIDER_ZONE_AMBIGUOUS' | 'PROVIDER_ZONE_NOT_FOUND' | 'PROJECT_NOT_FOUND' | 'VERCEL_AUTH_FAILED' | 'VERCEL_PROJECT_NOT_LINKED';
|
|
2
2
|
export declare class DoomainError extends Error {
|
|
3
3
|
readonly code: DoomainErrorCode;
|
|
4
4
|
readonly details?: unknown;
|
package/dist/lib/flags.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
export const jsonFlag = Flags.boolean({ description: 'Output a single JSON object and never prompt.' });
|
|
3
3
|
export const providerFlag = Flags.string({ description: 'DNS provider id. Inferred from the target domain when omitted.' });
|
|
4
|
-
export const accountFlag = Flags.string({ description: 'DNS provider account alias. Defaults to the provider default account.' });
|
|
4
|
+
export const accountFlag = Flags.string({ description: 'DNS provider profile/account alias. Defaults to the provider default account.' });
|
|
5
5
|
export const domainFlag = Flags.string({ description: 'Target domain or base zone, for example app.example.com or example.com.' });
|
|
6
6
|
export const subdomainFlag = Flags.string({ description: 'Subdomain to add, for example app for app.example.com.' });
|
|
7
7
|
export const apexFlag = Flags.boolean({ description: 'Use the root/apex domain instead of a subdomain.' });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DnsRecordInput } from './providers/types.js';
|
|
1
|
+
import type { DnsConflict, DnsRecordInput } from './providers/types.js';
|
|
2
2
|
export interface LinkDomainInput {
|
|
3
3
|
provider?: string;
|
|
4
4
|
account?: string;
|
|
@@ -11,14 +11,25 @@ export interface LinkDomainInput {
|
|
|
11
11
|
wait?: boolean;
|
|
12
12
|
timeoutSeconds?: number;
|
|
13
13
|
progress?: LinkDomainProgressCallback;
|
|
14
|
+
confirmDnsOverride?: (warning: DnsOverrideWarning) => Promise<boolean>;
|
|
14
15
|
}
|
|
15
16
|
export type LinkDomainProjectSource = 'config' | 'env' | 'flag' | 'packageJson' | 'vercelProjectFile';
|
|
16
|
-
export type LinkDomainProgressStage = 'dns:apply' | 'dns:plan' | 'dns:resolve-zone' | 'dns:wait' | 'vercel:add-domain' | 'vercel:get-domain' | 'vercel:get-target' | 'vercel:verify';
|
|
17
|
+
export type LinkDomainProgressStage = 'dns:apply' | 'dns:inspect' | 'dns:override-confirm' | 'dns:plan' | 'dns:resolve-zone' | 'dns:wait' | 'vercel:add-domain' | 'vercel:get-domain' | 'vercel:get-target' | 'vercel:verify';
|
|
17
18
|
export interface LinkDomainProgress {
|
|
18
19
|
message: string;
|
|
19
20
|
stage: LinkDomainProgressStage;
|
|
20
21
|
}
|
|
21
22
|
export type LinkDomainProgressCallback = (progress: LinkDomainProgress) => void;
|
|
23
|
+
export interface DnsOverrideWarning {
|
|
24
|
+
account: string;
|
|
25
|
+
conflicts: DnsConflict[];
|
|
26
|
+
desired: DnsRecordInput[];
|
|
27
|
+
domain: string;
|
|
28
|
+
provider: string;
|
|
29
|
+
providerName: string;
|
|
30
|
+
recordName: string;
|
|
31
|
+
zoneDomain: string;
|
|
32
|
+
}
|
|
22
33
|
export interface LinkDomainPlan {
|
|
23
34
|
provider: string;
|
|
24
35
|
providerInferred: boolean;
|
package/dist/lib/link-domain.js
CHANGED
|
@@ -459,6 +459,41 @@ async function waitForVercelDomainReady(opts) {
|
|
|
459
459
|
function reportProgress(input, stage, message) {
|
|
460
460
|
input.progress?.({ message, stage });
|
|
461
461
|
}
|
|
462
|
+
function dnsTargetConflictError(warning) {
|
|
463
|
+
return new DoomainError('DNS_TARGET_CONFLICT', `${warning.domain} already has DNS records that point somewhere else. Re-run with --force to overwrite them.`, {
|
|
464
|
+
account: warning.account,
|
|
465
|
+
conflicts: warning.conflicts,
|
|
466
|
+
desired: warning.desired,
|
|
467
|
+
domain: warning.domain,
|
|
468
|
+
provider: warning.provider,
|
|
469
|
+
providerName: warning.providerName,
|
|
470
|
+
recovery: 'Confirm the DNS override in interactive mode, or re-run with --force to overwrite conflicting DNS records.',
|
|
471
|
+
recordName: warning.recordName,
|
|
472
|
+
suggestedCommands: [`doomain link ${warning.domain} --project <project> --force --json`],
|
|
473
|
+
zoneDomain: warning.zoneDomain,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
async function resolveDnsForce(input, opts) {
|
|
477
|
+
reportProgress(input, 'dns:inspect', `Checking existing DNS records in ${opts.provider.name}`);
|
|
478
|
+
const dnsPlan = await opts.provider.planChanges(opts.zone, [opts.baseRecord], { force: input.force });
|
|
479
|
+
if (input.force || dnsPlan.conflicts.length === 0)
|
|
480
|
+
return Boolean(input.force);
|
|
481
|
+
const warning = {
|
|
482
|
+
account: opts.plan.account,
|
|
483
|
+
conflicts: dnsPlan.conflicts,
|
|
484
|
+
desired: [opts.baseRecord],
|
|
485
|
+
domain: opts.plan.domain,
|
|
486
|
+
provider: opts.plan.provider,
|
|
487
|
+
providerName: opts.provider.name,
|
|
488
|
+
recordName: opts.plan.recordName,
|
|
489
|
+
zoneDomain: opts.plan.zoneDomain,
|
|
490
|
+
};
|
|
491
|
+
reportProgress(input, 'dns:override-confirm', `Existing DNS records point ${opts.plan.domain} somewhere else`);
|
|
492
|
+
const confirmed = await input.confirmDnsOverride?.(warning);
|
|
493
|
+
if (!confirmed)
|
|
494
|
+
throw dnsTargetConflictError(warning);
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
462
497
|
export async function createLinkPlan(input) {
|
|
463
498
|
const project = await resolveProject(input.project);
|
|
464
499
|
const resolved = await resolveProviderTarget(input);
|
|
@@ -495,22 +530,23 @@ export async function linkDomain(input) {
|
|
|
495
530
|
const provider = await createProvider(plan.provider, { account: plan.account });
|
|
496
531
|
reportProgress(input, 'dns:resolve-zone', `Finding ${provider.name} DNS zone`);
|
|
497
532
|
const zone = await resolveZone(provider, plan.zoneDomain);
|
|
498
|
-
reportProgress(input, 'vercel:add-domain', 'Adding domain to Vercel');
|
|
499
|
-
const addResult = await vercel.addDomainToProject(plan.project, plan.domain, { force: input.force });
|
|
500
533
|
reportProgress(input, 'vercel:get-target', 'Reading Vercel DNS target');
|
|
501
534
|
const cname = plan.isApex ? undefined : await vercel.getRecommendedCname(plan.domain);
|
|
535
|
+
const baseRecord = planBaseRecord({ isApex: plan.isApex, provider: plan.provider, recordName: plan.recordName, cname });
|
|
536
|
+
const forceDns = await resolveDnsForce(input, { baseRecord, plan, provider, zone });
|
|
537
|
+
reportProgress(input, 'vercel:add-domain', 'Adding domain to Vercel');
|
|
538
|
+
const addResult = await vercel.addDomainToProject(plan.project, plan.domain, { force: input.force });
|
|
502
539
|
reportProgress(input, 'vercel:get-domain', 'Reading Vercel verification records');
|
|
503
540
|
const projectDomain = await vercel.getProjectDomain(plan.project, plan.domain);
|
|
504
|
-
const baseRecord = planBaseRecord({ isApex: plan.isApex, provider: plan.provider, recordName: plan.recordName, cname });
|
|
505
541
|
const verificationDnsRecords = uniqueRecords([
|
|
506
542
|
...planVerificationRecords(plan.provider, addResult.raw, plan.zoneDomain),
|
|
507
543
|
...planVerificationRecords(plan.provider, projectDomain, plan.zoneDomain),
|
|
508
544
|
]);
|
|
509
545
|
const records = [baseRecord, ...verificationDnsRecords];
|
|
510
546
|
reportProgress(input, 'dns:plan', `Reading ${provider.name} DNS records`);
|
|
511
|
-
const dnsPlan = await provider.planChanges(zone, records, { force:
|
|
547
|
+
const dnsPlan = await provider.planChanges(zone, records, { force: forceDns });
|
|
512
548
|
reportProgress(input, 'dns:apply', `Updating DNS records in ${provider.name}`);
|
|
513
|
-
const dnsResult = await provider.applyChanges(zone, dnsPlan, { force:
|
|
549
|
+
const dnsResult = await provider.applyChanges(zone, dnsPlan, { force: forceDns });
|
|
514
550
|
const shouldWait = input.wait ?? true;
|
|
515
551
|
if (shouldWait) {
|
|
516
552
|
reportProgress(input, 'dns:wait', verificationDnsRecords.length > 0 ? 'DNS records saved; asking Vercel to verify ownership' : 'DNS records saved; asking Vercel to verify');
|
|
@@ -518,7 +554,7 @@ export async function linkDomain(input) {
|
|
|
518
554
|
const waitResult = shouldWait
|
|
519
555
|
? await waitForVercelDomainReady({
|
|
520
556
|
domain: plan.domain,
|
|
521
|
-
force:
|
|
557
|
+
force: forceDns,
|
|
522
558
|
input,
|
|
523
559
|
project: plan.project,
|
|
524
560
|
provider,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DoomainConfig } from '../../config.js';
|
|
1
|
+
import { type DoomainConfig, type ProviderConfig } from '../../config.js';
|
|
2
2
|
import type { CredentialDefinition, DnsProviderDefinition, ProviderContext } from './types.js';
|
|
3
3
|
export declare const DEFAULT_PROVIDER_ACCOUNT = "default";
|
|
4
4
|
export interface ProviderAccountRef {
|
|
@@ -13,6 +13,8 @@ export declare function normalizeProviderAccount(account?: string): string;
|
|
|
13
13
|
export declare function isDefaultProviderAccount(account?: string): boolean;
|
|
14
14
|
export declare function getProviderCredentials(config: DoomainConfig, providerId: string, opts?: ProviderAccountOptions): Record<string, string>;
|
|
15
15
|
export declare function getProviderCredential(config: DoomainConfig, providerId: string, credential: CredentialDefinition, opts?: ProviderAccountOptions): string | undefined;
|
|
16
|
+
export declare function providerAccountHasCredentials(config: DoomainConfig, providerId: string, accountInput?: string): boolean;
|
|
17
|
+
export declare function withProviderAccountCredentials(current: ProviderConfig | undefined, accountInput: string, credentials: Record<string, string>): ProviderConfig;
|
|
16
18
|
export declare function isProviderAccountConfigured(definition: DnsProviderDefinition, config: DoomainConfig, opts?: ProviderAccountOptions): boolean;
|
|
17
19
|
export declare function listConfiguredProviderAccounts(config: DoomainConfig, definition: DnsProviderDefinition): ProviderAccountRef[];
|
|
18
20
|
export declare function createProviderContext(definition: DnsProviderDefinition, opts?: ProviderAccountOptions): Promise<ProviderContext>;
|
|
@@ -30,6 +30,9 @@ function credentialFromSavedConfig(config, providerId, account, key) {
|
|
|
30
30
|
return legacyCredential(config, providerId, key);
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
33
|
+
function hasCredentials(credentials) {
|
|
34
|
+
return credentials !== undefined && Object.keys(credentials).length > 0;
|
|
35
|
+
}
|
|
33
36
|
export function getProviderCredentials(config, providerId, opts = {}) {
|
|
34
37
|
const account = normalizeProviderAccount(opts.account);
|
|
35
38
|
const current = providerConfig(config, providerId);
|
|
@@ -40,6 +43,27 @@ export function getProviderCredential(config, providerId, credential, opts = {})
|
|
|
40
43
|
const account = normalizeProviderAccount(opts.account);
|
|
41
44
|
return process.env[credential.env] || credentialFromSavedConfig(config, providerId, account, credential.key);
|
|
42
45
|
}
|
|
46
|
+
export function providerAccountHasCredentials(config, providerId, accountInput) {
|
|
47
|
+
const account = normalizeProviderAccount(accountInput);
|
|
48
|
+
const current = providerConfig(config, providerId);
|
|
49
|
+
if (!current)
|
|
50
|
+
return false;
|
|
51
|
+
if (account !== DEFAULT_PROVIDER_ACCOUNT)
|
|
52
|
+
return hasCredentials(current.accounts?.[account]?.credentials);
|
|
53
|
+
return hasCredentials(current.credentials) || Boolean(legacyCredential(config, providerId, 'apiKey') || legacyCredential(config, providerId, 'apiSecret'));
|
|
54
|
+
}
|
|
55
|
+
export function withProviderAccountCredentials(current, accountInput, credentials) {
|
|
56
|
+
const account = normalizeProviderAccount(accountInput);
|
|
57
|
+
if (account === DEFAULT_PROVIDER_ACCOUNT)
|
|
58
|
+
return { ...current, credentials };
|
|
59
|
+
return {
|
|
60
|
+
...current,
|
|
61
|
+
accounts: {
|
|
62
|
+
...current?.accounts,
|
|
63
|
+
[account]: { credentials },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
43
67
|
export function isProviderAccountConfigured(definition, config, opts = {}) {
|
|
44
68
|
return definition.credentials.every((credential) => credential.required === false || Boolean(getProviderCredential(config, definition.id, credential, opts)));
|
|
45
69
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"flags": {
|
|
22
22
|
"account": {
|
|
23
|
-
"description": "DNS provider account alias. Defaults to the provider default account.",
|
|
23
|
+
"description": "DNS provider profile/account alias. Defaults to the provider default account.",
|
|
24
24
|
"name": "account",
|
|
25
25
|
"hasDynamicHelp": false,
|
|
26
26
|
"multiple": false,
|
|
@@ -273,7 +273,7 @@
|
|
|
273
273
|
"description": "List DNS zones and records for a provider.",
|
|
274
274
|
"flags": {
|
|
275
275
|
"account": {
|
|
276
|
-
"description": "DNS provider account alias. Defaults to the provider default account.",
|
|
276
|
+
"description": "DNS provider profile/account alias. Defaults to the provider default account.",
|
|
277
277
|
"name": "account",
|
|
278
278
|
"hasDynamicHelp": false,
|
|
279
279
|
"multiple": false,
|
|
@@ -363,7 +363,7 @@
|
|
|
363
363
|
"description": "Save DNS provider credentials locally.",
|
|
364
364
|
"flags": {
|
|
365
365
|
"account": {
|
|
366
|
-
"description": "DNS provider account alias. Defaults to the provider default account.",
|
|
366
|
+
"description": "DNS provider profile/account alias. Defaults to the provider default account.",
|
|
367
367
|
"name": "account",
|
|
368
368
|
"hasDynamicHelp": false,
|
|
369
369
|
"multiple": false,
|
|
@@ -432,7 +432,7 @@
|
|
|
432
432
|
"description": "Save DNS provider credentials locally.",
|
|
433
433
|
"flags": {
|
|
434
434
|
"account": {
|
|
435
|
-
"description": "DNS provider account alias. Defaults to the provider default account.",
|
|
435
|
+
"description": "DNS provider profile/account alias. Defaults to the provider default account.",
|
|
436
436
|
"name": "account",
|
|
437
437
|
"hasDynamicHelp": false,
|
|
438
438
|
"multiple": false,
|
|
@@ -507,7 +507,7 @@
|
|
|
507
507
|
],
|
|
508
508
|
"flags": {
|
|
509
509
|
"account": {
|
|
510
|
-
"description": "DNS provider account alias. Defaults to the provider default account.",
|
|
510
|
+
"description": "DNS provider profile/account alias. Defaults to the provider default account.",
|
|
511
511
|
"name": "account",
|
|
512
512
|
"hasDynamicHelp": false,
|
|
513
513
|
"multiple": false,
|
|
@@ -610,7 +610,7 @@
|
|
|
610
610
|
"description": "Verify saved DNS provider credentials.",
|
|
611
611
|
"flags": {
|
|
612
612
|
"account": {
|
|
613
|
-
"description": "DNS provider account alias. Defaults to the provider default account.",
|
|
613
|
+
"description": "DNS provider profile/account alias. Defaults to the provider default account.",
|
|
614
614
|
"name": "account",
|
|
615
615
|
"hasDynamicHelp": false,
|
|
616
616
|
"multiple": false,
|
|
@@ -673,5 +673,5 @@
|
|
|
673
673
|
]
|
|
674
674
|
}
|
|
675
675
|
},
|
|
676
|
-
"version": "0.1.
|
|
676
|
+
"version": "0.1.13"
|
|
677
677
|
}
|