doomain 0.1.14 → 0.1.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Doomain
2
2
 
3
- Doomain links a Vercel project to a custom domain from your terminal.
3
+ Doomain links Vercel projects and first-time Clerk production instances to custom domains from your terminal.
4
4
 
5
5
  It handles the boring parts of custom-domain setup: selecting the Vercel project, finding the right DNS zone, adding the domain to Vercel, writing the Vercel DNS records, waiting for public DNS propagation, and asking Vercel to verify the domain.
6
6
 
@@ -16,6 +16,7 @@ Use the interactive wizard when working by hand. Use explicit commands with `--j
16
16
  - Safety checks before replacing DNS records that point elsewhere.
17
17
  - DNS propagation and Vercel verification wait loop.
18
18
  - DNS provider support for Spaceship, Namecheap, Cloudflare, and Hostinger.
19
+ - First-time Clerk production setup with automatic CNAME configuration.
19
20
 
20
21
  ## Install
21
22
 
@@ -86,6 +87,17 @@ Apply it:
86
87
  doomain link app.example.com --project my-vercel-project --json
87
88
  ```
88
89
 
90
+ Set up a Clerk application's first production instance and primary domain:
91
+
92
+ ```bash
93
+ doomain auth clerk --platform-api-key "$CLERK_PLATFORM_API_KEY" --app app_123 --json
94
+ doomain clerk domains add example.com --app app_123 --json
95
+ ```
96
+
97
+ Create a Platform API key from the Clerk Dashboard API keys page. Platform keys start with `ak_`; Clerk instance secret keys (`sk_`) cannot create a production instance.
98
+
99
+ This Clerk command intentionally aborts when the application already has a production instance. Change existing production domains manually in the Clerk Dashboard or with Clerk CLI.
100
+
89
101
  If JSON mode returns `DNS_TARGET_CONFLICT`, the current DNS target appears to point to another project or site. Re-run with `--force` only when you intend to replace that DNS target.
90
102
 
91
103
  ## Provider Setup
@@ -142,7 +154,7 @@ doomain providers connect cloudflare \
142
154
  --credential accountId=your_cloudflare_account_id
143
155
  ```
144
156
 
145
- Cloudflare records created for Vercel `A`, `AAAA`, and `CNAME` targets are set to `proxied: false` so Vercel can validate the domain.
157
+ Cloudflare records created for Vercel or Clerk `A`, `AAAA`, and `CNAME` targets are set to `proxied: false` so the service can validate the domain.
146
158
 
147
159
  ### Hostinger
148
160
 
@@ -209,6 +221,27 @@ Use `-p` as shorthand for `--project`:
209
221
  doomain link app.example.com -p my-app
210
222
  ```
211
223
 
224
+ ## Clerk Production Domains
225
+
226
+ `doomain clerk domains add` mirrors Clerk CLI's initial production deployment API. It creates a production instance by cloning the application's development instance, sets the requested primary domain, writes every CNAME returned by Clerk, and optionally waits for Clerk's DNS, SSL, and email DNS status.
227
+
228
+ ```bash
229
+ doomain clerk domains add example.com --app app_123
230
+ ```
231
+
232
+ Use `--dry-run` to verify that the application has no production instance and that Doomain can resolve the DNS zone. Clerk only returns the exact CNAME records after production is created, so they are not included in the dry-run result.
233
+
234
+ If a production instance already exists, the command returns `CLERK_PRODUCTION_EXISTS` without changing DNS. Doomain does not automate production-domain migrations because Clerk domain changes can cause downtime and require publishable-key, OAuth redirect, and deployment updates.
235
+
236
+ After creation, the result includes Clerk CLI follow-up commands. Run them to link the local project, pull production keys, finish production OAuth configuration, and verify provisioning:
237
+
238
+ ```bash
239
+ clerk link --app app_123
240
+ clerk env pull --app app_123 --instance prod
241
+ clerk deploy
242
+ clerk deploy status
243
+ ```
244
+
212
245
  ## What Gets Created
213
246
 
214
247
  For apex/root domains, Doomain creates Vercel's apex `A` record:
@@ -306,6 +339,7 @@ doomain providers list --json
306
339
  doomain providers status --no-verify --json
307
340
  doomain domains list --provider cloudflare --domain example.com --json
308
341
  doomain projects list --search my-app --json
342
+ doomain clerk domains add example.com --app app_123 --json
309
343
  doomain schema --json
310
344
  doomain schema link --json
311
345
  ```
@@ -376,6 +410,28 @@ doomain auth logout vercel --json
376
410
 
377
411
  If `VERCEL_TOKEN` or `VERCEL_TEAM_ID` are still set, they continue to override local config.
378
412
 
413
+ ### `doomain auth clerk`
414
+
415
+ Saves and verifies a Clerk Platform API key and default application id.
416
+
417
+ ```bash
418
+ doomain auth clerk --platform-api-key ak_123 --app app_123
419
+ ```
420
+
421
+ ### `doomain auth logout clerk`
422
+
423
+ Removes saved Clerk credentials. `CLERK_PLATFORM_API_KEY` and `CLERK_APPLICATION_ID` continue to override local config when set.
424
+
425
+ ### `doomain clerk domains add <domain>`
426
+
427
+ Creates the application's first Clerk production instance and primary domain, then configures Clerk's returned CNAME records.
428
+
429
+ ```bash
430
+ doomain clerk domains add example.com --app app_123
431
+ doomain clerk domains add example.com --app app_123 --dry-run --json
432
+ doomain clerk domains add example.com --app app_123 --no-wait --json
433
+ ```
434
+
379
435
  ### `doomain providers list`
380
436
 
381
437
  Lists supported DNS providers.
@@ -491,6 +547,13 @@ VERCEL_TOKEN
491
547
  VERCEL_TEAM_ID
492
548
  ```
