doomain 0.1.23 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/auth/clerk.js +3 -2
- package/dist/commands/auth/logout/clerk.js +3 -2
- package/dist/commands/auth/logout/vercel.js +3 -2
- package/dist/commands/auth/vercel.js +5 -4
- package/dist/commands/clerk/domains/add.js +3 -2
- package/dist/commands/dns/diagnose.js +3 -2
- package/dist/commands/dns/point.js +7 -3
- package/dist/commands/dns/remove.js +3 -2
- package/dist/commands/domains/find.js +3 -2
- package/dist/commands/domains/list.js +46 -33
- package/dist/commands/link.js +4 -3
- package/dist/commands/projects/list.js +5 -2
- package/dist/commands/providers/connect.js +14 -23
- package/dist/commands/providers/disconnect.js +3 -2
- package/dist/commands/providers/status.js +2 -1
- package/dist/commands/providers/verify.js +3 -2
- package/dist/commands/schema.js +2 -1
- package/dist/commands/update.js +2 -1
- package/dist/commands/verify.js +5 -2
- package/dist/commands/wizard.js +45 -46
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/clerk-domain.d.ts +2 -1
- package/dist/lib/clerk-domain.js +122 -110
- package/dist/lib/clerk.d.ts +9 -5
- package/dist/lib/clerk.js +45 -36
- package/dist/lib/command-schema.d.ts +2 -1
- package/dist/lib/command-schema.js +9 -9
- package/dist/lib/config.d.ts +6 -4
- package/dist/lib/config.js +29 -27
- package/dist/lib/diagnose-dns.d.ts +8 -5
- package/dist/lib/diagnose-dns.js +90 -92
- package/dist/lib/dns-propagation.d.ts +5 -4
- package/dist/lib/dns-propagation.js +80 -54
- package/dist/lib/dns-reconciliation.d.ts +4 -3
- package/dist/lib/dns-reconciliation.js +52 -47
- package/dist/lib/domain-provider.d.ts +5 -1
- package/dist/lib/domain-provider.js +136 -131
- package/dist/lib/effect.d.ts +6 -0
- package/dist/lib/effect.js +20 -0
- package/dist/lib/link-domain.d.ts +3 -2
- package/dist/lib/link-domain.js +239 -218
- package/dist/lib/point-domain.d.ts +7 -4
- package/dist/lib/point-domain.js +92 -90
- package/dist/lib/providers/cloudflare/index.d.ts +9 -8
- package/dist/lib/providers/cloudflare/index.js +93 -87
- package/dist/lib/providers/core/config.d.ts +4 -1
- package/dist/lib/providers/core/config.js +17 -13
- package/dist/lib/providers/core/http.d.ts +4 -1
- package/dist/lib/providers/core/http.js +28 -18
- package/dist/lib/providers/core/pagination.d.ts +3 -2
- package/dist/lib/providers/core/pagination.js +17 -14
- package/dist/lib/providers/core/planner.d.ts +5 -4
- package/dist/lib/providers/core/planner.js +18 -15
- package/dist/lib/providers/core/types.d.ts +11 -8
- package/dist/lib/providers/hostinger/index.d.ts +9 -8
- package/dist/lib/providers/hostinger/index.js +74 -72
- package/dist/lib/providers/namecheap/index.d.ts +10 -8
- package/dist/lib/providers/namecheap/index.js +134 -109
- package/dist/lib/providers/registry.d.ts +3 -3
- package/dist/lib/providers/registry.js +7 -3
- package/dist/lib/providers/spaceship/index.d.ts +9 -8
- package/dist/lib/providers/spaceship/index.js +48 -47
- package/dist/lib/providers/status.d.ts +2 -1
- package/dist/lib/providers/status.js +41 -34
- package/dist/lib/public-ip.d.ts +2 -0
- package/dist/lib/public-ip.js +14 -0
- package/dist/lib/remove-domain.d.ts +6 -3
- package/dist/lib/remove-domain.js +83 -77
- package/dist/lib/self-update.d.ts +3 -2
- package/dist/lib/self-update.js +15 -23
- package/dist/lib/vercel-auth.d.ts +2 -1
- package/dist/lib/vercel-auth.js +21 -24
- package/dist/lib/vercel.d.ts +16 -12
- package/dist/lib/vercel.js +174 -114
- package/oclif.manifest.json +1 -1
- package/package.json +2 -1
package/dist/commands/wizard.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import { Command } from '@oclif/core';
|
|
3
|
+
import { Effect } from 'effect';
|
|
3
4
|
import { loadConfig, maskSecret, updateConfig } from '../lib/config.js';
|
|
5
|
+
import { runDoomainEffect } from '../lib/effect.js';
|
|
4
6
|
import { DoomainError } from '../lib/errors.js';
|
|
5
7
|
import { jsonFlag } from '../lib/flags.js';
|
|
6
8
|
import { createLinkPlan, linkDomain } from '../lib/link-domain.js';
|
|
@@ -8,6 +10,7 @@ import { detectLocalVercelProject } from '../lib/local-vercel.js';
|
|
|
8
10
|
import { createOutput, outputError } from '../lib/output.js';
|
|
9
11
|
import { DEFAULT_PROVIDER_ACCOUNT, isDefaultProviderAccount, listConfiguredProviderAccounts, normalizeProviderAccount, providerAccountHasCredentials, withProviderAccountCredentials, } from '../lib/providers/core/config.js';
|
|
10
12
|
import { createProvider, getProviderDefinition, listProviderDefinitions } from '../lib/providers/registry.js';
|
|
13
|
+
import { fetchPublicIp } from '../lib/public-ip.js';
|
|
11
14
|
import { createVercelClient } from '../lib/vercel.js';
|
|
12
15
|
import { listGlobalVercelTokens } from '../lib/vercel-auth.js';
|
|
13
16
|
const PERSONAL_ACCOUNT = '__personal__';
|
|
@@ -56,42 +59,28 @@ async function confirmProviderAccountOverwrite(definition, account) {
|
|
|
56
59
|
const resolved = cancelIfNeeded(value);
|
|
57
60
|
return resolved === true;
|
|
58
61
|
}
|
|
59
|
-
|
|
60
|
-
const controller = new AbortController();
|
|
61
|
-
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
62
|
-
try {
|
|
63
|
-
const response = await fetch('https://api.ipify.org', { signal: controller.signal });
|
|
64
|
-
if (!response.ok)
|
|
65
|
-
return undefined;
|
|
66
|
-
const ip = (await response.text()).trim();
|
|
67
|
-
return ip || undefined;
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
finally {
|
|
73
|
-
clearTimeout(timeout);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
async function credentialInitialValue(credential) {
|
|
62
|
+
function credentialInitialValue(credential, detectedPublicIp) {
|
|
77
63
|
if (credential.key !== 'clientIp')
|
|
78
64
|
return undefined;
|
|
79
|
-
return
|
|
65
|
+
return detectedPublicIp;
|
|
80
66
|
}
|
|
81
|
-
|
|
82
|
-
|
|
67
|
+
function usesClientIp(definition) {
|
|
68
|
+
return definition.credentials.some((credential) => credential.key === 'clientIp');
|
|
69
|
+
}
|
|
70
|
+
async function promptCredential(credential, detectedPublicIp) {
|
|
71
|
+
const initialValue = credential.secret ? undefined : credentialInitialValue(credential, detectedPublicIp);
|
|
83
72
|
const value = credential.secret
|
|
84
73
|
? await p.password({ message: credential.label })
|
|
85
74
|
: await p.text({ message: credential.label, initialValue, placeholder: credential.placeholder ?? credential.hint });
|
|
86
75
|
const resolved = cancelIfNeeded(value);
|
|
87
76
|
return typeof resolved === 'string' && resolved.trim() ? resolved.trim() : null;
|
|
88
77
|
}
|
|
89
|
-
async function promptProviderCredentials(definition) {
|
|
78
|
+
async function promptProviderCredentials(definition, detectedPublicIp) {
|
|
90
79
|
const credentials = {};
|
|
91
80
|
for (const credential of definition.credentials) {
|
|
92
81
|
if (credential.required === false)
|
|
93
82
|
continue;
|
|
94
|
-
const value = await promptCredential(credential);
|
|
83
|
+
const value = await promptCredential(credential, detectedPublicIp);
|
|
95
84
|
if (!value)
|
|
96
85
|
return null;
|
|
97
86
|
credentials[credential.key] = value;
|
|
@@ -117,10 +106,15 @@ function showProviderSetup(definition) {
|
|
|
117
106
|
return;
|
|
118
107
|
p.note(definition.setup.notes.join('\n'), `${definition.displayName} setup`);
|
|
119
108
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
109
|
+
function listProviderDomainOptions(definition, account) {
|
|
110
|
+
return Effect.gen(function* () {
|
|
111
|
+
const provider = yield* createProvider(definition.id, {
|
|
112
|
+
account: account.account,
|
|
113
|
+
transportErrorCode: 'DOMAIN_LINK_FAILED',
|
|
114
|
+
});
|
|
115
|
+
const zones = yield* provider.listZones();
|
|
116
|
+
return zones.map((zone) => toProviderDomainOption(definition, account, zone));
|
|
117
|
+
});
|
|
124
118
|
}
|
|
125
119
|
function toProviderDomainOption(definition, account, zone) {
|
|
126
120
|
return {
|
|
@@ -211,7 +205,7 @@ export default class Wizard extends Command {
|
|
|
211
205
|
}
|
|
212
206
|
try {
|
|
213
207
|
p.intro('Doomain');
|
|
214
|
-
const config = await loadConfig();
|
|
208
|
+
const config = await runDoomainEffect(loadConfig());
|
|
215
209
|
const providerDefinitions = listProviderDefinitions();
|
|
216
210
|
const localProject = detectLocalVercelProject();
|
|
217
211
|
let vercelToken = config.vercel?.token;
|
|
@@ -220,7 +214,7 @@ export default class Wizard extends Command {
|
|
|
220
214
|
const defaultDomain = process.env.DOOMAIN_DOMAIN || config.defaults?.domain;
|
|
221
215
|
let globalVercelTokens = [];
|
|
222
216
|
if (!vercelToken) {
|
|
223
|
-
globalVercelTokens = await listGlobalVercelTokens();
|
|
217
|
+
globalVercelTokens = await runDoomainEffect(listGlobalVercelTokens());
|
|
224
218
|
vercelToken = (await promptVercelToken(globalVercelTokens)) ?? undefined;
|
|
225
219
|
if (!vercelToken)
|
|
226
220
|
return;
|
|
@@ -235,7 +229,7 @@ export default class Wizard extends Command {
|
|
|
235
229
|
else {
|
|
236
230
|
activeSpinner = teamSpinner;
|
|
237
231
|
teamSpinner.start('Loading Vercel teams');
|
|
238
|
-
teams = await createVercelClient({ token: vercelToken }).listTeams();
|
|
232
|
+
teams = await runDoomainEffect(createVercelClient({ token: vercelToken }).listTeams());
|
|
239
233
|
teamSpinner.stop(`Loaded ${teams.length} Vercel team${teams.length === 1 ? '' : 's'}`);
|
|
240
234
|
activeSpinner = undefined;
|
|
241
235
|
const selected = await p.select({
|
|
@@ -260,7 +254,7 @@ export default class Wizard extends Command {
|
|
|
260
254
|
activeSpinner = undefined;
|
|
261
255
|
p.log.warning(error instanceof Error ? error.message : String(error));
|
|
262
256
|
if (globalVercelTokens.length === 0)
|
|
263
|
-
globalVercelTokens = await listGlobalVercelTokens();
|
|
257
|
+
globalVercelTokens = await runDoomainEffect(listGlobalVercelTokens());
|
|
264
258
|
globalVercelTokens = globalVercelTokens.filter((token) => token.token !== vercelToken);
|
|
265
259
|
vercelToken = (await promptVercelToken(globalVercelTokens)) ?? undefined;
|
|
266
260
|
if (!vercelToken)
|
|
@@ -275,7 +269,7 @@ export default class Wizard extends Command {
|
|
|
275
269
|
const projectSpinner = p.spinner();
|
|
276
270
|
activeSpinner = projectSpinner;
|
|
277
271
|
projectSpinner.start('Loading Vercel projects');
|
|
278
|
-
const projects = await createVercelClient({ token: vercelToken, teamId: vercelTeamId }).listProjects();
|
|
272
|
+
const projects = await runDoomainEffect(createVercelClient({ token: vercelToken, teamId: vercelTeamId }).listProjects());
|
|
279
273
|
projectSpinner.stop(`Loaded ${projects.length} projects`);
|
|
280
274
|
activeSpinner = undefined;
|
|
281
275
|
if (projects.length === 0) {
|
|
@@ -324,18 +318,23 @@ export default class Wizard extends Command {
|
|
|
324
318
|
isDefaultAccount: isDefaultProviderAccount(account),
|
|
325
319
|
providerId: selectedDefinition.id,
|
|
326
320
|
};
|
|
327
|
-
const
|
|
321
|
+
const detectedPublicIp = usesClientIp(selectedDefinition) ? await runDoomainEffect(fetchPublicIp()) : undefined;
|
|
322
|
+
const credentials = await promptProviderCredentials(selectedDefinition, detectedPublicIp);
|
|
328
323
|
if (!credentials)
|
|
329
324
|
return;
|
|
330
325
|
const domainSpinner = p.spinner();
|
|
331
326
|
activeSpinner = domainSpinner;
|
|
332
327
|
domainSpinner.start(`Verifying ${selectedDefinition.displayName} credentials and loading domains`);
|
|
333
|
-
const provider = selectedDefinition.create({
|
|
334
|
-
|
|
328
|
+
const provider = selectedDefinition.create({
|
|
329
|
+
credentials,
|
|
330
|
+
debug: process.env.DOOMAIN_DEBUG === '1',
|
|
331
|
+
transportErrorCode: 'DOMAIN_LINK_FAILED',
|
|
332
|
+
});
|
|
333
|
+
const zones = await runDoomainEffect(provider.listZones());
|
|
335
334
|
domainSpinner.stop(`Connected ${selectedDefinition.displayName} and loaded ${zones.length} domain${zones.length === 1 ? '' : 's'}`);
|
|
336
335
|
activeSpinner = undefined;
|
|
337
336
|
domainOptions.push(...zones.map((zone) => toProviderDomainOption(selectedDefinition, providerAccount, zone)));
|
|
338
|
-
await updateConfig((current) => ({
|
|
337
|
+
await runDoomainEffect(updateConfig((current) => ({
|
|
339
338
|
...current,
|
|
340
339
|
defaults: { ...current.defaults, provider: selectedDefinition.id },
|
|
341
340
|
providers: {
|
|
@@ -343,7 +342,7 @@ export default class Wizard extends Command {
|
|
|
343
342
|
[selectedDefinition.id]: withProviderAccountCredentials(current.providers?.[selectedDefinition.id], account, credentials),
|
|
344
343
|
},
|
|
345
344
|
vercel: { token: vercelToken, teamId: vercelTeamId },
|
|
346
|
-
}));
|
|
345
|
+
}), 'DOMAIN_LINK_FAILED'));
|
|
347
346
|
}
|
|
348
347
|
else {
|
|
349
348
|
const domainSpinner = p.spinner();
|
|
@@ -351,7 +350,7 @@ export default class Wizard extends Command {
|
|
|
351
350
|
domainSpinner.start(`Loading domains from ${configuredProviderAccounts.length} provider account${configuredProviderAccounts.length === 1 ? '' : 's'}`);
|
|
352
351
|
for (const { account, definition } of configuredProviderAccounts) {
|
|
353
352
|
try {
|
|
354
|
-
domainOptions.push(...(await listProviderDomainOptions(definition, account)));
|
|
353
|
+
domainOptions.push(...(await runDoomainEffect(listProviderDomainOptions(definition, account))));
|
|
355
354
|
}
|
|
356
355
|
catch (error) {
|
|
357
356
|
const label = account.isDefaultAccount
|
|
@@ -364,10 +363,10 @@ export default class Wizard extends Command {
|
|
|
364
363
|
activeSpinner = undefined;
|
|
365
364
|
for (const failure of providerFailures)
|
|
366
365
|
p.log.warning(failure);
|
|
367
|
-
await updateConfig((current) => ({
|
|
366
|
+
await runDoomainEffect(updateConfig((current) => ({
|
|
368
367
|
...current,
|
|
369
368
|
vercel: { token: vercelToken, teamId: vercelTeamId },
|
|
370
|
-
}));
|
|
369
|
+
}), 'DOMAIN_LINK_FAILED'));
|
|
371
370
|
}
|
|
372
371
|
if (domainOptions.length === 0) {
|
|
373
372
|
throw new DoomainError('CONFIG_NOT_FOUND', 'No domains found in configured DNS providers. Verify provider credentials and domain permissions.');
|
|
@@ -396,10 +395,10 @@ export default class Wizard extends Command {
|
|
|
396
395
|
}
|
|
397
396
|
p.log.info(`Using domain ${selectedDomain.domain} from ${providerAccountLabel(selectedDomain)}.`);
|
|
398
397
|
const domain = selectedDomain.domain;
|
|
399
|
-
await updateConfig((current) => ({
|
|
398
|
+
await runDoomainEffect(updateConfig((current) => ({
|
|
400
399
|
...current,
|
|
401
400
|
defaults: { ...current.defaults, domain, provider: selectedDomain.providerId },
|
|
402
|
-
}));
|
|
401
|
+
}), 'DOMAIN_LINK_FAILED'));
|
|
403
402
|
const mode = await p.select({
|
|
404
403
|
message: 'What should Doomain add?',
|
|
405
404
|
options: [
|
|
@@ -418,14 +417,14 @@ export default class Wizard extends Command {
|
|
|
418
417
|
return;
|
|
419
418
|
}
|
|
420
419
|
const fullDomain = apex ? domain : `${subdomain}.${domain}`;
|
|
421
|
-
const preview = await createLinkPlan({
|
|
420
|
+
const preview = await runDoomainEffect(createLinkPlan({
|
|
422
421
|
account: selectedDomain.account,
|
|
423
422
|
provider: selectedDomain.providerId,
|
|
424
423
|
domain,
|
|
425
424
|
subdomain,
|
|
426
425
|
apex,
|
|
427
426
|
project,
|
|
428
|
-
});
|
|
427
|
+
}));
|
|
429
428
|
p.note([
|
|
430
429
|
`Vercel: add ${preview.domain} to ${projectDisplay}`,
|
|
431
430
|
...preview.records.map((record) => recordPreview(record, providerAccountLabel(selectedDomain))),
|
|
@@ -440,7 +439,7 @@ export default class Wizard extends Command {
|
|
|
440
439
|
const spinner = p.spinner();
|
|
441
440
|
activeSpinner = spinner;
|
|
442
441
|
spinner.start('Adding domain to Vercel');
|
|
443
|
-
const result = await linkDomain({
|
|
442
|
+
const result = await runDoomainEffect(linkDomain({
|
|
444
443
|
account: selectedDomain.account,
|
|
445
444
|
provider: selectedDomain.providerId,
|
|
446
445
|
domain,
|
|
@@ -463,7 +462,7 @@ export default class Wizard extends Command {
|
|
|
463
462
|
},
|
|
464
463
|
progress: ({ message }) => spinner.message(message),
|
|
465
464
|
wait: true,
|
|
466
|
-
});
|
|
465
|
+
}));
|
|
467
466
|
spinner.stop(result.vercel.verified ? 'Domain linked and verified' : 'Domain linked');
|
|
468
467
|
activeSpinner = undefined;
|
|
469
468
|
p.outro(result.vercel.verified
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { run } from '@oclif/core';
|
|
2
|
-
export { type DomainProviderResult, type FindDomainProviderInput, findDomainProvider, type ProviderSearchWarning, } from './lib/domain-provider.js';
|
|
2
|
+
export { type DomainProviderResult, type FindDomainProviderInput, findDomainProvider, findDomainProviderEffect, type ProviderSearchWarning, } from './lib/domain-provider.js';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { run } from '@oclif/core';
|
|
2
|
-
export { findDomainProvider, } from './lib/domain-provider.js';
|
|
2
|
+
export { findDomainProvider, findDomainProviderEffect, } from './lib/domain-provider.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ClerkDomainStatus } from './clerk.js';
|
|
2
|
+
import { type DoomainEffect } from './effect.js';
|
|
2
3
|
import { type DnsOverrideWarning } from './link-domain.js';
|
|
3
4
|
import type { DnsRecordInput } from './providers/types.js';
|
|
4
5
|
export interface AddClerkDomainInput {
|
|
@@ -36,4 +37,4 @@ export interface AddClerkDomainResult {
|
|
|
36
37
|
zoneDomain: string;
|
|
37
38
|
nextSteps: string[];
|
|
38
39
|
}
|
|
39
|
-
export declare function addClerkProductionDomain(input: AddClerkDomainInput):
|
|
40
|
+
export declare function addClerkProductionDomain(input: AddClerkDomainInput): DoomainEffect<AddClerkDomainResult>;
|
package/dist/lib/clerk-domain.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
1
2
|
import { createClerkPlatformClient, resolveClerkPlatformConfig } from './clerk.js';
|
|
2
3
|
import { resolveProviderTarget } from './domain-provider.js';
|
|
4
|
+
import { tryPromise, trySync } from './effect.js';
|
|
3
5
|
import { DoomainError } from './errors.js';
|
|
4
6
|
import { withProviderRecordOptions } from './link-domain.js';
|
|
5
7
|
import { createProvider } from './providers/registry.js';
|
|
@@ -39,125 +41,135 @@ function dnsConflictError(warning, productionInstanceId) {
|
|
|
39
41
|
function statusComplete(check) {
|
|
40
42
|
return check?.required === false || check?.status === 'complete';
|
|
41
43
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
|
|
44
|
+
function waitForStatus(client, app, domainId, timeoutSeconds, progress) {
|
|
45
|
+
return Effect.gen(function* () {
|
|
46
|
+
const trigger = yield* client.triggerDomainDnsCheck(app, domainId).pipe(Effect.either);
|
|
47
|
+
if (trigger._tag === 'Left') {
|
|
48
|
+
const conflict = JSON.stringify(trigger.left.details).toLowerCase().includes('conflict');
|
|
49
|
+
if (!conflict)
|
|
50
|
+
return yield* Effect.fail(trigger.left);
|
|
51
|
+
}
|
|
52
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
53
|
+
let status = { status: 'incomplete' };
|
|
54
|
+
let attempt = 1;
|
|
55
|
+
while (Date.now() <= deadline) {
|
|
56
|
+
progress?.(`Checking Clerk DNS, SSL, and email DNS status (attempt ${attempt})`);
|
|
57
|
+
status = yield* client.getDomainStatus(app, domainId);
|
|
58
|
+
if (status.status === 'complete')
|
|
59
|
+
return status;
|
|
60
|
+
attempt += 1;
|
|
61
|
+
if (Date.now() > deadline)
|
|
62
|
+
break;
|
|
63
|
+
yield* Effect.sleep(5000);
|
|
64
|
+
}
|
|
65
|
+
return status;
|
|
47
66
|
});
|
|
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
67
|
}
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
68
|
+
export function addClerkProductionDomain(input) {
|
|
69
|
+
return Effect.gen(function* () {
|
|
70
|
+
const domain = yield* trySync(() => normalizeDomain(input.domain ?? ''), 'INVALID_INPUT');
|
|
71
|
+
const config = yield* resolveClerkPlatformConfig(input.app);
|
|
72
|
+
const client = createClerkPlatformClient(config);
|
|
73
|
+
input.progress?.('Checking Clerk application');
|
|
74
|
+
const application = yield* client.fetchApplication(config.appId);
|
|
75
|
+
const production = application.instances.find((instance) => instance.environment_type === 'production');
|
|
76
|
+
if (production)
|
|
77
|
+
return yield* Effect.fail(productionExistsError(config.appId, production.instance_id));
|
|
78
|
+
const development = application.instances.find((instance) => instance.environment_type === 'development');
|
|
79
|
+
if (!development)
|
|
80
|
+
return yield* Effect.fail(new DoomainError('PROJECT_NOT_FOUND', `Clerk application ${config.appId} does not have a development instance to clone.`));
|
|
81
|
+
input.progress?.('Finding the DNS provider and zone');
|
|
82
|
+
const resolved = yield* resolveProviderTarget({ account: input.account, domain, provider: input.provider }, { transportErrorCode: 'DOMAIN_LINK_FAILED' });
|
|
83
|
+
const provider = yield* createProvider(resolved.provider, {
|
|
84
|
+
account: resolved.account,
|
|
85
|
+
transportErrorCode: 'DOMAIN_LINK_FAILED',
|
|
86
|
+
});
|
|
87
|
+
const zone = yield* provider.getZone(resolved.target.zoneDomain);
|
|
88
|
+
if (!zone)
|
|
89
|
+
return yield* Effect.fail(new DoomainError('PROVIDER_ZONE_NOT_FOUND', `${provider.name} does not have a DNS zone for ${resolved.target.zoneDomain}.`));
|
|
90
|
+
const nextSteps = [
|
|
91
|
+
`clerk link --app ${config.appId}`,
|
|
92
|
+
`clerk env pull --app ${config.appId} --instance prod`,
|
|
93
|
+
'clerk deploy',
|
|
94
|
+
'clerk deploy status',
|
|
95
|
+
];
|
|
96
|
+
if (input.dryRun) {
|
|
97
|
+
return {
|
|
98
|
+
account: resolved.account,
|
|
99
|
+
app: config.appId,
|
|
100
|
+
clerk: { productionInstanceCreated: false, verified: false },
|
|
101
|
+
dns: { propagated: false, skipped: [], updated: false },
|
|
102
|
+
domain,
|
|
103
|
+
dryRun: true,
|
|
104
|
+
isDefaultAccount: resolved.isDefaultAccount,
|
|
105
|
+
nextSteps,
|
|
106
|
+
provider: resolved.provider,
|
|
107
|
+
records: [],
|
|
108
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
input.progress?.('Creating Clerk production instance and primary domain');
|
|
112
|
+
const created = yield* client.createProductionInstance(config.appId, domain, development.instance_id);
|
|
113
|
+
const clerkDomain = created.active_domain;
|
|
114
|
+
if (!clerkDomain) {
|
|
115
|
+
return yield* Effect.fail(new DoomainError('DOMAIN_LINK_FAILED', 'Clerk created the production instance but did not return its primary domain.', {
|
|
116
|
+
productionInstanceId: created.id,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
const records = yield* trySync(() => recordsFromTargets(resolved.provider, resolved.target.zoneDomain, clerkDomain.cname_targets ?? []), 'INVALID_INPUT');
|
|
120
|
+
if (records.length === 0) {
|
|
121
|
+
return yield* Effect.fail(new DoomainError('DOMAIN_LINK_FAILED', 'Clerk created the production instance but did not return any DNS records.', {
|
|
122
|
+
domainId: clerkDomain.id,
|
|
123
|
+
productionInstanceId: created.id,
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
input.progress?.(`Checking existing DNS records in ${provider.name}`);
|
|
127
|
+
let forceDns = Boolean(input.force);
|
|
128
|
+
let dnsPlan = yield* provider.planChanges(zone, records, { force: forceDns });
|
|
129
|
+
if (!forceDns && dnsPlan.conflicts.length > 0) {
|
|
130
|
+
const warning = {
|
|
131
|
+
account: resolved.account,
|
|
132
|
+
conflicts: dnsPlan.conflicts,
|
|
133
|
+
desired: records,
|
|
134
|
+
domain,
|
|
135
|
+
provider: resolved.provider,
|
|
136
|
+
providerName: provider.name,
|
|
137
|
+
recordName: records[0]?.name ?? '@',
|
|
138
|
+
zoneDomain: resolved.target.zoneDomain,
|
|
139
|
+
};
|
|
140
|
+
forceDns = input.confirmDnsOverride
|
|
141
|
+
? yield* tryPromise(() => input.confirmDnsOverride?.(warning) ?? Promise.resolve(false), 'DOMAIN_LINK_FAILED')
|
|
142
|
+
: false;
|
|
143
|
+
if (!forceDns)
|
|
144
|
+
return yield* Effect.fail(dnsConflictError(warning, created.id));
|
|
145
|
+
dnsPlan = yield* provider.planChanges(zone, records, { force: true });
|
|
146
|
+
}
|
|
147
|
+
input.progress?.(`Creating Clerk DNS records in ${provider.name}`);
|
|
148
|
+
const dnsResult = yield* provider.applyChanges(zone, dnsPlan, { force: forceDns });
|
|
149
|
+
const shouldWait = input.wait ?? true;
|
|
150
|
+
const status = shouldWait
|
|
151
|
+
? yield* waitForStatus(client, config.appId, clerkDomain.id, input.timeoutSeconds ?? 300, input.progress)
|
|
152
|
+
: undefined;
|
|
153
|
+
const verified = status?.status === 'complete';
|
|
154
|
+
const propagated = Boolean(status && statusComplete(status.dns) && statusComplete(status.mail));
|
|
88
155
|
return {
|
|
89
156
|
account: resolved.account,
|
|
90
157
|
app: config.appId,
|
|
91
|
-
clerk: {
|
|
92
|
-
|
|
158
|
+
clerk: {
|
|
159
|
+
domainId: clerkDomain.id,
|
|
160
|
+
productionInstanceCreated: true,
|
|
161
|
+
productionInstanceId: created.id,
|
|
162
|
+
status,
|
|
163
|
+
verified,
|
|
164
|
+
},
|
|
165
|
+
dns: { propagated, skipped: dnsResult.skipped, updated: dnsResult.applied.length > 0 },
|
|
93
166
|
domain,
|
|
94
|
-
dryRun:
|
|
167
|
+
dryRun: false,
|
|
95
168
|
isDefaultAccount: resolved.isDefaultAccount,
|
|
96
169
|
nextSteps,
|
|
97
170
|
provider: resolved.provider,
|
|
98
|
-
records
|
|
171
|
+
records,
|
|
99
172
|
zoneDomain: resolved.target.zoneDomain,
|
|
100
173
|
};
|
|
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
|
-
};
|
|
174
|
+
});
|
|
163
175
|
}
|
package/dist/lib/clerk.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DoomainEffect } from './effect.js';
|
|
2
|
+
import { type DoomainErrorCode } from './errors.js';
|
|
1
3
|
export interface ClerkPlatformConfig {
|
|
2
4
|
appId: string;
|
|
3
5
|
platformApiKey: string;
|
|
@@ -44,13 +46,15 @@ export interface ClerkDomainStatus {
|
|
|
44
46
|
};
|
|
45
47
|
status: string;
|
|
46
48
|
}
|
|
47
|
-
export declare function resolveClerkPlatformConfig(appId?: string):
|
|
49
|
+
export declare function resolveClerkPlatformConfig(appId?: string): DoomainEffect<ClerkPlatformConfig>;
|
|
48
50
|
export declare function createClerkPlatformClient(config: {
|
|
49
51
|
platformApiKey: string;
|
|
52
|
+
}, opts?: {
|
|
53
|
+
transportErrorCode?: DoomainErrorCode;
|
|
50
54
|
}): {
|
|
51
|
-
fetchApplication(appId: string):
|
|
52
|
-
createProductionInstance(appId: string, domain: string, developmentInstanceId: string):
|
|
53
|
-
getDomainStatus(appId: string, domainId: string):
|
|
54
|
-
triggerDomainDnsCheck(appId: string, domainId: string):
|
|
55
|
+
fetchApplication(appId: string): DoomainEffect<ClerkApplication>;
|
|
56
|
+
createProductionInstance(appId: string, domain: string, developmentInstanceId: string): DoomainEffect<ClerkProductionInstance>;
|
|
57
|
+
getDomainStatus(appId: string, domainId: string): DoomainEffect<ClerkDomainStatus>;
|
|
58
|
+
triggerDomainDnsCheck(appId: string, domainId: string): DoomainEffect<ClerkDomainStatus>;
|
|
55
59
|
};
|
|
56
60
|
export type ClerkPlatformClient = ReturnType<typeof createClerkPlatformClient>;
|
package/dist/lib/clerk.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
1
2
|
import { loadConfig } from './config.js';
|
|
2
|
-
import { DoomainError } from './errors.js';
|
|
3
|
+
import { DoomainError, toDoomainError } from './errors.js';
|
|
3
4
|
const CLERK_API_URL = 'https://api.clerk.com';
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
export function resolveClerkPlatformConfig(appId) {
|
|
6
|
+
return Effect.gen(function* () {
|
|
7
|
+
const config = yield* loadConfig();
|
|
8
|
+
const platformApiKey = process.env.CLERK_PLATFORM_API_KEY || config.clerk?.platformApiKey;
|
|
9
|
+
const resolvedAppId = appId || process.env.CLERK_APPLICATION_ID || config.clerk?.appId;
|
|
10
|
+
if (!platformApiKey) {
|
|
11
|
+
return yield* Effect.fail(new DoomainError('MISSING_CREDENTIALS', 'Missing Clerk Platform API key. Run `doomain auth clerk`, set CLERK_PLATFORM_API_KEY, or pass saved credentials.'));
|
|
12
|
+
}
|
|
13
|
+
if (!platformApiKey.startsWith('ak_')) {
|
|
14
|
+
return yield* Effect.fail(new DoomainError('INVALID_INPUT', 'Clerk Platform API keys must start with ak_.'));
|
|
15
|
+
}
|
|
16
|
+
if (!resolvedAppId) {
|
|
17
|
+
return yield* Effect.fail(new DoomainError('MISSING_ARGUMENT', 'Clerk application is required. Pass --app, set CLERK_APPLICATION_ID, or save it with `doomain auth clerk`.'));
|
|
18
|
+
}
|
|
19
|
+
return { appId: resolvedAppId, platformApiKey };
|
|
20
|
+
});
|
|
18
21
|
}
|
|
19
22
|
function apiErrorMessage(status, body) {
|
|
20
23
|
return (body?.errors?.[0]?.long_message ??
|
|
@@ -26,28 +29,34 @@ function apiErrorMessage(status, body) {
|
|
|
26
29
|
function apiErrorCode(body) {
|
|
27
30
|
return body?.errors?.[0]?.code ?? body?.error?.code ?? body?.code;
|
|
28
31
|
}
|
|
29
|
-
export function createClerkPlatformClient(config) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
if (response.
|
|
46
|
-
|
|
32
|
+
export function createClerkPlatformClient(config, opts = {}) {
|
|
33
|
+
function request(path, init = {}) {
|
|
34
|
+
return Effect.gen(function* () {
|
|
35
|
+
const response = yield* Effect.tryPromise({
|
|
36
|
+
try: (signal) => fetch(`${CLERK_API_URL}${path}`, {
|
|
37
|
+
...init,
|
|
38
|
+
headers: {
|
|
39
|
+
Accept: 'application/json',
|
|
40
|
+
Authorization: `Bearer ${config.platformApiKey}`,
|
|
41
|
+
...(init.body ? { 'Content-Type': 'application/json' } : {}),
|
|
42
|
+
...(init.headers ?? {}),
|
|
43
|
+
},
|
|
44
|
+
signal: init.signal ?? signal,
|
|
45
|
+
}),
|
|
46
|
+
catch: (cause) => toDoomainError(cause, opts.transportErrorCode ?? 'DOMAIN_LINK_FAILED'),
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const body = yield* Effect.tryPromise(() => response.json()).pipe(Effect.catchAll(() => Effect.succeed(undefined)));
|
|
50
|
+
if (response.status === 401 || response.status === 403) {
|
|
51
|
+
return yield* Effect.fail(new DoomainError('CLERK_AUTH_FAILED', `Clerk Platform API authorization failed. Check CLERK_PLATFORM_API_KEY and its application access. ${apiErrorMessage(response.status, body)}`, body));
|
|
52
|
+
}
|
|
53
|
+
if (response.status === 409 && apiErrorCode(body) === 'production_instance_exists') {
|
|
54
|
+
return yield* Effect.fail(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));
|
|
55
|
+
}
|
|
56
|
+
return yield* Effect.fail(new DoomainError('DOMAIN_LINK_FAILED', apiErrorMessage(response.status, body), body));
|
|
47
57
|
}
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
return (await response.json());
|
|
58
|
+
return (yield* Effect.tryPromise(() => response.json()).pipe(Effect.mapError((cause) => toDoomainError(cause, opts.transportErrorCode ?? 'DOMAIN_LINK_FAILED'))));
|
|
59
|
+
});
|
|
51
60
|
}
|
|
52
61
|
return {
|
|
53
62
|
fetchApplication(appId) {
|