doomain 0.1.22 → 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.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/auth/clerk.js +3 -2
  3. package/dist/commands/auth/logout/clerk.js +3 -2
  4. package/dist/commands/auth/logout/vercel.js +3 -2
  5. package/dist/commands/auth/vercel.js +5 -4
  6. package/dist/commands/clerk/domains/add.js +3 -2
  7. package/dist/commands/dns/diagnose.js +3 -2
  8. package/dist/commands/dns/point.js +7 -3
  9. package/dist/commands/dns/remove.js +3 -2
  10. package/dist/commands/domains/find.js +3 -2
  11. package/dist/commands/domains/list.js +46 -33
  12. package/dist/commands/link.js +4 -3
  13. package/dist/commands/projects/list.js +5 -2
  14. package/dist/commands/providers/connect.js +14 -23
  15. package/dist/commands/providers/disconnect.js +3 -2
  16. package/dist/commands/providers/status.js +2 -1
  17. package/dist/commands/providers/verify.js +3 -2
  18. package/dist/commands/schema.js +2 -1
  19. package/dist/commands/update.js +2 -1
  20. package/dist/commands/verify.js +5 -2
  21. package/dist/commands/version.d.ts +5 -0
  22. package/dist/commands/version.js +8 -0
  23. package/dist/commands/wizard.js +45 -46
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/lib/clerk-domain.d.ts +2 -1
  27. package/dist/lib/clerk-domain.js +122 -110
  28. package/dist/lib/clerk.d.ts +9 -5
  29. package/dist/lib/clerk.js +45 -36
  30. package/dist/lib/command-schema.d.ts +2 -1
  31. package/dist/lib/command-schema.js +16 -9
  32. package/dist/lib/config.d.ts +6 -4
  33. package/dist/lib/config.js +29 -27
  34. package/dist/lib/diagnose-dns.d.ts +8 -5
  35. package/dist/lib/diagnose-dns.js +90 -92
  36. package/dist/lib/dns-propagation.d.ts +5 -4
  37. package/dist/lib/dns-propagation.js +80 -54
  38. package/dist/lib/dns-reconciliation.d.ts +4 -3
  39. package/dist/lib/dns-reconciliation.js +52 -47
  40. package/dist/lib/domain-provider.d.ts +5 -1
  41. package/dist/lib/domain-provider.js +136 -131
  42. package/dist/lib/effect.d.ts +6 -0
  43. package/dist/lib/effect.js +20 -0
  44. package/dist/lib/link-domain.d.ts +3 -2
  45. package/dist/lib/link-domain.js +239 -218
  46. package/dist/lib/point-domain.d.ts +7 -4
  47. package/dist/lib/point-domain.js +92 -90
  48. package/dist/lib/providers/cloudflare/index.d.ts +9 -8
  49. package/dist/lib/providers/cloudflare/index.js +93 -87
  50. package/dist/lib/providers/core/config.d.ts +4 -1
  51. package/dist/lib/providers/core/config.js +17 -13
  52. package/dist/lib/providers/core/http.d.ts +4 -1
  53. package/dist/lib/providers/core/http.js +28 -18
  54. package/dist/lib/providers/core/pagination.d.ts +3 -2
  55. package/dist/lib/providers/core/pagination.js +17 -14
  56. package/dist/lib/providers/core/planner.d.ts +5 -4
  57. package/dist/lib/providers/core/planner.js +18 -15
  58. package/dist/lib/providers/core/types.d.ts +11 -8
  59. package/dist/lib/providers/hostinger/index.d.ts +9 -8
  60. package/dist/lib/providers/hostinger/index.js +74 -72
  61. package/dist/lib/providers/namecheap/index.d.ts +10 -8
  62. package/dist/lib/providers/namecheap/index.js +134 -109
  63. package/dist/lib/providers/registry.d.ts +3 -3
  64. package/dist/lib/providers/registry.js +7 -3
  65. package/dist/lib/providers/spaceship/index.d.ts +9 -8
  66. package/dist/lib/providers/spaceship/index.js +48 -47
  67. package/dist/lib/providers/status.d.ts +2 -1
  68. package/dist/lib/providers/status.js +41 -34
  69. package/dist/lib/public-ip.d.ts +2 -0
  70. package/dist/lib/public-ip.js +14 -0
  71. package/dist/lib/remove-domain.d.ts +6 -3
  72. package/dist/lib/remove-domain.js +83 -77
  73. package/dist/lib/self-update.d.ts +3 -2
  74. package/dist/lib/self-update.js +15 -23
  75. package/dist/lib/vercel-auth.d.ts +2 -1
  76. package/dist/lib/vercel-auth.js +21 -24
  77. package/dist/lib/vercel.d.ts +16 -12
  78. package/dist/lib/vercel.js +174 -114
  79. package/oclif.manifest.json +21 -1
  80. package/package.json +8 -1
@@ -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
- async function fetchPublicIp() {
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 fetchPublicIp();
65
+ return detectedPublicIp;
80
66
  }
81
- async function promptCredential(credential) {
82
- const initialValue = credential.secret ? undefined : await credentialInitialValue(credential);
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
- async function listProviderDomainOptions(definition, account) {
121
- const provider = await createProvider(definition.id, { account: account.account });
122
- const zones = await provider.listZones();
123
- return zones.map((zone) => toProviderDomainOption(definition, account, zone));
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 credentials = await promptProviderCredentials(selectedDefinition);
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({ credentials, debug: process.env.DOOMAIN_DEBUG === '1' });
334
- const zones = await provider.listZones();
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): Promise<AddClerkDomainResult>;
40
+ export declare function addClerkProductionDomain(input: AddClerkDomainInput): DoomainEffect<AddClerkDomainResult>;
@@ -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
- 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;
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 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) {
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: { productionInstanceCreated: false, verified: false },
92
- dns: { propagated: false, skipped: [], updated: false },
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: true,
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
  }
@@ -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): Promise<ClerkPlatformConfig>;
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): 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
+ 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 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 };
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
- async function request(path, init = {}) {
31
- const response = await fetch(`${CLERK_API_URL}${path}`, {
32
- ...init,
33
- headers: {
34
- Accept: 'application/json',
35
- Authorization: `Bearer ${config.platformApiKey}`,
36
- ...(init.body ? { 'Content-Type': 'application/json' } : {}),
37
- ...(init.headers ?? {}),
38
- },
39
- });
40
- if (!response.ok) {
41
- const body = (await response.json().catch(() => undefined));
42
- if (response.status === 401 || response.status === 403) {
43
- 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);
44
- }
45
- if (response.status === 409 && apiErrorCode(body) === 'production_instance_exists') {
46
- 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);
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
- throw new DoomainError('DOMAIN_LINK_FAILED', apiErrorMessage(response.status, body), body);
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) {