493
549
 
550
+ Clerk:
551
+
552
+ ```bash
553
+ CLERK_PLATFORM_API_KEY
554
+ CLERK_APPLICATION_ID
555
+ ```
556
+
494
557
  Spaceship:
495
558
 
496
559
  ```bash
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AuthClerk extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ app: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ 'platform-api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,55 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import * as p from '@clack/prompts';
3
+ import { createClerkPlatformClient } from '../../lib/clerk.js';
4
+ import { getConfigPath, maskSecret, updateConfig } from '../../lib/config.js';
5
+ import { jsonFlag } from '../../lib/flags.js';
6
+ import { createOutput, outputError } from '../../lib/output.js';
7
+ function value(input, message) {
8
+ const resolved = input?.trim();
9
+ if (!resolved)
10
+ throw new Error(message);
11
+ return resolved;
12
+ }
13
+ export default class AuthClerk extends Command {
14
+ static description = 'Save Clerk Platform API credentials locally.';
15
+ static examples = ['<%= config.bin %> <%= command.id %> --platform-api-key ak_123 --app app_123 --json'];
16
+ static flags = {
17
+ app: Flags.string({ description: 'Default Clerk application id.' }),
18
+ json: jsonFlag,
19
+ 'platform-api-key': Flags.string({ description: 'Clerk Platform API key (ak_...).' }),
20
+ };
21
+ async run() {
22
+ const { flags } = await this.parse(AuthClerk);
23
+ const out = createOutput({ json: flags.json });
24
+ try {
25
+ let platformApiKey = flags['platform-api-key'] ?? process.env.CLERK_PLATFORM_API_KEY;
26
+ let appId = flags.app ?? process.env.CLERK_APPLICATION_ID;
27
+ if (!out.json) {
28
+ if (!platformApiKey) {
29
+ const entered = await p.password({ message: 'Clerk Platform API key' });
30
+ if (p.isCancel(entered))
31
+ return;
32
+ platformApiKey = entered;
33
+ }
34
+ if (!appId) {
35
+ const entered = await p.text({ message: 'Default Clerk application id' });
36
+ if (p.isCancel(entered))
37
+ return;
38
+ appId = entered;
39
+ }
40
+ }
41
+ platformApiKey = value(platformApiKey, 'Missing Clerk Platform API key. Pass --platform-api-key or set CLERK_PLATFORM_API_KEY.');
42
+ appId = value(appId, 'Missing Clerk application id. Pass --app or set CLERK_APPLICATION_ID.');
43
+ if (!platformApiKey.startsWith('ak_'))
44
+ throw new Error('Clerk Platform API keys must start with ak_.');
45
+ await createClerkPlatformClient({ platformApiKey }).fetchApplication(appId);
46
+ await updateConfig((config) => ({ ...config, clerk: { appId, platformApiKey } }));
47
+ out.result({ clerk: { appId, platformApiKey: maskSecret(platformApiKey) }, configPath: getConfigPath() });
48
+ out.success(`Clerk credentials saved to ${getConfigPath()}.`);
49
+ }
50
+ catch (error) {
51
+ outputError(out.json, error, 'CLERK_AUTH_FAILED');
52
+ this.exit(1);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AuthLogoutClerk extends Command {
3
+ static description: string;
4
+ static flags: {
5
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
+ };
7
+ run(): Promise<void>;
8
+ }
@@ -0,0 +1,29 @@
1
+ import { Command } from '@oclif/core';
2
+ import { getConfigPath, updateConfig } from '../../../lib/config.js';
3
+ import { jsonFlag } from '../../../lib/flags.js';
4
+ import { createOutput, outputError } from '../../../lib/output.js';
5
+ export default class AuthLogoutClerk extends Command {
6
+ static description = 'Remove saved Clerk credentials locally.';
7
+ static flags = { json: jsonFlag };
8
+ async run() {
9
+ const { flags } = await this.parse(AuthLogoutClerk);
10
+ const out = createOutput({ json: flags.json });
11
+ try {
12
+ let removed = false;
13
+ await updateConfig((config) => {
14
+ removed = config.clerk !== undefined;
15
+ const { clerk: _clerk, ...next } = config;
16
+ return next;
17
+ });
18
+ const environmentOverrides = ['CLERK_PLATFORM_API_KEY', 'CLERK_APPLICATION_ID'].filter((key) => process.env[key]);
19
+ out.result({ configPath: getConfigPath(), environmentOverrides, removed, service: 'clerk' });
20
+ if (environmentOverrides.length > 0)
21
+ out.warn(`Clerk environment credentials are still set: ${environmentOverrides.join(', ')}.`);
22
+ out.success(removed ? `Clerk credentials removed from ${getConfigPath()}.` : 'Clerk credentials were not saved.');
23
+ }
24
+ catch (error) {
25
+ outputError(out.json, error, 'MISSING_CREDENTIALS');
26
+ this.exit(1);
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,19 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ClerkDomainsAdd extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ domain: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ account: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ app: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ provider: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
16
+ wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ };
18
+ run(): Promise<void>;
19
+ }
@@ -0,0 +1,92 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import * as p from '@clack/prompts';
3
+ import { addClerkProductionDomain } from '../../../lib/clerk-domain.js';
4
+ import { accountFlag, jsonFlag, providerFlag } from '../../../lib/flags.js';
5
+ import { createOutput, outputError } from '../../../lib/output.js';
6
+ function recordLine(record) {
7
+ return `${record.type} ${record.name} -> ${record.value}${record.proxied === undefined ? '' : ` (proxied ${record.proxied})`}`;
8
+ }
9
+ function conflictNote(warning) {
10
+ return [
11
+ 'Existing:',
12
+ ...warning.conflicts.map((conflict) => `- ${recordLine(conflict.existing)}`),
13
+ '',
14
+ 'Required by Clerk:',
15
+ ...warning.desired.map((record) => `- ${recordLine(record)}`),
16
+ ].join('\n');
17
+ }
18
+ function resultMessage(result) {
19
+ if (result.clerk.verified)
20
+ return `${result.domain} is configured and verified for Clerk production.`;
21
+ return `${result.domain} is configured for Clerk production. Run ${result.nextSteps.at(-1)} to check provisioning.`;
22
+ }
23
+ export default class ClerkDomainsAdd extends Command {
24
+ static description = 'Create a Clerk production instance with its primary domain and configure DNS.';
25
+ static examples = [
26
+ '<%= config.bin %> <%= command.id %> example.com --app app_123 --json',
27
+ '<%= config.bin %> <%= command.id %> example.com --app app_123 --provider cloudflare --no-wait',
28
+ '<%= config.bin %> <%= command.id %> example.com --app app_123 --dry-run --json',
29
+ ];
30
+ static args = {
31
+ domain: Args.string({ description: 'Production primary domain, for example example.com.', required: true }),
32
+ };
33
+ static flags = {
34
+ account: accountFlag,
35
+ app: Flags.string({ description: 'Clerk application id. Defaults to CLERK_APPLICATION_ID or saved Clerk config.' }),
36
+ 'dry-run': Flags.boolean({ description: 'Check the Clerk application and DNS zone without creating the production instance.' }),
37
+ force: Flags.boolean({ description: 'Overwrite DNS records that conflict with Clerk requirements.' }),
38
+ json: jsonFlag,
39
+ provider: providerFlag,
40
+ timeout: Flags.integer({ default: 300, description: 'Wait timeout in seconds.' }),
41
+ wait: Flags.boolean({ allowNo: true, default: true, description: 'Wait for Clerk DNS, SSL, and email DNS verification.' }),
42
+ };
43
+ async run() {
44
+ const { args, flags } = await this.parse(ClerkDomainsAdd);
45
+ const out = createOutput({ json: flags.json });
46
+ let spinner;
47
+ try {
48
+ if (!out.json && !flags['dry-run']) {
49
+ p.note(`This creates the first production instance for Clerk application ${flags.app ?? 'the configured app'} and sets ${args.domain} as its primary domain. Existing production instances are never modified.`, 'Clerk production setup');
50
+ const confirmed = await p.confirm({ message: 'Create the Clerk production instance?', initialValue: false });
51
+ if (confirmed !== true) {
52
+ p.cancel('Cancelled');
53
+ return;
54
+ }
55
+ }
56
+ spinner = out.spinner();
57
+ spinner.start(flags['dry-run'] ? 'Checking Clerk production setup' : 'Creating Clerk production setup');
58
+ const result = await addClerkProductionDomain({
59
+ account: flags.account,
60
+ app: flags.app,
61
+ confirmDnsOverride: out.json
62
+ ? undefined
63
+ : async (warning) => {
64
+ spinner?.stop('DNS conflict found');
65
+ p.note(conflictNote(warning), 'DNS records point elsewhere');
66
+ const confirmed = await p.confirm({ message: 'Overwrite these DNS records?', initialValue: false });
67
+ if (confirmed === true)
68
+ spinner?.start('Continuing Clerk production setup');
69
+ return confirmed === true;
70
+ },
71
+ domain: args.domain,
72
+ dryRun: flags['dry-run'],
73
+ force: flags.force,
74
+ progress: out.json ? undefined : (message) => spinner?.message(message),
75
+ provider: flags.provider,
76
+ timeoutSeconds: flags.timeout,
77
+ wait: flags.wait,
78
+ });
79
+ spinner.stop(flags['dry-run'] ? 'Clerk production setup is available' : 'Clerk production domain configured');
80
+ out.result(result);
81
+ if (!out.json) {
82
+ p.note(result.nextSteps.map((step) => `- ${step}`).join('\n'), 'Next steps');
83
+ out.outro(flags['dry-run'] ? `Ready to create Clerk production for ${result.domain}.` : resultMessage(result));
84
+ }
85
+ }
86
+ catch (error) {
87
+ spinner?.error('Clerk production setup failed');
88
+ outputError(out.json, error, 'DOMAIN_LINK_FAILED');
89
+ this.exit(1);
90
+ }
91
+ }
92
+ }
@@ -1,6 +1,5 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import * as p from '@clack/prompts';
3
- import { DoomainError } from '../lib/errors.js';
4
3
  import { accountFlag, apexFlag, domainFlag, jsonFlag, projectFlag, providerFlag, subdomainFlag } from '../lib/flags.js';
5
4
  import { linkDomain } from '../lib/link-domain.js';
6
5
  import { createOutput, outputError } from '../lib/output.js';
@@ -27,6 +26,18 @@ function dnsOverrideNote(warning) {
27
26
  ...warning.desired.map((record) => `- ${recordLine(record)}`),
28
27
  ].join('\n');
29
28
  }
29
+ function providerAccount(result) {
30
+ return result.isDefaultAccount ? result.provider : `${result.provider}/${result.account}`;
31
+ }
32
+ function dryRunPreview(result) {
33
+ const account = providerAccount(result);
34
+ return [
35
+ `Vercel: add ${result.domain} to ${result.project}`,
36
+ `DNS provider: ${account} (${result.zoneDomain})`,
37
+ ...result.records.map((record) => `DNS: ${recordLine(record)} in ${account}`),
38
+ `Actions: ${result.actions.join(', ')}`,
39
+ ].join('\n');
40
+ }
30
41
  export default class Link extends Command {
31
42
  static description = 'Link a Vercel project to a domain and create DNS records.';
32
43
  static examples = [
@@ -59,14 +70,13 @@ export default class Link extends Command {
59
70
  let spinner;
60
71
  const domain = flags.domain ?? args.domain;
61
72
  try {
62
- if (!domain) {
63
- throw new DoomainError('MISSING_ARGUMENT', 'Domain is required. Use `doomain link <domain>` or pass --domain.');
64
- }
65
73
  if (flags['dry-run']) {
66
74
  const result = await linkDomain({ ...flags, domain, dryRun: true, timeoutSeconds: flags.timeout });
67
75
  out.result(result);
68
- if (!out.json)
76
+ if (!out.json) {
77
+ p.note(dryRunPreview(result), 'Dry run');
69
78
  out.success(`Dry run ready for ${result.domain}.`);
79
+ }
70
80
  return;
71
81
  }
72
82
  out.intro('Doomain');
@@ -0,0 +1,39 @@
1
+ import { type ClerkDomainStatus } from './clerk.js';
2
+ import { type DnsOverrideWarning } from './link-domain.js';
3
+ import type { DnsRecordInput } from './providers/types.js';
4
+ export interface AddClerkDomainInput {
5
+ account?: string;
6
+ app?: string;
7
+ domain?: string;
8
+ dryRun?: boolean;
9
+ force?: boolean;
10
+ provider?: string;
11
+ wait?: boolean;
12
+ timeoutSeconds?: number;
13
+ confirmDnsOverride?: (warning: DnsOverrideWarning) => Promise<boolean>;
14
+ progress?: (message: string) => void;
15
+ }
16
+ export interface AddClerkDomainResult {
17
+ account: string;
18
+ app: string;
19
+ clerk: {
20
+ domainId?: string;
21
+ productionInstanceCreated: boolean;
22
+ productionInstanceId?: string;
23
+ status?: ClerkDomainStatus;
24
+ verified: boolean;
25
+ };
26
+ dns: {
27
+ propagated: boolean;
28
+ skipped: DnsRecordInput[];
29
+ updated: boolean;
30
+ };
31
+ domain: string;
32
+ dryRun: boolean;
33
+ isDefaultAccount: boolean;
34
+ provider: string;
35
+ records: DnsRecordInput[];
36
+ zoneDomain: string;
37
+ nextSteps: string[];
38
+ }
39
+ export declare function addClerkProductionDomain(input: AddClerkDomainInput): Promise<AddClerkDomainResult>;
@@ -0,0 +1,162 @@
1
+ import { createClerkPlatformClient, resolveClerkPlatformConfig } from './clerk.js';
2
+ import { DoomainError } from './errors.js';
3
+ import { resolveProviderTarget, withProviderRecordOptions } from './link-domain.js';
4
+ import { createProvider } from './providers/registry.js';
5
+ import { normalizeDomain } from './validate.js';
6
+ function relativeRecordName(host, zoneDomain) {
7
+ const normalized = host.trim().toLowerCase().replace(/\.$/, '');
8
+ if (normalized === zoneDomain)
9
+ return '@';
10
+ if (!normalized.endsWith(`.${zoneDomain}`)) {
11
+ throw new DoomainError('INVALID_INPUT', `Clerk returned DNS host ${host}, which is outside the selected zone ${zoneDomain}.`);
12
+ }
13
+ return normalized.slice(0, -(zoneDomain.length + 1));
14
+ }
15
+ function recordsFromTargets(provider, zoneDomain, targets) {
16
+ return targets.map((target) => withProviderRecordOptions(provider, {
17
+ name: relativeRecordName(target.host, zoneDomain),
18
+ ttl: 300,
19
+ type: 'CNAME',
20
+ value: target.value.trim().replace(/\.$/, ''),
21
+ }));
22
+ }
23
+ function productionExistsError(app, instanceId) {
24
+ return new DoomainError('CLERK_PRODUCTION_EXISTS', `Clerk application ${app} already has a production instance. Configure domain changes manually in Clerk; Doomain will not modify an existing production domain.`, {
25
+ app,
26
+ instanceId,
27
+ recovery: 'Use the Clerk Dashboard Domains page or `clerk deploy` to inspect and configure the existing production instance.',
28
+ suggestedCommands: [`clerk link --app ${app}`, 'clerk deploy status', 'clerk open domains'],
29
+ });
30
+ }
31
+ function dnsConflictError(warning, productionInstanceId) {
32
+ return new DoomainError('DNS_TARGET_CONFLICT', `${warning.domain} has DNS records that conflict with Clerk's required records. Re-run with --force to overwrite them.`, {
33
+ ...warning,
34
+ partialState: { productionInstanceCreated: true, productionInstanceId },
35
+ recovery: 'The Clerk production instance now exists. Resolve the DNS conflict, then finish setup with `clerk deploy`.',
36
+ });
37
+ }
38
+ function statusComplete(check) {
39
+ return check?.required === false || check?.status === 'complete';
40
+ }
41
+ async function waitForStatus(client, app, domainId, timeoutSeconds, progress) {
42
+ await client.triggerDomainDnsCheck(app, domainId).catch((error) => {
43
+ const conflict = error instanceof DoomainError && JSON.stringify(error.details).toLowerCase().includes('conflict');
44
+ if (!conflict)
45
+ throw error;
46
+ });
47
+ const deadline = Date.now() + timeoutSeconds * 1000;
48
+ let status = { status: 'incomplete' };
49
+ let attempt = 1;
50
+ while (Date.now() <= deadline) {
51
+ progress?.(`Checking Clerk DNS, SSL, and email DNS status (attempt ${attempt})`);
52
+ status = await client.getDomainStatus(app, domainId);
53
+ if (status.status === 'complete')
54
+ return status;
55
+ attempt += 1;
56
+ if (Date.now() > deadline)
57
+ break;
58
+ await new Promise((resolve) => setTimeout(resolve, 5000));
59
+ }
60
+ return status;
61
+ }
62
+ export async function addClerkProductionDomain(input) {
63
+ const domain = normalizeDomain(input.domain ?? '');
64
+ const config = await resolveClerkPlatformConfig(input.app);
65
+ const client = createClerkPlatformClient(config);
66
+ input.progress?.('Checking Clerk application');
67
+ const application = await client.fetchApplication(config.appId);
68
+ const production = application.instances.find((instance) => instance.environment_type === 'production');
69
+ if (production)
70
+ throw productionExistsError(config.appId, production.instance_id);
71
+ const development = application.instances.find((instance) => instance.environment_type === 'development');
72
+ if (!development)
73
+ throw new DoomainError('PROJECT_NOT_FOUND', `Clerk application ${config.appId} does not have a development instance to clone.`);
74
+ input.progress?.('Finding the DNS provider and zone');
75
+ const resolved = await resolveProviderTarget({ account: input.account, domain, provider: input.provider });
76
+ const provider = await createProvider(resolved.provider, { account: resolved.account });
77
+ const zone = await provider.getZone(resolved.target.zoneDomain);
78
+ if (!zone)
79
+ throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', `${provider.name} does not have a DNS zone for ${resolved.target.zoneDomain}.`);
80
+ const nextSteps = [
81
+ `clerk link --app ${config.appId}`,
82
+ `clerk env pull --app ${config.appId} --instance prod`,
83
+ 'clerk deploy',
84
+ 'clerk deploy status',
85
+ ];
86
+ if (input.dryRun) {
87
+ return {
88
+ account: resolved.account,
89
+ app: config.appId,
90
+ clerk: { productionInstanceCreated: false, verified: false },
91
+ dns: { propagated: false, skipped: [], updated: false },
92
+ domain,
93
+ dryRun: true,
94
+ isDefaultAccount: resolved.isDefaultAccount,
95
+ nextSteps,
96
+ provider: resolved.provider,
97
+ records: [],
98
+ zoneDomain: resolved.target.zoneDomain,
99
+ };
100
+ }
101
+ input.progress?.('Creating Clerk production instance and primary domain');
102
+ const created = await client.createProductionInstance(config.appId, domain, development.instance_id);
103
+ const clerkDomain = created.active_domain;
104
+ if (!clerkDomain) {
105
+ throw new DoomainError('DOMAIN_LINK_FAILED', 'Clerk created the production instance but did not return its primary domain.', {
106
+ productionInstanceId: created.id,
107
+ });
108
+ }
109
+ const records = recordsFromTargets(resolved.provider, resolved.target.zoneDomain, clerkDomain.cname_targets ?? []);
110
+ if (records.length === 0) {
111
+ throw new DoomainError('DOMAIN_LINK_FAILED', 'Clerk created the production instance but did not return any DNS records.', {
112
+ domainId: clerkDomain.id,
113
+ productionInstanceId: created.id,
114
+ });
115
+ }
116
+ input.progress?.(`Checking existing DNS records in ${provider.name}`);
117
+ let forceDns = Boolean(input.force);
118
+ let dnsPlan = await provider.planChanges(zone, records, { force: forceDns });
119
+ if (!forceDns && dnsPlan.conflicts.length > 0) {
120
+ const warning = {
121
+ account: resolved.account,
122
+ conflicts: dnsPlan.conflicts,
123
+ desired: records,
124
+ domain,
125
+ provider: resolved.provider,
126
+ providerName: provider.name,
127
+ recordName: records[0]?.name ?? '@',
128
+ zoneDomain: resolved.target.zoneDomain,
129
+ };
130
+ forceDns = (await input.confirmDnsOverride?.(warning)) === true;
131
+ if (!forceDns)
132
+ throw dnsConflictError(warning, created.id);
133
+ dnsPlan = await provider.planChanges(zone, records, { force: true });
134
+ }
135
+ input.progress?.(`Creating Clerk DNS records in ${provider.name}`);
136
+ const dnsResult = await provider.applyChanges(zone, dnsPlan, { force: forceDns });
137
+ const shouldWait = input.wait ?? true;
138
+ const status = shouldWait
139
+ ? await waitForStatus(client, config.appId, clerkDomain.id, input.timeoutSeconds ?? 300, input.progress)
140
+ : undefined;
141
+ const verified = status?.status === 'complete';
142
+ const propagated = Boolean(status && statusComplete(status.dns) && statusComplete(status.mail));
143
+ return {
144
+ account: resolved.account,
145
+ app: config.appId,
146
+ clerk: {
147
+ domainId: clerkDomain.id,
148
+ productionInstanceCreated: true,
149
+ productionInstanceId: created.id,
150
+ status,
151
+ verified,
152
+ },
153
+ dns: { propagated, skipped: dnsResult.skipped, updated: dnsResult.applied.length > 0 },
154
+ domain,
155
+ dryRun: false,
156
+ isDefaultAccount: resolved.isDefaultAccount,
157
+ nextSteps,
158
+ provider: resolved.provider,
159
+ records,
160
+ zoneDomain: resolved.target.zoneDomain,
161
+ };
162
+ }
@@ -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
+ }
@@ -8,12 +8,13 @@ export const commandSchemas = [
8
8
  'doomain link app.example.com --project my-app --json',
9
9
  'doomain link --domain app.example.com --project my-app --json',
10
10
  'doomain link --domain example.com --subdomain app --project my-app --json',
11
+ 'doomain link --project my-app --json',
11
12
  'doomain link --provider spaceship --domain example.com --apex --project my-app --dry-run --json',
12
13
  'doomain link app.example.com --provider spaceship --account work --project my-app --json',
13
14
  ],
