doomain 0.1.15 → 0.1.17
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 +91 -2
- package/dist/commands/auth/clerk.d.ts +11 -0
- package/dist/commands/auth/clerk.js +55 -0
- package/dist/commands/auth/logout/clerk.d.ts +8 -0
- package/dist/commands/auth/logout/clerk.js +29 -0
- package/dist/commands/clerk/domains/add.d.ts +19 -0
- package/dist/commands/clerk/domains/add.js +92 -0
- package/dist/commands/domains/find.d.ts +15 -0
- package/dist/commands/domains/find.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/clerk-domain.d.ts +39 -0
- package/dist/lib/clerk-domain.js +163 -0
- package/dist/lib/clerk.d.ts +56 -0
- package/dist/lib/clerk.js +65 -0
- package/dist/lib/command-schema.js +64 -2
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/domain-provider.d.ts +53 -0
- package/dist/lib/domain-provider.js +230 -0
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/link-domain.d.ts +1 -0
- package/dist/lib/link-domain.js +3 -177
- package/oclif.manifest.json +222 -1
- package/package.json +7 -3
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface ClerkPlatformConfig {
|
|
2
|
+
appId: string;
|
|
3
|
+
platformApiKey: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ClerkApplication {
|
|
6
|
+
application_id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
instances: Array<{
|
|
9
|
+
environment_type: string;
|
|
10
|
+
instance_id: string;
|
|
11
|
+
publishable_key: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export interface ClerkCnameTarget {
|
|
15
|
+
host: string;
|
|
16
|
+
required: boolean;
|
|
17
|
+
value: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ClerkApplicationDomain {
|
|
20
|
+
cname_targets?: ClerkCnameTarget[];
|
|
21
|
+
frontend_api_url: string;
|
|
22
|
+
id: string;
|
|
23
|
+
is_satellite: boolean;
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ClerkProductionInstance {
|
|
27
|
+
active_domain: ClerkApplicationDomain | null;
|
|
28
|
+
environment_type: 'production';
|
|
29
|
+
id: string;
|
|
30
|
+
publishable_key: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ClerkDomainStatus {
|
|
33
|
+
dns?: {
|
|
34
|
+
required?: boolean;
|
|
35
|
+
status: string;
|
|
36
|
+
};
|
|
37
|
+
mail?: {
|
|
38
|
+
required?: boolean;
|
|
39
|
+
status: string;
|
|
40
|
+
};
|
|
41
|
+
ssl?: {
|
|
42
|
+
required?: boolean;
|
|
43
|
+
status: string;
|
|
44
|
+
};
|
|
45
|
+
status: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function resolveClerkPlatformConfig(appId?: string): Promise<ClerkPlatformConfig>;
|
|
48
|
+
export declare function createClerkPlatformClient(config: {
|
|
49
|
+
platformApiKey: string;
|
|
50
|
+
}): {
|
|
51
|
+
fetchApplication(appId: string): Promise<ClerkApplication>;
|
|
52
|
+
createProductionInstance(appId: string, domain: string, developmentInstanceId: string): Promise<ClerkProductionInstance>;
|
|
53
|
+
getDomainStatus(appId: string, domainId: string): Promise<ClerkDomainStatus>;
|
|
54
|
+
triggerDomainDnsCheck(appId: string, domainId: string): Promise<ClerkDomainStatus>;
|
|
55
|
+
};
|
|
56
|
+
export type ClerkPlatformClient = ReturnType<typeof createClerkPlatformClient>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { loadConfig } from './config.js';
|
|
2
|
+
import { DoomainError } from './errors.js';
|
|
3
|
+
const CLERK_API_URL = 'https://api.clerk.com';
|
|
4
|
+
export async function resolveClerkPlatformConfig(appId) {
|
|
5
|
+
const config = await loadConfig();
|
|
6
|
+
const platformApiKey = process.env.CLERK_PLATFORM_API_KEY || config.clerk?.platformApiKey;
|
|
7
|
+
const resolvedAppId = appId || process.env.CLERK_APPLICATION_ID || config.clerk?.appId;
|
|
8
|
+
if (!platformApiKey) {
|
|
9
|
+
throw new DoomainError('MISSING_CREDENTIALS', 'Missing Clerk Platform API key. Run `doomain auth clerk`, set CLERK_PLATFORM_API_KEY, or pass saved credentials.');
|
|
10
|
+
}
|
|
11
|
+
if (!platformApiKey.startsWith('ak_')) {
|
|
12
|
+
throw new DoomainError('INVALID_INPUT', 'Clerk Platform API keys must start with ak_.');
|
|
13
|
+
}
|
|
14
|
+
if (!resolvedAppId) {
|
|
15
|
+
throw new DoomainError('MISSING_ARGUMENT', 'Clerk application is required. Pass --app, set CLERK_APPLICATION_ID, or save it with `doomain auth clerk`.');
|
|
16
|
+
}
|
|
17
|
+
return { appId: resolvedAppId, platformApiKey };
|
|
18
|
+
}
|
|
19
|
+
function apiErrorMessage(status, body) {
|
|
20
|
+
return body?.errors?.[0]?.long_message ?? body?.errors?.[0]?.message ?? body?.error?.message ?? body?.message ?? `Clerk API error (${status}).`;
|
|
21
|
+
}
|
|
22
|
+
function apiErrorCode(body) {
|
|
23
|
+
return body?.errors?.[0]?.code ?? body?.error?.code ?? body?.code;
|
|
24
|
+
}
|
|
25
|
+
export function createClerkPlatformClient(config) {
|
|
26
|
+
async function request(path, init = {}) {
|
|
27
|
+
const response = await fetch(`${CLERK_API_URL}${path}`, {
|
|
28
|
+
...init,
|
|
29
|
+
headers: {
|
|
30
|
+
Accept: 'application/json',
|
|
31
|
+
Authorization: `Bearer ${config.platformApiKey}`,
|
|
32
|
+
...(init.body ? { 'Content-Type': 'application/json' } : {}),
|
|
33
|
+
...(init.headers ?? {}),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const body = (await response.json().catch(() => undefined));
|
|
38
|
+
if (response.status === 401 || response.status === 403) {
|
|
39
|
+
throw new DoomainError('CLERK_AUTH_FAILED', `Clerk Platform API authorization failed. Check CLERK_PLATFORM_API_KEY and its application access. ${apiErrorMessage(response.status, body)}`, body);
|
|
40
|
+
}
|
|
41
|
+
if (response.status === 409 && apiErrorCode(body) === 'production_instance_exists') {
|
|
42
|
+
throw new DoomainError('CLERK_PRODUCTION_EXISTS', 'This Clerk application already has a production instance. Configure domain changes manually in Clerk; Doomain will not modify it.', body);
|
|
43
|
+
}
|
|
44
|
+
throw new DoomainError('DOMAIN_LINK_FAILED', apiErrorMessage(response.status, body), body);
|
|
45
|
+
}
|
|
46
|
+
return (await response.json());
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
fetchApplication(appId) {
|
|
50
|
+
return request(`/v1/platform/applications/${encodeURIComponent(appId)}`);
|
|
51
|
+
},
|
|
52
|
+
createProductionInstance(appId, domain, developmentInstanceId) {
|
|
53
|
+
return request(`/v1/platform/applications/${encodeURIComponent(appId)}/instances`, {
|
|
54
|
+
body: JSON.stringify({ clone_instance_id: developmentInstanceId, domain, environment_type: 'production' }),
|
|
55
|
+
method: 'POST',
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
getDomainStatus(appId, domainId) {
|
|
59
|
+
return request(`/v1/platform/applications/${encodeURIComponent(appId)}/domains/${encodeURIComponent(domainId)}/status`);
|
|
60
|
+
},
|
|
61
|
+
triggerDomainDnsCheck(appId, domainId) {
|
|
62
|
+
return request(`/v1/platform/applications/${encodeURIComponent(appId)}/domains/${encodeURIComponent(domainId)}/dns_check`, { method: 'POST' });
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -14,7 +14,7 @@ export const commandSchemas = [
|
|
|
14
14
|
],
|
|
15
15
|
agentHint: 'For agent use, try `doomain link <domain> --json` first. Do not inspect project files, run provider status, or use --dry-run unless the user explicitly asks for a preview. The command infers project/provider and returns structured recovery errors when inference fails.',
|
|
16
16
|
agentInstructions: [
|
|
17
|
-
'When the user asks to add or link a domain, run `doomain link <domain> --json` first.',
|
|
17
|
+
'When the user asks to add or link a Vercel domain, run `doomain link <domain> --json` first.',
|
|
18
18
|
'Do not run `providers status`, `projects list`, `--help`, or `--dry-run` before the first link attempt unless the user asks for a preview or diagnosis.',
|
|
19
19
|
'Only run follow-up commands when the JSON error includes a recovery action that requires them.',
|
|
20
20
|
],
|
|
@@ -46,6 +46,33 @@ export const commandSchemas = [
|
|
|
46
46
|
{ name: 'timeout', type: 'integer', description: 'Wait timeout in seconds.', default: 300 },
|
|
47
47
|
],
|
|
48
48
|
},
|
|
49
|
+
{
|
|
50
|
+
name: 'clerk domains add',
|
|
51
|
+
description: 'Create the first Clerk production instance with its primary domain and configure returned DNS records.',
|
|
52
|
+
examples: [
|
|
53
|
+
'doomain clerk domains add example.com --app app_123 --json',
|
|
54
|
+
'doomain clerk domains add example.com --app app_123 --provider cloudflare --no-wait --json',
|
|
55
|
+
'doomain clerk domains add example.com --app app_123 --dry-run --json',
|
|
56
|
+
],
|
|
57
|
+
agentHint: 'Use only for first-time Clerk production setup. The command aborts with CLERK_PRODUCTION_EXISTS when production already exists; domain changes must then be completed manually in Clerk.',
|
|
58
|
+
agentInstructions: [
|
|
59
|
+
'When the user asks to configure a Clerk production domain for the first time, run `doomain clerk domains add <domain> --app <app_id> --json`.',
|
|
60
|
+
'Never use this command to migrate or replace an existing Clerk production domain.',
|
|
61
|
+
'After success, follow the returned nextSteps to pull production keys, finish OAuth setup, and verify provisioning with Clerk CLI.',
|
|
62
|
+
],
|
|
63
|
+
mutates: true,
|
|
64
|
+
safeForAgents: true,
|
|
65
|
+
flags: [
|
|
66
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
67
|
+
{ name: 'app', type: 'string', description: 'Clerk application id. Defaults to CLERK_APPLICATION_ID or saved Clerk config.' },
|
|
68
|
+
{ name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
|
|
69
|
+
{ name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
|
|
70
|
+
{ name: 'dry-run', type: 'boolean', description: 'Check application eligibility and DNS zone without creating production.' },
|
|
71
|
+
{ name: 'force', type: 'boolean', description: 'Overwrite DNS records that conflict with Clerk requirements.' },
|
|
72
|
+
{ name: 'wait', type: 'boolean', description: 'Wait for Clerk DNS, SSL, and email DNS verification. Use --no-wait to skip waiting.', default: true },
|
|
73
|
+
{ name: 'timeout', type: 'integer', description: 'Wait timeout in seconds.', default: 300 },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
49
76
|
{
|
|
50
77
|
name: 'schema',
|
|
51
78
|
description: 'Print machine-readable command schemas for agents.',
|
|
@@ -132,6 +159,24 @@ export const commandSchemas = [
|
|
|
132
159
|
{ name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
|
|
133
160
|
],
|
|
134
161
|
},
|
|
162
|
+
{
|
|
163
|
+
name: 'auth clerk',
|
|
164
|
+
description: 'Save and verify Clerk Platform API credentials locally.',
|
|
165
|
+
examples: ['doomain auth clerk --platform-api-key ak_123 --app app_123 --json', 'doomain auth clerk'],
|
|
166
|
+
flags: [
|
|
167
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
168
|
+
{ name: 'platform-api-key', type: 'string', description: 'Clerk Platform API key (ak_...).' },
|
|
169
|
+
{ name: 'app', type: 'string', description: 'Default Clerk application id.' },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'auth logout clerk',
|
|
174
|
+
description: 'Remove saved Clerk credentials locally.',
|
|
175
|
+
examples: ['doomain auth logout clerk --json'],
|
|
176
|
+
flags: [
|
|
177
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
178
|
+
],
|
|
179
|
+
},
|
|
135
180
|
{
|
|
136
181
|
name: 'auth vercel',
|
|
137
182
|
description: 'Save Vercel credentials locally.',
|
|
@@ -150,6 +195,23 @@ export const commandSchemas = [
|
|
|
150
195
|
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
151
196
|
],
|
|
152
197
|
},
|
|
198
|
+
{
|
|
199
|
+
name: 'domains find',
|
|
200
|
+
description: 'Find the configured DNS provider and account for a domain.',
|
|
201
|
+
examples: [
|
|
202
|
+
'doomain domains find hacktheandes.com --json',
|
|
203
|
+
'doomain domains find api.example.com --json',
|
|
204
|
+
'doomain domains find --domain example.com --provider spaceship --account work --json',
|
|
205
|
+
],
|
|
206
|
+
agentHint: 'Use this command when you need to identify who manages DNS for a domain. It checks all configured provider accounts, tolerates individual provider failures, and selects the longest matching DNS zone. Inspect complete and warnings before treating the result as exhaustive.',
|
|
207
|
+
safeForAgents: true,
|
|
208
|
+
flags: [
|
|
209
|
+
{ name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
|
|
210
|
+
{ name: 'domain', type: 'string', description: 'Domain to find. May also be passed as the positional argument.' },
|
|
211
|
+
{ name: 'provider', type: 'string', description: 'Limit discovery to one DNS provider id.' },
|
|
212
|
+
{ name: 'account', type: 'string', description: 'Limit discovery to one DNS provider profile/account alias.' },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
153
215
|
{
|
|
154
216
|
name: 'domains list',
|
|
155
217
|
description: 'List DNS zones and records for a provider.',
|
|
@@ -204,7 +266,7 @@ async function configuredProviders() {
|
|
|
204
266
|
}));
|
|
205
267
|
}
|
|
206
268
|
function withProviderConnections(schema, providers) {
|
|
207
|
-
if (schema.name !== 'link')
|
|
269
|
+
if (schema.name !== 'link' && schema.name !== 'clerk domains add')
|
|
208
270
|
return schema;
|
|
209
271
|
return { ...schema, configuredProviders: providers };
|
|
210
272
|
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export interface VercelConfig {
|
|
|
6
6
|
token?: string;
|
|
7
7
|
teamId?: string;
|
|
8
8
|
}
|
|
9
|
+
export interface ClerkConfig {
|
|
10
|
+
appId?: string;
|
|
11
|
+
platformApiKey?: string;
|
|
12
|
+
}
|
|
9
13
|
export interface ProviderAccountConfig {
|
|
10
14
|
credentials?: Record<string, string>;
|
|
11
15
|
settings?: Record<string, unknown>;
|
|
@@ -19,6 +23,7 @@ export interface SpaceshipProviderConfig extends ProviderConfig {
|
|
|
19
23
|
domains?: string[];
|
|
20
24
|
}
|
|
21
25
|
export interface DoomainConfig {
|
|
26
|
+
clerk?: ClerkConfig;
|
|
22
27
|
vercel?: VercelConfig;
|
|
23
28
|
providers?: {
|
|
24
29
|
spaceship?: SpaceshipProviderConfig;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type DoomainErrorCode } from './errors.js';
|
|
2
|
+
export interface FindDomainProviderInput {
|
|
3
|
+
account?: string;
|
|
4
|
+
domain: string;
|
|
5
|
+
provider?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ProviderSearchWarning {
|
|
8
|
+
account: string;
|
|
9
|
+
error: {
|
|
10
|
+
code?: DoomainErrorCode;
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
isDefaultAccount: boolean;
|
|
14
|
+
provider: string;
|
|
15
|
+
providerName: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DomainProviderResult {
|
|
18
|
+
account: string;
|
|
19
|
+
accountInferred: boolean;
|
|
20
|
+
complete: boolean;
|
|
21
|
+
domain: string;
|
|
22
|
+
isApex: boolean;
|
|
23
|
+
isDefaultAccount: boolean;
|
|
24
|
+
provider: string;
|
|
25
|
+
providerInferred: boolean;
|
|
26
|
+
recordName: string;
|
|
27
|
+
warnings: ProviderSearchWarning[];
|
|
28
|
+
zoneDomain: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ResolveProviderTargetInput extends FindDomainProviderInput {
|
|
31
|
+
apex?: boolean;
|
|
32
|
+
subdomain?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ResolveProviderTargetOptions {
|
|
35
|
+
tolerateProviderAccountErrors?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface ResolvedDnsTarget {
|
|
38
|
+
account: string;
|
|
39
|
+
accountInferred: boolean;
|
|
40
|
+
isDefaultAccount: boolean;
|
|
41
|
+
provider: string;
|
|
42
|
+
providerInferred: boolean;
|
|
43
|
+
target: {
|
|
44
|
+
fullDomain: string;
|
|
45
|
+
isApex: boolean;
|
|
46
|
+
recordName: string;
|
|
47
|
+
zoneDomain: string;
|
|
48
|
+
};
|
|
49
|
+
warnings: ProviderSearchWarning[];
|
|
50
|
+
}
|
|
51
|
+
export declare function resolveProviderTarget(input: ResolveProviderTargetInput, options?: ResolveProviderTargetOptions): Promise<ResolvedDnsTarget>;
|
|
52
|
+
/** Find the configured DNS provider account with the longest zone match for a domain. */
|
|
53
|
+
export declare function findDomainProvider(input: FindDomainProviderInput): Promise<DomainProviderResult>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { loadConfig } from './config.js';
|
|
2
|
+
import { DoomainError } from './errors.js';
|
|
3
|
+
import { DEFAULT_PROVIDER_ACCOUNT, isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount, } from './providers/core/config.js';
|
|
4
|
+
import { createProvider, getProviderDefinition, listProviderDefinitions } from './providers/registry.js';
|
|
5
|
+
import { listProviderStatuses } from './providers/status.js';
|
|
6
|
+
import { normalizeDomain, normalizeSubdomain } from './validate.js';
|
|
7
|
+
function resolveRequestedDomain(opts) {
|
|
8
|
+
if (opts.apex && opts.subdomain) {
|
|
9
|
+
throw new DoomainError('INVALID_INPUT', 'Use either --apex or --subdomain, not both.');
|
|
10
|
+
}
|
|
11
|
+
const domain = normalizeDomain(opts.domain);
|
|
12
|
+
if (opts.apex)
|
|
13
|
+
return { forceExactZone: true, fullDomain: domain };
|
|
14
|
+
if (!opts.subdomain)
|
|
15
|
+
return { forceExactZone: false, fullDomain: domain };
|
|
16
|
+
return { forceExactZone: false, fullDomain: `${normalizeSubdomain(opts.subdomain)}.${domain}` };
|
|
17
|
+
}
|
|
18
|
+
function zoneMatchesDomain(fullDomain, zoneDomain, forceExactZone) {
|
|
19
|
+
if (fullDomain === zoneDomain)
|
|
20
|
+
return true;
|
|
21
|
+
if (forceExactZone)
|
|
22
|
+
return false;
|
|
23
|
+
return fullDomain.endsWith(`.${zoneDomain}`);
|
|
24
|
+
}
|
|
25
|
+
function targetFromZone(fullDomain, zoneDomain) {
|
|
26
|
+
if (fullDomain === zoneDomain) {
|
|
27
|
+
return { fullDomain, isApex: true, recordName: '@', zoneDomain };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
fullDomain,
|
|
31
|
+
isApex: false,
|
|
32
|
+
recordName: fullDomain.slice(0, -(zoneDomain.length + 1)),
|
|
33
|
+
zoneDomain,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function candidateDetails(candidates) {
|
|
37
|
+
return candidates.map((candidate) => ({
|
|
38
|
+
account: candidate.account,
|
|
39
|
+
isDefaultAccount: candidate.isDefaultAccount,
|
|
40
|
+
provider: candidate.provider,
|
|
41
|
+
providerName: candidate.providerName,
|
|
42
|
+
zoneDomain: candidate.zone.name,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
function defaultAccountRef(providerId) {
|
|
46
|
+
return { account: DEFAULT_PROVIDER_ACCOUNT, isDefaultAccount: true, providerId };
|
|
47
|
+
}
|
|
48
|
+
function explicitAccountRef(providerId, account) {
|
|
49
|
+
const normalized = normalizeProviderAccount(account);
|
|
50
|
+
return { account: normalized, isDefaultAccount: isDefaultProviderAccount(normalized), providerId };
|
|
51
|
+
}
|
|
52
|
+
function searchError(error) {
|
|
53
|
+
return {
|
|
54
|
+
...(error instanceof DoomainError ? { code: error.code } : {}),
|
|
55
|
+
message: error instanceof Error ? error.message : String(error),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function loadProviderZones(definition, account) {
|
|
59
|
+
const provider = await createProvider(definition.id, { account: account.account });
|
|
60
|
+
const zones = await provider.listZones();
|
|
61
|
+
return {
|
|
62
|
+
candidates: zones.map((zone) => ({
|
|
63
|
+
account: account.account,
|
|
64
|
+
isDefaultAccount: account.isDefaultAccount,
|
|
65
|
+
provider: definition.id,
|
|
66
|
+
providerName: definition.displayName,
|
|
67
|
+
zone,
|
|
68
|
+
})),
|
|
69
|
+
search: {
|
|
70
|
+
account: account.account,
|
|
71
|
+
displayName: definition.displayName,
|
|
72
|
+
id: definition.id,
|
|
73
|
+
isDefaultAccount: account.isDefaultAccount,
|
|
74
|
+
zones: zones.map((zone) => zone.name),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function loadProviderZonesSafely(definition, account) {
|
|
79
|
+
try {
|
|
80
|
+
return await loadProviderZones(definition, account);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
candidates: [],
|
|
85
|
+
search: {
|
|
86
|
+
account: account.account,
|
|
87
|
+
displayName: definition.displayName,
|
|
88
|
+
error: searchError(error),
|
|
89
|
+
id: definition.id,
|
|
90
|
+
isDefaultAccount: account.isDefaultAccount,
|
|
91
|
+
zones: [],
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function providerConnectionDetails() {
|
|
97
|
+
return (await listProviderStatuses({ verify: false })).map((provider) => ({
|
|
98
|
+
configured: provider.configured,
|
|
99
|
+
account: provider.account,
|
|
100
|
+
default: provider.default,
|
|
101
|
+
displayName: provider.displayName,
|
|
102
|
+
docsUrl: provider.docsUrl,
|
|
103
|
+
id: provider.id,
|
|
104
|
+
isDefaultAccount: provider.isDefaultAccount,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
function searchWarnings(searches) {
|
|
108
|
+
return searches.flatMap((search) => search.error
|
|
109
|
+
? [
|
|
110
|
+
{
|
|
111
|
+
account: search.account,
|
|
112
|
+
error: search.error,
|
|
113
|
+
isDefaultAccount: search.isDefaultAccount,
|
|
114
|
+
provider: search.id,
|
|
115
|
+
providerName: search.displayName,
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
: []);
|
|
119
|
+
}
|
|
120
|
+
async function loadConfiguredProviderZones(providerId, accountInput, tolerateProviderAccountErrors = false) {
|
|
121
|
+
const config = await loadConfig();
|
|
122
|
+
const account = accountInput ? normalizeProviderAccount(accountInput) : undefined;
|
|
123
|
+
if (providerId) {
|
|
124
|
+
const definition = getProviderDefinition(providerId);
|
|
125
|
+
const accounts = account ? [explicitAccountRef(definition.id, account)] : listConfiguredProviderAccounts(config, definition);
|
|
126
|
+
const selectedAccounts = accounts.length > 0 ? accounts : [defaultAccountRef(definition.id)];
|
|
127
|
+
const tolerateAccountErrors = tolerateProviderAccountErrors && !account && selectedAccounts.length > 1;
|
|
128
|
+
const results = await Promise.all(selectedAccounts.map((ref) => tolerateAccountErrors ? loadProviderZonesSafely(definition, ref) : loadProviderZones(definition, ref)));
|
|
129
|
+
return {
|
|
130
|
+
accountInferred: account === undefined,
|
|
131
|
+
candidates: results.flatMap((result) => result.candidates),
|
|
132
|
+
providerInferred: false,
|
|
133
|
+
searched: results.map((result) => result.search),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const providerAccounts = listProviderDefinitions().flatMap((definition) => listConfiguredProviderAccounts(config, definition)
|
|
137
|
+
.filter((ref) => !account || ref.account === account)
|
|
138
|
+
.map((ref) => ({ definition, ref })));
|
|
139
|
+
if (providerAccounts.length === 0) {
|
|
140
|
+
const message = account
|
|
141
|
+
? `No DNS provider account named ${account} is configured. Run \`doomain providers connect <provider> --account ${account}\` first.`
|
|
142
|
+
: 'No DNS provider is configured. Run `doomain providers connect` first.';
|
|
143
|
+
throw new DoomainError('CONFIG_NOT_FOUND', message, {
|
|
144
|
+
account,
|
|
145
|
+
configuredProviders: await providerConnectionDetails(),
|
|
146
|
+
recovery: 'Connect the DNS provider that owns this domain, then retry `doomain link <domain> --json`.',
|
|
147
|
+
suggestedCommands: account
|
|
148
|
+
? [`doomain providers connect <provider> --account ${account}`, 'doomain link <domain> --json']
|
|
149
|
+
: ['doomain providers connect', 'doomain link <domain> --json'],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const results = await Promise.all(providerAccounts.map(({ definition, ref }) => loadProviderZonesSafely(definition, ref)));
|
|
153
|
+
return {
|
|
154
|
+
accountInferred: account === undefined,
|
|
155
|
+
candidates: results.flatMap((result) => result.candidates),
|
|
156
|
+
providerInferred: true,
|
|
157
|
+
searched: results.map((result) => result.search),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export async function resolveProviderTarget(input, options = {}) {
|
|
161
|
+
const requested = resolveRequestedDomain(input);
|
|
162
|
+
const zones = await loadConfiguredProviderZones(input.provider, input.account, options.tolerateProviderAccountErrors);
|
|
163
|
+
const matches = zones.candidates
|
|
164
|
+
.filter((candidate) => zoneMatchesDomain(requested.fullDomain, candidate.zone.name, requested.forceExactZone))
|
|
165
|
+
.sort((a, b) => b.zone.name.length - a.zone.name.length);
|
|
166
|
+
if (matches.length === 0) {
|
|
167
|
+
const account = input.account ? normalizeProviderAccount(input.account) : undefined;
|
|
168
|
+
const providerMessage = input.provider
|
|
169
|
+
? `${getProviderDefinition(input.provider).displayName}${account ? ` account ${account}` : ''} does not have a matching DNS zone for ${requested.fullDomain}.`
|
|
170
|
+
: `No configured DNS provider has a matching DNS zone for ${requested.fullDomain}.`;
|
|
171
|
+
throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', providerMessage, {
|
|
172
|
+
account,
|
|
173
|
+
configuredProviders: await providerConnectionDetails(),
|
|
174
|
+
domain: requested.fullDomain,
|
|
175
|
+
recovery: 'Retry with --provider <id> --account <alias> only if another configured provider account owns this zone. Otherwise connect the DNS provider account that owns this domain.',
|
|
176
|
+
searchedZones: zones.searched,
|
|
177
|
+
suggestedCommands: [`doomain link ${requested.fullDomain} --provider <id> --account <alias> --json`, 'doomain providers connect'],
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const bestLength = matches[0].zone.name.length;
|
|
181
|
+
const bestMatches = matches.filter((candidate) => candidate.zone.name.length === bestLength);
|
|
182
|
+
const uniqueBestMatches = bestMatches.filter((candidate, index, candidates) => candidates.findIndex((item) => item.provider === candidate.provider && item.account === candidate.account && item.zone.name === candidate.zone.name) === index);
|
|
183
|
+
if (uniqueBestMatches.length > 1) {
|
|
184
|
+
throw new DoomainError('PROVIDER_ZONE_AMBIGUOUS', `Multiple DNS provider accounts have a matching DNS zone for ${requested.fullDomain}. Pass --provider and --account to choose one.`, { candidates: candidateDetails(uniqueBestMatches), domain: requested.fullDomain });
|
|
185
|
+
}
|
|
186
|
+
const selected = uniqueBestMatches[0];
|
|
187
|
+
return {
|
|
188
|
+
account: selected.account,
|
|
189
|
+
accountInferred: zones.accountInferred,
|
|
190
|
+
isDefaultAccount: selected.isDefaultAccount,
|
|
191
|
+
provider: selected.provider,
|
|
192
|
+
providerInferred: zones.providerInferred,
|
|
193
|
+
target: targetFromZone(requested.fullDomain, selected.zone.name),
|
|
194
|
+
warnings: searchWarnings(zones.searched),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function discoveryError(error, domain) {
|
|
198
|
+
if (error.code !== 'CONFIG_NOT_FOUND' && error.code !== 'PROVIDER_ZONE_NOT_FOUND')
|
|
199
|
+
return error;
|
|
200
|
+
const details = error.details && typeof error.details === 'object' ? error.details : {};
|
|
201
|
+
return new DoomainError(error.code, error.message, {
|
|
202
|
+
...details,
|
|
203
|
+
recovery: `Connect or repair the DNS provider account that owns this domain, then retry \`doomain domains find ${domain} --json\`.`,
|
|
204
|
+
suggestedCommands: ['doomain providers connect', `doomain domains find ${domain} --json`],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** Find the configured DNS provider account with the longest zone match for a domain. */
|
|
208
|
+
export async function findDomainProvider(input) {
|
|
209
|
+
try {
|
|
210
|
+
const resolved = await resolveProviderTarget(input, { tolerateProviderAccountErrors: true });
|
|
211
|
+
return {
|
|
212
|
+
account: resolved.account,
|
|
213
|
+
accountInferred: resolved.accountInferred,
|
|
214
|
+
complete: resolved.warnings.length === 0,
|
|
215
|
+
domain: resolved.target.fullDomain,
|
|
216
|
+
isApex: resolved.target.isApex,
|
|
217
|
+
isDefaultAccount: resolved.isDefaultAccount,
|
|
218
|
+
provider: resolved.provider,
|
|
219
|
+
providerInferred: resolved.providerInferred,
|
|
220
|
+
recordName: resolved.target.recordName,
|
|
221
|
+
warnings: resolved.warnings,
|
|
222
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error instanceof DoomainError)
|
|
227
|
+
throw discoveryError(error, input.domain);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
1
|
+
export type DoomainErrorCode = 'CONFIG_NOT_FOUND' | 'CLERK_AUTH_FAILED' | 'CLERK_PRODUCTION_EXISTS' | 'DNS_TARGET_CONFLICT' | 'DOMAIN_LINK_FAILED' | 'DOMAIN_PROVIDER_DISCOVERY_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;
|
|
@@ -59,6 +59,7 @@ export interface LinkDomainResult extends LinkDomainPlan {
|
|
|
59
59
|
verified: boolean;
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
export declare function withProviderRecordOptions(provider: string, record: DnsRecordInput): DnsRecordInput;
|
|
62
63
|
export declare function verificationRecords(raw: unknown, zoneDomain: string): DnsRecordInput[];
|
|
63
64
|
export declare function createLinkPlan(input: LinkDomainInput): Promise<LinkDomainPlan>;
|
|
64
65
|
export declare function linkDomain(input: LinkDomainInput): Promise<LinkDomainResult>;
|