doomain 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -2
- package/dist/commands/auth/clerk.d.ts +11 -0
- package/dist/commands/auth/clerk.js +55 -0
- package/dist/commands/auth/logout/clerk.d.ts +8 -0
- package/dist/commands/auth/logout/clerk.js +29 -0
- package/dist/commands/clerk/domains/add.d.ts +19 -0
- package/dist/commands/clerk/domains/add.js +92 -0
- package/dist/commands/domains/find.d.ts +15 -0
- package/dist/commands/domains/find.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/clerk-domain.d.ts +39 -0
- package/dist/lib/clerk-domain.js +163 -0
- package/dist/lib/clerk.d.ts +56 -0
- package/dist/lib/clerk.js +65 -0
- package/dist/lib/command-schema.js +64 -2
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/domain-provider.d.ts +53 -0
- package/dist/lib/domain-provider.js +230 -0
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/link-domain.d.ts +1 -0
- package/dist/lib/link-domain.js +3 -177
- package/oclif.manifest.json +222 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Doomain
|
|
2
2
|
|
|
3
|
-
Doomain links
|
|
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
|
|
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:
|
|
@@ -304,8 +337,10 @@ Useful agent-safe commands:
|
|
|
304
337
|
doomain link app.example.com --project my-app --json
|
|
305
338
|
doomain providers list --json
|
|
306
339
|
doomain providers status --no-verify --json
|
|
340
|
+
doomain domains find hacktheandes.com --json
|
|
307
341
|
doomain domains list --provider cloudflare --domain example.com --json
|
|
308
342
|
doomain projects list --search my-app --json
|
|
343
|
+
doomain clerk domains add example.com --app app_123 --json
|
|
309
344
|
doomain schema --json
|
|
310
345
|
doomain schema link --json
|
|
311
346
|
```
|
|
@@ -317,6 +352,19 @@ doomain schema --json
|
|
|
317
352
|
doomain schema "providers connect" --json
|
|
318
353
|
```
|
|
319
354
|
|
|
355
|
+
## Programmatic API
|
|
356
|
+
|
|
357
|
+
Use `findDomainProvider` to perform the same discovery from TypeScript or JavaScript:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import {findDomainProvider} from 'doomain'
|
|
361
|
+
|
|
362
|
+
const match = await findDomainProvider({domain: 'api.hacktheandes.com'})
|
|
363
|
+
console.log(match.provider, match.account, match.zoneDomain)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The API checks configured provider accounts, tolerates failures from individual providers, and returns the longest matching DNS zone. Pass `provider` or `account` to constrain the search. When a provider cannot be checked, `complete` is `false` and `warnings` identifies the affected provider account.
|
|
367
|
+
|
|
320
368
|
## Command Reference
|
|
321
369
|
|
|
322
370
|
Run `doomain help <command>` for oclif-generated help.
|
|
@@ -376,6 +424,28 @@ doomain auth logout vercel --json
|
|
|
376
424
|
|
|
377
425
|
If `VERCEL_TOKEN` or `VERCEL_TEAM_ID` are still set, they continue to override local config.
|
|
378
426
|
|
|
427
|
+
### `doomain auth clerk`
|
|
428
|
+
|
|
429
|
+
Saves and verifies a Clerk Platform API key and default application id.
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
doomain auth clerk --platform-api-key ak_123 --app app_123
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### `doomain auth logout clerk`
|
|
436
|
+
|
|
437
|
+
Removes saved Clerk credentials. `CLERK_PLATFORM_API_KEY` and `CLERK_APPLICATION_ID` continue to override local config when set.
|
|
438
|
+
|
|
439
|
+
### `doomain clerk domains add <domain>`
|
|
440
|
+
|
|
441
|
+
Creates the application's first Clerk production instance and primary domain, then configures Clerk's returned CNAME records.
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
doomain clerk domains add example.com --app app_123
|
|
445
|
+
doomain clerk domains add example.com --app app_123 --dry-run --json
|
|
446
|
+
doomain clerk domains add example.com --app app_123 --no-wait --json
|
|
447
|
+
```
|
|
448
|
+
|
|
379
449
|
### `doomain providers list`
|
|
380
450
|
|
|
381
451
|
Lists supported DNS providers.
|
|
@@ -441,6 +511,18 @@ doomain providers logout namecheap --json
|
|
|
441
511
|
|
|
442
512
|
Environment variables for that provider still override local config after disconnect.
|
|
443
513
|
|
|
514
|
+
### `doomain domains find [domain]`
|
|
515
|
+
|
|
516
|
+
Finds the configured DNS provider account with the longest matching zone. It checks all configured accounts and continues when an individual provider fails, so one expired credential does not hide a match from another provider.
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
doomain domains find hacktheandes.com --json
|
|
520
|
+
doomain domains find api.example.com --json
|
|
521
|
+
doomain domains find --domain example.com --provider spaceship --account personal --json
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Successful JSON includes the provider, account, matching zone, and relative DNS record name. Check `complete` and `warnings` before treating the result as exhaustive; for example, an expired token may prevent one provider from participating in discovery.
|
|
525
|
+
|
|
444
526
|
### `doomain domains list`
|
|
445
527
|
|
|
446
528
|
Lists DNS zones and records.
|
|
@@ -491,6 +573,13 @@ VERCEL_TOKEN
|
|
|
491
573
|
VERCEL_TEAM_ID
|
|
492
574
|
```
|
|
493
575
|
|
|
576
|
+
Clerk:
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
CLERK_PLATFORM_API_KEY
|
|
580
|
+
CLERK_APPLICATION_ID
|
|
581
|
+
```
|
|
582
|
+
|
|
494
583
|
Spaceship:
|
|
495
584
|
|
|
496
585
|
```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,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,15 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class DomainsFind extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
domain: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
account: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
provider: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { findDomainProvider } from '../../lib/domain-provider.js';
|
|
3
|
+
import { DoomainError } from '../../lib/errors.js';
|
|
4
|
+
import { accountFlag, domainFlag, jsonFlag, providerFlag } from '../../lib/flags.js';
|
|
5
|
+
import { createOutput, outputError } from '../../lib/output.js';
|
|
6
|
+
export default class DomainsFind extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
domain: Args.string({ description: 'Domain whose DNS provider should be found.', required: false }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Find the configured DNS provider and account for a domain.';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> <%= command.id %> example.com --json',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> api.example.com --json',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --domain example.com --provider spaceship --json',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
account: accountFlag,
|
|
18
|
+
domain: domainFlag,
|
|
19
|
+
json: jsonFlag,
|
|
20
|
+
provider: providerFlag,
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(DomainsFind);
|
|
24
|
+
const out = createOutput({ json: flags.json });
|
|
25
|
+
try {
|
|
26
|
+
const domain = flags.domain ?? args.domain;
|
|
27
|
+
if (!domain)
|
|
28
|
+
throw new DoomainError('MISSING_ARGUMENT', 'Domain is required. Pass it as an argument or use --domain.');
|
|
29
|
+
const result = await findDomainProvider({ account: flags.account, domain, provider: flags.provider });
|
|
30
|
+
const account = result.isDefaultAccount ? result.provider : `${result.provider}/${result.account}`;
|
|
31
|
+
out.info(`${result.domain} is managed by ${account} in DNS zone ${result.zoneDomain}.`);
|
|
32
|
+
for (const warning of result.warnings) {
|
|
33
|
+
const warningAccount = warning.isDefaultAccount ? warning.provider : `${warning.provider}/${warning.account}`;
|
|
34
|
+
out.warn(`${warningAccount} could not be checked: ${warning.error.message}`);
|
|
35
|
+
}
|
|
36
|
+
out.result(result);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
outputError(out.json, error, 'DOMAIN_PROVIDER_DISCOVERY_FAILED');
|
|
40
|
+
this.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -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,163 @@
|
|
|
1
|
+
import { createClerkPlatformClient, resolveClerkPlatformConfig } from './clerk.js';
|
|
2
|
+
import { resolveProviderTarget } from './domain-provider.js';
|
|
3
|
+
import { DoomainError } from './errors.js';
|
|
4
|
+
import { withProviderRecordOptions } from './link-domain.js';
|
|
5
|
+
import { createProvider } from './providers/registry.js';
|
|
6
|
+
import { normalizeDomain } from './validate.js';
|
|
7
|
+
function relativeRecordName(host, zoneDomain) {
|
|
8
|
+
const normalized = host.trim().toLowerCase().replace(/\.$/, '');
|
|
9
|
+
if (normalized === zoneDomain)
|
|
10
|
+
return '@';
|
|
11
|
+
if (!normalized.endsWith(`.${zoneDomain}`)) {
|
|
12
|
+
throw new DoomainError('INVALID_INPUT', `Clerk returned DNS host ${host}, which is outside the selected zone ${zoneDomain}.`);
|
|
13
|
+
}
|
|
14
|
+
return normalized.slice(0, -(zoneDomain.length + 1));
|
|
15
|
+
}
|
|
16
|
+
function recordsFromTargets(provider, zoneDomain, targets) {
|
|
17
|
+
return targets.map((target) => withProviderRecordOptions(provider, {
|
|
18
|
+
name: relativeRecordName(target.host, zoneDomain),
|
|
19
|
+
ttl: 300,
|
|
20
|
+
type: 'CNAME',
|
|
21
|
+
value: target.value.trim().replace(/\.$/, ''),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
function productionExistsError(app, instanceId) {
|
|
25
|
+
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.`, {
|
|
26
|
+
app,
|
|
27
|
+
instanceId,
|
|
28
|
+
recovery: 'Use the Clerk Dashboard Domains page or `clerk deploy` to inspect and configure the existing production instance.',
|
|
29
|
+
suggestedCommands: [`clerk link --app ${app}`, 'clerk deploy status', 'clerk open domains'],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function dnsConflictError(warning, productionInstanceId) {
|
|
33
|
+
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.`, {
|
|
34
|
+
...warning,
|
|
35
|
+
partialState: { productionInstanceCreated: true, productionInstanceId },
|
|
36
|
+
recovery: 'The Clerk production instance now exists. Resolve the DNS conflict, then finish setup with `clerk deploy`.',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function statusComplete(check) {
|
|
40
|
+
return check?.required === false || check?.status === 'complete';
|
|
41
|
+
}
|
|
42
|
+
async function waitForStatus(client, app, domainId, timeoutSeconds, progress) {
|
|
43
|
+
await client.triggerDomainDnsCheck(app, domainId).catch((error) => {
|
|
44
|
+
const conflict = error instanceof DoomainError && JSON.stringify(error.details).toLowerCase().includes('conflict');
|
|
45
|
+
if (!conflict)
|
|
46
|
+
throw error;
|
|
47
|
+
});
|
|
48
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
49
|
+
let status = { status: 'incomplete' };
|
|
50
|
+
let attempt = 1;
|
|
51
|
+
while (Date.now() <= deadline) {
|
|
52
|
+
progress?.(`Checking Clerk DNS, SSL, and email DNS status (attempt ${attempt})`);
|
|
53
|
+
status = await client.getDomainStatus(app, domainId);
|
|
54
|
+
if (status.status === 'complete')
|
|
55
|
+
return status;
|
|
56
|
+
attempt += 1;
|
|
57
|
+
if (Date.now() > deadline)
|
|
58
|
+
break;
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
60
|
+
}
|
|
61
|
+
return status;
|
|
62
|
+
}
|
|
63
|
+
export async function addClerkProductionDomain(input) {
|
|
64
|
+
const domain = normalizeDomain(input.domain ?? '');
|
|
65
|
+
const config = await resolveClerkPlatformConfig(input.app);
|
|
66
|
+
const client = createClerkPlatformClient(config);
|
|
67
|
+
input.progress?.('Checking Clerk application');
|
|
68
|
+
const application = await client.fetchApplication(config.appId);
|
|
69
|
+
const production = application.instances.find((instance) => instance.environment_type === 'production');
|
|
70
|
+
if (production)
|
|
71
|
+
throw productionExistsError(config.appId, production.instance_id);
|
|
72
|
+
const development = application.instances.find((instance) => instance.environment_type === 'development');
|
|
73
|
+
if (!development)
|
|
74
|
+
throw new DoomainError('PROJECT_NOT_FOUND', `Clerk application ${config.appId} does not have a development instance to clone.`);
|
|
75
|
+
input.progress?.('Finding the DNS provider and zone');
|
|
76
|
+
const resolved = await resolveProviderTarget({ account: input.account, domain, provider: input.provider });
|
|
77
|
+
const provider = await createProvider(resolved.provider, { account: resolved.account });
|
|
78
|
+
const zone = await provider.getZone(resolved.target.zoneDomain);
|
|
79
|
+
if (!zone)
|
|
80
|
+
throw new DoomainError('PROVIDER_ZONE_NOT_FOUND', `${provider.name} does not have a DNS zone for ${resolved.target.zoneDomain}.`);
|
|
81
|
+
const nextSteps = [
|
|
82
|
+
`clerk link --app ${config.appId}`,
|
|
83
|
+
`clerk env pull --app ${config.appId} --instance prod`,
|
|
84
|
+
'clerk deploy',
|
|
85
|
+
'clerk deploy status',
|
|
86
|
+
];
|
|
87
|
+
if (input.dryRun) {
|
|
88
|
+
return {
|
|
89
|
+
account: resolved.account,
|
|
90
|
+
app: config.appId,
|
|
91
|
+
clerk: { productionInstanceCreated: false, verified: false },
|
|
92
|
+
dns: { propagated: false, skipped: [], updated: false },
|
|
93
|
+
domain,
|
|
94
|
+
dryRun: true,
|
|
95
|
+
isDefaultAccount: resolved.isDefaultAccount,
|
|
96
|
+
nextSteps,
|
|
97
|
+
provider: resolved.provider,
|
|
98
|
+
records: [],
|
|
99
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
input.progress?.('Creating Clerk production instance and primary domain');
|
|
103
|
+
const created = await client.createProductionInstance(config.appId, domain, development.instance_id);
|
|
104
|
+
const clerkDomain = created.active_domain;
|
|
105
|
+
if (!clerkDomain) {
|
|
106
|
+
throw new DoomainError('DOMAIN_LINK_FAILED', 'Clerk created the production instance but did not return its primary domain.', {
|
|
107
|
+
productionInstanceId: created.id,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const records = recordsFromTargets(resolved.provider, resolved.target.zoneDomain, clerkDomain.cname_targets ?? []);
|
|
111
|
+
if (records.length === 0) {
|
|
112
|
+
throw new DoomainError('DOMAIN_LINK_FAILED', 'Clerk created the production instance but did not return any DNS records.', {
|
|
113
|
+
domainId: clerkDomain.id,
|
|
114
|
+
productionInstanceId: created.id,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
input.progress?.(`Checking existing DNS records in ${provider.name}`);
|
|
118
|
+
let forceDns = Boolean(input.force);
|
|
119
|
+
let dnsPlan = await provider.planChanges(zone, records, { force: forceDns });
|
|
120
|
+
if (!forceDns && dnsPlan.conflicts.length > 0) {
|
|
121
|
+
const warning = {
|
|
122
|
+
account: resolved.account,
|
|
123
|
+
conflicts: dnsPlan.conflicts,
|
|
124
|
+
desired: records,
|
|
125
|
+
domain,
|
|
126
|
+
provider: resolved.provider,
|
|
127
|
+
providerName: provider.name,
|
|
128
|
+
recordName: records[0]?.name ?? '@',
|
|
129
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
130
|
+
};
|
|
131
|
+
forceDns = (await input.confirmDnsOverride?.(warning)) === true;
|
|
132
|
+
if (!forceDns)
|
|
133
|
+
throw dnsConflictError(warning, created.id);
|
|
134
|
+
dnsPlan = await provider.planChanges(zone, records, { force: true });
|
|
135
|
+
}
|
|
136
|
+
input.progress?.(`Creating Clerk DNS records in ${provider.name}`);
|
|
137
|
+
const dnsResult = await provider.applyChanges(zone, dnsPlan, { force: forceDns });
|
|
138
|
+
const shouldWait = input.wait ?? true;
|
|
139
|
+
const status = shouldWait
|
|
140
|
+
? await waitForStatus(client, config.appId, clerkDomain.id, input.timeoutSeconds ?? 300, input.progress)
|
|
141
|
+
: undefined;
|
|
142
|
+
const verified = status?.status === 'complete';
|
|
143
|
+
const propagated = Boolean(status && statusComplete(status.dns) && statusComplete(status.mail));
|
|
144
|
+
return {
|
|
145
|
+
account: resolved.account,
|
|
146
|
+
app: config.appId,
|
|
147
|
+
clerk: {
|
|
148
|
+
domainId: clerkDomain.id,
|
|
149
|
+
productionInstanceCreated: true,
|
|
150
|
+
productionInstanceId: created.id,
|
|
151
|
+
status,
|
|
152
|
+
verified,
|
|
153
|
+
},
|
|
154
|
+
dns: { propagated, skipped: dnsResult.skipped, updated: dnsResult.applied.length > 0 },
|
|
155
|
+
domain,
|
|
156
|
+
dryRun: false,
|
|
157
|
+
isDefaultAccount: resolved.isDefaultAccount,
|
|
158
|
+
nextSteps,
|
|
159
|
+
provider: resolved.provider,
|
|
160
|
+
records,
|
|
161
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
162
|
+
};
|
|
163
|
+
}
|