14
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.',
15
16
  agentInstructions: [
16
- '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.',
17
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.',
18
19
  'Only run follow-up commands when the JSON error includes a recovery action that requires them.',
19
20
  ],
@@ -27,16 +28,69 @@ export const commandSchemas = [
27
28
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
28
29
  { name: 'provider', type: 'string', description: 'DNS provider id. Inferred from the target domain when omitted.' },
29
30
  { name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
30
- { name: 'domain', type: 'string', description: 'Target domain or base zone, for example app.example.com or example.com.' },
31
+ {
32
+ name: 'domain',
33
+ type: 'string',
34
+ description: 'Target domain or base zone, for example app.example.com or example.com. Optional when DOOMAIN_DOMAIN or a default domain is configured.',
35
+ },
31
36
  { name: 'subdomain', type: 'string', description: 'Subdomain to add.' },
32
37
  { name: 'apex', type: 'boolean', description: 'Use the root/apex domain.' },
33
38
  { name: 'project', type: 'string', description: 'Vercel project id/name. Optional when project inference succeeds.' },
34
39
  { name: 'dry-run', type: 'boolean', description: 'Preview changes without writing. Intended for human previews; agents should not use this unless explicitly asked.' },
35
- { name: 'force', type: 'boolean', description: 'Overwrite conflicting DNS records.' },
36
- { name: 'wait', type: 'boolean', description: 'Wait for DNS and Vercel verification.', default: true },
40
+ {
41
+ name: 'force',
42
+ type: 'boolean',
43
+ description: 'Move existing Vercel project domains and overwrite conflicting DNS records. Interactive DNS override confirmation does not move Vercel aliases; pass --force for that.',
44
+ },
45
+ { name: 'wait', type: 'boolean', description: 'Wait for DNS and Vercel verification. Use --no-wait to skip waiting.', default: true },
37
46
  { name: 'timeout', type: 'integer', description: 'Wait timeout in seconds.', default: 300 },
38
47
  ],
39
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
+ },
76
+ {
77
+ name: 'schema',
78
+ description: 'Print machine-readable command schemas for agents.',
79
+ examples: ['doomain schema --json', 'doomain schema link --json'],
80
+ safeForAgents: true,
81
+ flags: [
82
+ { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
83
+ ],
84
+ },
85
+ {
86
+ name: 'providers list',
87
+ description: 'List supported DNS providers.',
88
+ examples: ['doomain providers list --json'],
89
+ safeForAgents: true,
90
+ flags: [
91
+ { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
92
+ ],
93
+ },
40
94
  {
41
95
  name: 'providers connect',
42
96
  description: 'Save DNS provider credentials locally after verifying them. Prompts for a provider when omitted.',
@@ -105,6 +159,24 @@ export const commandSchemas = [
105
159
  { name: 'account', type: 'string', description: 'DNS provider profile/account alias. Defaults to the provider default account.' },
106
160
  ],
107
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
+ },
108
180
  {
109
181
  name: 'auth vercel',
110
182
  description: 'Save Vercel credentials locally.',
@@ -123,6 +195,42 @@ export const commandSchemas = [
123
195
  { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
124
196
  ],
125
197
  },
198
+ {
199
+ name: 'domains list',
200
+ description: 'List DNS zones and records for a provider.',
201
+ examples: ['doomain domains list --json', 'doomain domains list --provider cloudflare --domain example.com --json'],
202
+ safeForAgents: true,
203
+ flags: [
204
+ { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
205
+ { name: 'provider', type: 'string', description: 'DNS provider id. Defaults to DOOMAIN_PROVIDER, configured default provider, then spaceship.' },
206
+ { name: 'account', type: 'string', description: 'DNS provider profile/account alias. Omit to list all configured accounts for the provider.' },
207
+ { name: 'domain', type: 'string', description: 'Limit output to one DNS zone.' },
208
+ ],
209
+ },
210
+ {
211
+ name: 'projects list',
212
+ description: 'List Vercel projects.',
213
+ examples: ['doomain projects list --json', 'doomain projects list --search my-app --json'],
214
+ safeForAgents: true,
215
+ flags: [
216
+ { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
217
+ { name: 'search', type: 'string', description: 'Filter projects by search term.' },
218
+ ],
219
+ },
220
+ {
221
+ name: 'verify',
222
+ description: 'Ask Vercel to verify a project domain.',
223
+ examples: ['doomain verify --domain app.example.com --project my-app --json', 'doomain verify --domain example.com --apex --project my-app --json'],
224
+ mutates: true,
225
+ safeForAgents: true,
226
+ flags: [
227
+ { name: 'json', type: 'boolean', description: 'Output a single JSON object and never prompt.' },
228
+ { name: 'domain', type: 'string', description: 'Target domain or base zone, for example app.example.com or example.com.', required: true },
229
+ { name: 'subdomain', type: 'string', description: 'Subdomain to verify.' },
230
+ { name: 'apex', type: 'boolean', description: 'Use the root/apex domain.' },
231
+ { name: 'project', type: 'string', description: 'Vercel project id/name. Optional when local .vercel/project.json is available.' },
232
+ ],
233
+ },
126
234
  ];
127
235
  export function getCommandSchema(name) {
128
236
  if (!name)
@@ -141,7 +249,7 @@ async function configuredProviders() {
141
249
  }));
142
250
  }
143
251
  function withProviderConnections(schema, providers) {
144
- if (schema.name !== 'link')
252
+ if (schema.name !== 'link' && schema.name !== 'clerk domains add')
145
253
  return schema;
146
254
  return { ...schema, configuredProviders: providers };
147
255
  }
@@ -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;
@@ -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_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,21 @@ export interface LinkDomainResult extends LinkDomainPlan {
59
59
  verified: boolean;
60
60
  };
61
61
  }
62
+ export interface ResolvedDnsTarget {
63
+ provider: string;
64
+ providerInferred: boolean;
65
+ account: string;
66
+ accountInferred: boolean;
67
+ isDefaultAccount: boolean;
68
+ target: {
69
+ fullDomain: string;
70
+ isApex: boolean;
71
+ recordName: string;
72
+ zoneDomain: string;
73
+ };
74
+ }
75
+ export declare function resolveProviderTarget(input: Pick<LinkDomainInput, 'account' | 'apex' | 'domain' | 'provider' | 'subdomain'>): Promise<ResolvedDnsTarget>;
76
+ export declare function withProviderRecordOptions(provider: string, record: DnsRecordInput): DnsRecordInput;
62
77
  export declare function verificationRecords(raw: unknown, zoneDomain: string): DnsRecordInput[];
63
78
  export declare function createLinkPlan(input: LinkDomainInput): Promise<LinkDomainPlan>;
64
79
  export declare function linkDomain(input: LinkDomainInput): Promise<LinkDomainResult>;
@@ -248,7 +248,7 @@ async function loadConfiguredProviderZones(providerId, accountInput) {
248
248
  searched: results.map((result) => result.search),
249
249
  };
250
250
  }
