doomain 0.1.15 → 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
+ }
@@ -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
+ }
@@ -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.',
@@ -204,7 +249,7 @@ async function configuredProviders() {
204
249
  }));
205
250
  }
206
251
  function withProviderConnections(schema, providers) {
207
- if (schema.name !== 'link')
252
+ if (schema.name !== 'link' && schema.name !== 'clerk domains add')
208
253
  return schema;
209
254
  return { ...schema, configuredProviders: providers };
210
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 };
@@ -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": {},
@@ -639,6 +684,35 @@
639
684
  "verify.js"
640
685
  ]
641
686
  },
687
+ "auth:logout:clerk": {
688
+ "aliases": [],
689
+ "args": {},
690
+ "description": "Remove saved Clerk credentials locally.",
691
+ "flags": {
692
+ "json": {
693
+ "description": "Output a single JSON object and never prompt.",
694
+ "name": "json",
695
+ "allowNo": false,
696
+ "type": "boolean"
697
+ }
698
+ },
699
+ "hasDynamicHelp": false,
700
+ "hiddenAliases": [],
701
+ "id": "auth:logout:clerk",
702
+ "pluginAlias": "doomain",
703
+ "pluginName": "doomain",
704
+ "pluginType": "core",
705
+ "strict": true,
706
+ "enableJsonFlag": false,
707
+ "isESM": true,
708
+ "relativePath": [
709
+ "dist",
710
+ "commands",
711
+ "auth",
712
+ "logout",
713
+ "clerk.js"
714
+ ]
715
+ },
642
716
  "auth:logout:vercel": {
643
717
  "aliases": [],
644
718
  "args": {},
@@ -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.15"
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.15",
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
  },