251
- async function resolveProviderTarget(input) {
251
+ export async function resolveProviderTarget(input) {
252
252
  const requested = resolveRequestedDomain({
253
253
  apex: input.apex,
254
254
  domain: await resolveConfiguredDomain(input.domain),
@@ -288,7 +288,7 @@ async function resolveProviderTarget(input) {
288
288
  target: targetFromZone(requested.fullDomain, selected.zone.name),
289
289
  };
290
290
  }
291
- function withProviderRecordOptions(provider, record) {
291
+ export function withProviderRecordOptions(provider, record) {
292
292
  if (provider !== 'cloudflare' || !['A', 'AAAA', 'CNAME'].includes(record.type))
293
293
  return record;
294
294
  return { ...record, proxied: false };
@@ -495,8 +495,9 @@ async function resolveDnsForce(input, opts) {
495
495
  return true;
496
496
  }
497
497
  export async function createLinkPlan(input) {
498
+ const domain = await resolveConfiguredDomain(input.domain);
498
499
  const project = await resolveProject(input.project);
499
- const resolved = await resolveProviderTarget(input);
500
+ const resolved = await resolveProviderTarget({ ...input, domain });
500
501
  const { account, accountInferred, isDefaultAccount, provider, providerInferred, target } = resolved;
501
502
  const record = planBaseRecord({ isApex: target.isApex, provider, recordName: target.recordName });
502
503
  return {
@@ -225,6 +225,51 @@
225
225
  "wizard.js"
226
226
  ]
227
227
  },
228
+ "auth:clerk": {
229
+ "aliases": [],
230
+ "args": {},
231
+ "description": "Save Clerk Platform API credentials locally.",
232
+ "examples": [
233
+ "<%= config.bin %> <%= command.id %> --platform-api-key ak_123 --app app_123 --json"
234
+ ],
235
+ "flags": {
236
+ "app": {
237
+ "description": "Default Clerk application id.",
238
+ "name": "app",
239
+ "hasDynamicHelp": false,
240
+ "multiple": false,
241
+ "type": "option"
242
+ },
243
+ "json": {
244
+ "description": "Output a single JSON object and never prompt.",
245
+ "name": "json",
246
+ "allowNo": false,
247
+ "type": "boolean"
248
+ },
249
+ "platform-api-key": {
250
+ "description": "Clerk Platform API key (ak_...).",
251
+ "name": "platform-api-key",
252
+ "hasDynamicHelp": false,
253
+ "multiple": false,
254
+ "type": "option"
255
+ }
256
+ },
257
+ "hasDynamicHelp": false,
258
+ "hiddenAliases": [],
259
+ "id": "auth:clerk",
260
+ "pluginAlias": "doomain",
261
+ "pluginName": "doomain",
262
+ "pluginType": "core",
263
+ "strict": true,
264
+ "enableJsonFlag": false,
265
+ "isESM": true,
266
+ "relativePath": [
267
+ "dist",
268
+ "commands",
269
+ "auth",
270
+ "clerk.js"
271
+ ]
272
+ },
228
273
  "auth:vercel": {
229
274
  "aliases": [],
230
275
  "args": {},
@@ -316,6 +361,41 @@
316
361
  "list.js"
317
362
  ]
318
363
  },
364
+ "projects:list": {
365
+ "aliases": [],
366
+ "args": {},
367
+ "description": "List Vercel projects.",
368
+ "flags": {
369
+ "json": {
370
+ "description": "Output a single JSON object and never prompt.",
371
+ "name": "json",
372
+ "allowNo": false,
373
+ "type": "boolean"
374
+ },
375
+ "search": {
376
+ "description": "Filter projects by search term.",
377
+ "name": "search",
378
+ "hasDynamicHelp": false,
379
+ "multiple": false,
380
+ "type": "option"
381
+ }
382
+ },
383
+ "hasDynamicHelp": false,
384
+ "hiddenAliases": [],
385
+ "id": "projects:list",
386
+ "pluginAlias": "doomain",
387
+ "pluginName": "doomain",
388
+ "pluginType": "core",
389
+ "strict": true,
390
+ "enableJsonFlag": false,
391
+ "isESM": true,
392
+ "relativePath": [
393
+ "dist",
394
+ "commands",
395
+ "projects",
396
+ "list.js"
397
+ ]
398
+ },
319
399
  "providers:add": {
320
400
  "aliases": [],
321
401
  "args": {
@@ -604,28 +684,21 @@
604
684
  "verify.js"
605
685
  ]
606
686
  },
607
- "projects:list": {
687
+ "auth:logout:clerk": {
608
688
  "aliases": [],
609
689
  "args": {},
610
- "description": "List Vercel projects.",
690
+ "description": "Remove saved Clerk credentials locally.",
611
691
  "flags": {
612
692
  "json": {
613
693
  "description": "Output a single JSON object and never prompt.",
614
694
  "name": "json",
615
695
  "allowNo": false,
616
696
  "type": "boolean"
617
- },
618
- "search": {
619
- "description": "Filter projects by search term.",
620
- "name": "search",
621
- "hasDynamicHelp": false,
622
- "multiple": false,
623
- "type": "option"
624
697
  }
625
698
  },
626
699
  "hasDynamicHelp": false,
627
700
  "hiddenAliases": [],
628
- "id": "projects:list",
701
+ "id": "auth:logout:clerk",
629
702
  "pluginAlias": "doomain",
630
703
  "pluginName": "doomain",
631
704
  "pluginType": "core",
@@ -635,8 +708,9 @@
635
708
  "relativePath": [
636
709
  "dist",
637
710
  "commands",
638
- "projects",
639
- "list.js"
711
+ "auth",
712
+ "logout",
713
+ "clerk.js"
640
714
  ]
641
715
  },
642
716
  "auth:logout:vercel": {
@@ -671,7 +745,94 @@
671
745
  "logout",
672
746
  "vercel.js"
673
747
  ]
748
+ },
749
+ "clerk:domains:add": {
750
+ "aliases": [],
751
+ "args": {
752
+ "domain": {
753
+ "description": "Production primary domain, for example example.com.",
754
+ "name": "domain",
755
+ "required": true
756
+ }
757
+ },
758
+ "description": "Create a Clerk production instance with its primary domain and configure DNS.",
759
+ "examples": [
760
+ "<%= config.bin %> <%= command.id %> example.com --app app_123 --json",
761
+ "<%= config.bin %> <%= command.id %> example.com --app app_123 --provider cloudflare --no-wait",
762
+ "<%= config.bin %> <%= command.id %> example.com --app app_123 --dry-run --json"
763
+ ],
764
+ "flags": {
765
+ "account": {
766
+ "description": "DNS provider profile/account alias. Defaults to the provider default account.",
767
+ "name": "account",
768
+ "hasDynamicHelp": false,
769
+ "multiple": false,
770
+ "type": "option"
771
+ },
772
+ "app": {
773
+ "description": "Clerk application id. Defaults to CLERK_APPLICATION_ID or saved Clerk config.",
774
+ "name": "app",
775
+ "hasDynamicHelp": false,
776
+ "multiple": false,
777
+ "type": "option"
778
+ },
779
+ "dry-run": {
780
+ "description": "Check the Clerk application and DNS zone without creating the production instance.",
781
+ "name": "dry-run",
782
+ "allowNo": false,
783
+ "type": "boolean"
784
+ },
785
+ "force": {
786
+ "description": "Overwrite DNS records that conflict with Clerk requirements.",
787
+ "name": "force",
788
+ "allowNo": false,
789
+ "type": "boolean"
790
+ },
791
+ "json": {
792
+ "description": "Output a single JSON object and never prompt.",
793
+ "name": "json",
794
+ "allowNo": false,
795
+ "type": "boolean"
796
+ },
797
+ "provider": {
798
+ "description": "DNS provider id. Inferred from the target domain when omitted.",
799
+ "name": "provider",
800
+ "hasDynamicHelp": false,
801
+ "multiple": false,
802
+ "type": "option"
803
+ },
804
+ "timeout": {
805
+ "description": "Wait timeout in seconds.",
806
+ "name": "timeout",
807
+ "default": 300,
808
+ "hasDynamicHelp": false,
809
+ "multiple": false,
810
+ "type": "option"
811
+ },
812
+ "wait": {
813
+ "description": "Wait for Clerk DNS, SSL, and email DNS verification.",
814
+ "name": "wait",
815
+ "allowNo": true,
816
+ "type": "boolean"
817
+ }
818
+ },
819
+ "hasDynamicHelp": false,
820
+ "hiddenAliases": [],
821
+ "id": "clerk:domains:add",
822
+ "pluginAlias": "doomain",
823
+ "pluginName": "doomain",
824
+ "pluginType": "core",
825
+ "strict": true,
826
+ "enableJsonFlag": false,
827
+ "isESM": true,
828
+ "relativePath": [
829
+ "dist",
830
+ "commands",
831
+ "clerk",
832
+ "domains",
833
+ "add.js"
834
+ ]
674
835
  }
675
836
  },
676
- "version": "0.1.14"
837
+ "version": "0.1.16"
677
838
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "doomain",
3
- "description": "Link your vercel project and domain in seconds",
4
- "version": "0.1.14",
3
+ "description": "Configure Vercel and Clerk production domains in seconds",
4
+ "version": "0.1.16",
5
5
  "author": "Crafter Station",
6
6
  "packageManager": "bun@1.3.13",
7
7
  "bin": {
@@ -39,7 +39,8 @@
39
39
  "keywords": [
40
40
  "dns",
41
41
  "domains",
42
- "vercel"
42
+ "vercel",
43
+ "clerk"
43
44
  ],
44
45
  "license": "MIT",
45
46
  "main": "dist/index.js",
@@ -56,6 +57,9 @@
56
57
  "auth": {
57
58
  "description": "Manage authentication"
58
59
  },
60
+ "clerk": {
61
+ "description": "Configure Clerk production domains"
62
+ },
59
63
  "domains": {
60
64
  "description": "Inspect DNS domains"
61
65
  },