@myapihq/cli 1.2.1 → 1.2.4

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.
@@ -11,6 +11,8 @@ export declare function list(flags: Flags): Promise<void>;
11
11
  export declare function assign(domainArg: string, flags: Flags): Promise<void>;
12
12
  export declare function unassign(domainArg: string, flags: Flags): Promise<void>;
13
13
  export declare function status(domainArg: string, flags: Flags): Promise<void>;
14
+ export declare function emailSetup(domainArg: string, flags: Flags): Promise<void>;
15
+ export declare function retryProvisioning(domainArg: string, flags: Flags): Promise<void>;
14
16
  export declare function settings(domainArg: string, flags: Flags): Promise<void>;
15
17
  export declare function updateSettings(domainArg: string, flags: Flags): Promise<void>;
16
18
  export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
@@ -14,6 +14,8 @@ export const EXPOSES = [
14
14
  'GET /domain/orgs/{org_id}/{domain}/settings',
15
15
  'POST /domain/orgs/{org_id}/{domain}/settings',
16
16
  'GET /domain/orgs/{org_id}/{domain}/status',
17
+ 'POST /domain/orgs/{org_id}/{domain}/email-infra',
18
+ 'POST /domain/orgs/{org_id}/{domain}/retry-provisioning',
17
19
  ];
18
20
  export const SCHEMA = {
19
21
  org: 'string',
@@ -38,6 +40,16 @@ export const SCHEMA = {
38
40
  'registrant-postal-code': 'string',
39
41
  'registrant-country': 'string',
40
42
  'registrant-organization': 'string',
43
+ // DNS records sub-surface
44
+ type: 'string',
45
+ name: 'string',
46
+ content: 'string',
47
+ ttl: 'number',
48
+ priority: 'number',
49
+ proxied: 'boolean',
50
+ yes: 'boolean',
51
+ // Email-infra opt-in
52
+ subdomain: 'string',
41
53
  };
42
54
  export async function check(domainArg, flags) {
43
55
  const config = requireConfig();
@@ -161,8 +173,10 @@ export async function unassign(domainArg, flags) {
161
173
  await sdkDomain.unassignDomain(config.api_key, orgId, domainArg);
162
174
  success(`Unassigned ${domainArg} from org ${orgId}`);
163
175
  }
164
- // Terminal states — stop polling under --watch.
165
- const TERMINAL_STATUSES = new Set(['active', 'failed', 'error', 'expired']);
176
+ // Terminal states — stop polling under --watch. `infra_error` is included
177
+ // because watching past it is pointless — the user has to act (retry or
178
+ // contact support); a re-watch after retry is the natural next step.
179
+ const TERMINAL_STATUSES = new Set(['active', 'failed', 'error', 'expired', 'infra_error']);
166
180
  export async function status(domainArg, flags) {
167
181
  const config = requireConfig();
168
182
  const orgId = requireOrg(flags, config, 'myapi domain status <domain> [--org <id>] [--watch]');
@@ -204,13 +218,63 @@ function renderStatus(res) {
204
218
  info(`Status: ${res.status}`);
205
219
  if (res.expires_at)
206
220
  info(`Expires: ${formatDate(res.expires_at)}`);
221
+ if (res.dns_active !== undefined)
222
+ info(`DNS: ${res.dns_active ? 'active' : 'pending'}`);
223
+ if (res.email_infra) {
224
+ const where = res.email_subdomain ? ` on ${res.email_subdomain}` : '';
225
+ info(`Email: ${res.email_infra}${where}`);
226
+ }
207
227
  if (res.status === 'pending_ns_change') {
208
228
  info('Waiting for you to change nameservers at your current registrar. Each poll re-checks Cloudflare.');
209
229
  }
230
+ if (res.status === 'infra_error' && res.error_detail) {
231
+ info(` Failed step: ${res.error_detail.failed_step}`);
232
+ info(` Message: ${res.error_detail.message}`);
233
+ if (res.error_detail.attempt_count !== undefined) {
234
+ const last = res.error_detail.last_attempt_at ? `; last try ${res.error_detail.last_attempt_at}` : '';
235
+ info(` Attempts: ${res.error_detail.attempt_count}${last}`);
236
+ }
237
+ if (res.error_detail.retryable) {
238
+ info(` → myapi domain retry-provisioning ${res.domain}`);
239
+ }
240
+ else {
241
+ info(' Not retryable — contact support.');
242
+ }
243
+ }
210
244
  if (res.status === 'active') {
211
245
  info('Note: if recently activated, the SSL certificate may still be provisioning — allow a few minutes before the site is reachable over HTTPS.');
212
246
  }
213
247
  }
248
+ // ── Email infra opt-in + retry-provisioning ──────────────────────────────────
249
+ export async function emailSetup(domainArg, flags) {
250
+ const config = requireConfig();
251
+ const orgId = requireOrg(flags, config, 'myapi domain email-setup <domain> [--subdomain <label>] [--org <id>]');
252
+ const domain = requireDomain(domainArg, flags, config, 'myapi domain email-setup <domain> [--subdomain <label>] [--org <id>]');
253
+ const subdomain = typeof flags.subdomain === 'string' ? flags.subdomain : undefined;
254
+ const res = await sdkDomain.setupEmailInfra(config.api_key, orgId, domain, subdomain);
255
+ if (flags.json) {
256
+ printJson(res);
257
+ return;
258
+ }
259
+ success(`Email infra provisioning on ${res.email_subdomain} (state: ${res.email_infra})`);
260
+ if (res.next_step)
261
+ info(res.next_step);
262
+ info(`Track with: myapi domain status ${domain} --watch`);
263
+ }
264
+ export async function retryProvisioning(domainArg, flags) {
265
+ const config = requireConfig();
266
+ const orgId = requireOrg(flags, config, 'myapi domain retry-provisioning <domain> [--org <id>]');
267
+ const domain = requireDomain(domainArg, flags, config, 'myapi domain retry-provisioning <domain> [--org <id>]');
268
+ const res = await sdkDomain.retryProvisioning(config.api_key, orgId, domain);
269
+ if (flags.json) {
270
+ printJson(res);
271
+ return;
272
+ }
273
+ success(`Retry triggered for ${res.domain}`);
274
+ if (res.next_step)
275
+ info(res.next_step);
276
+ info(`Track with: myapi domain status ${domain} --watch`);
277
+ }
214
278
  export async function settings(domainArg, flags) {
215
279
  const config = requireConfig();
216
280
  const orgId = requireOrg(flags, config, 'myapi domain settings <domain> [--org <id>]');
@@ -235,6 +299,157 @@ export async function updateSettings(domainArg, flags) {
235
299
  const res = await sdkDomain.updateDomainSettings(config.api_key, orgId, domain, payload);
236
300
  success(`Updated settings for ${domain}!\n${JSON.stringify(res, null, 2)}`);
237
301
  }
302
+ // ── DNS records sub-surface (`myapi domain records ...`) ────────────────────
303
+ const ALLOWED_RECORD_TYPES = new Set(['A', 'AAAA', 'CNAME', 'MX', 'TXT']);
304
+ function parseRecordType(raw, where) {
305
+ if (typeof raw !== 'string' || !ALLOWED_RECORD_TYPES.has(raw.toUpperCase())) {
306
+ error(`${where}: --type must be one of A, AAAA, CNAME, MX, TXT (got: ${raw ?? '(missing)'}).`);
307
+ }
308
+ return raw.toUpperCase();
309
+ }
310
+ function recordRow(r) {
311
+ return {
312
+ id: r.id,
313
+ type: r.type,
314
+ name: r.name,
315
+ content: r.content,
316
+ ttl: r.ttl === 1 ? 'auto' : String(r.ttl),
317
+ priority: r.priority ?? '—',
318
+ proxied: r.proxied === null ? '—' : (r.proxied ? 'yes' : 'no'),
319
+ };
320
+ }
321
+ async function recordsList(domainArg, flags) {
322
+ const config = requireConfig();
323
+ const orgId = requireOrg(flags, config, 'myapi domain records list <domain> [--type <T>] [--org <id>]');
324
+ const domain = requireDomain(domainArg, flags, config, 'myapi domain records list <domain> [--type <T>] [--org <id>]');
325
+ const type = flags.type ? parseRecordType(flags.type, 'records list') : undefined;
326
+ const records = await sdkDomain.listDnsRecords(config.api_key, orgId, domain, type);
327
+ if (flags.json) {
328
+ printJson(records);
329
+ return;
330
+ }
331
+ printTable(records.map(recordRow), { flags, empty: 'No records in this zone.' });
332
+ }
333
+ async function recordsGet(domainArg, recordId, flags) {
334
+ const config = requireConfig();
335
+ const orgId = requireOrg(flags, config, 'myapi domain records get <domain> <record-id> [--org <id>]');
336
+ const domain = requireDomain(domainArg, flags, config, 'myapi domain records get <domain> <record-id> [--org <id>]');
337
+ if (!recordId)
338
+ error('Missing record id.\nUsage: myapi domain records get <domain> <record-id>');
339
+ const r = await sdkDomain.getDnsRecord(config.api_key, orgId, domain, recordId);
340
+ if (flags.json) {
341
+ printJson(r);
342
+ return;
343
+ }
344
+ printJson(r);
345
+ }
346
+ async function recordsCreate(domainArg, flags) {
347
+ const config = requireConfig();
348
+ const orgId = requireOrg(flags, config, 'myapi domain records create <domain> --type <T> --name <n> --content <c> [...]');
349
+ const domain = requireDomain(domainArg, flags, config, 'myapi domain records create <domain> --type <T> --name <n> --content <c> [...]');
350
+ const type = parseRecordType(flags.type, 'records create');
351
+ const name = typeof flags.name === 'string' ? flags.name : '';
352
+ const content = typeof flags.content === 'string' ? flags.content : '';
353
+ if (!name)
354
+ error('Missing --name. For apex, pass --name=@ or --name=<domain>.');
355
+ if (!content)
356
+ error('Missing --content.');
357
+ const input = { type, name, content };
358
+ if (typeof flags.ttl === 'number')
359
+ input.ttl = flags.ttl;
360
+ if (typeof flags.priority === 'number')
361
+ input.priority = flags.priority;
362
+ if (typeof flags.proxied === 'boolean')
363
+ input.proxied = flags.proxied;
364
+ if (type === 'MX' && input.priority === undefined)
365
+ error('Missing --priority. Required for MX records (typical value: 10).');
366
+ const r = await sdkDomain.createDnsRecord(config.api_key, orgId, domain, input);
367
+ success(`Created ${r.type} record ${r.id}`);
368
+ printJson(r);
369
+ }
370
+ async function recordsUpdate(domainArg, recordId, flags) {
371
+ const config = requireConfig();
372
+ const orgId = requireOrg(flags, config, 'myapi domain records update <domain> <record-id> [--content ...] [...]');
373
+ const domain = requireDomain(domainArg, flags, config, 'myapi domain records update <domain> <record-id> [--content ...] [...]');
374
+ if (!recordId)
375
+ error('Missing record id.\nUsage: myapi domain records update <domain> <record-id> [flags]');
376
+ const patch = {};
377
+ if (typeof flags.name === 'string')
378
+ patch.name = flags.name;
379
+ if (typeof flags.content === 'string')
380
+ patch.content = flags.content;
381
+ if (typeof flags.ttl === 'number')
382
+ patch.ttl = flags.ttl;
383
+ if (typeof flags.priority === 'number')
384
+ patch.priority = flags.priority;
385
+ if (typeof flags.proxied === 'boolean')
386
+ patch.proxied = flags.proxied;
387
+ if (Object.keys(patch).length === 0)
388
+ error('Nothing to update. Pass at least one of --name, --content, --ttl, --priority, --proxied.');
389
+ const r = await sdkDomain.updateDnsRecord(config.api_key, orgId, domain, recordId, patch);
390
+ success(`Updated record ${r.id}`);
391
+ printJson(r);
392
+ }
393
+ async function recordsDelete(domainArg, recordId, flags) {
394
+ const config = requireConfig();
395
+ const orgId = requireOrg(flags, config, 'myapi domain records delete <domain> <record-id> [--yes] [--org <id>]');
396
+ const domain = requireDomain(domainArg, flags, config, 'myapi domain records delete <domain> <record-id> [--yes] [--org <id>]');
397
+ if (!recordId)
398
+ error('Missing record id.\nUsage: myapi domain records delete <domain> <record-id> [--yes]');
399
+ if (!flags.yes) {
400
+ info(`About to delete record ${recordId} from ${domain}. Re-run with --yes to confirm.`);
401
+ return;
402
+ }
403
+ await sdkDomain.deleteDnsRecord(config.api_key, orgId, domain, recordId);
404
+ success(`Deleted record ${recordId}`);
405
+ }
406
+ const RECORDS_HELP = `Usage: myapi domain records <subcommand> <domain> [...]
407
+
408
+ Subcommands:
409
+ list <domain> [--type A|AAAA|CNAME|MX|TXT]
410
+ get <domain> <record-id>
411
+ create <domain> --type <T> --name <n> --content <c> [--ttl <n>]
412
+ [--priority <n>] [--proxied]
413
+ update <domain> <record-id> [--content ...] [--ttl ...] [--priority ...] [--proxied]
414
+ delete <domain> <record-id> [--yes]
415
+
416
+ Name normalization:
417
+ --name=@ Apex (e.g. example.com)
418
+ --name=mail Host-only — backend appends the zone
419
+ --name=mail.example.com FQDN
420
+ --name="" Apex (empty == @)
421
+
422
+ Notes:
423
+ --ttl defaults to 1 (Cloudflare "automatic"). Explicit range: [60, 86400].
424
+ --priority is required for MX (typical: 10).
425
+ --proxied (CF "orange-cloud") applies to A/AAAA/CNAME only.
426
+
427
+ Examples:
428
+ Fix the broken SPF on x80security.com:
429
+ myapi domain records list x80security.com --type TXT
430
+ myapi domain records delete x80security.com <bad-record-id> --yes
431
+ myapi domain records create x80security.com --type TXT --name @ \\
432
+ --content 'v=spf1 include:_spf.mailersend.net include:_spf.google.com ~all'
433
+ myapi domain records create x80security.com --type TXT --name @ \\
434
+ --content 'google-site-verification=...'`;
435
+ async function recordsRun(sub, args, flags) {
436
+ if (!sub || (flags.help && !sub)) {
437
+ info(RECORDS_HELP);
438
+ return;
439
+ }
440
+ if (flags.help) {
441
+ info(RECORDS_HELP);
442
+ return;
443
+ }
444
+ switch (sub) {
445
+ case 'list': return recordsList(args[0], flags);
446
+ case 'get': return recordsGet(args[0], args[1], flags);
447
+ case 'create': return recordsCreate(args[0], flags);
448
+ case 'update': return recordsUpdate(args[0], args[1], flags);
449
+ case 'delete': return recordsDelete(args[0], args[1], flags);
450
+ default: error(`Unknown records subcommand: ${sub}. Run "myapi domain records --help".`);
451
+ }
452
+ }
238
453
  // ── Dispatcher ───────────────────────────────────────────────────────────────
239
454
  const SUBCOMMAND_USAGE = {
240
455
  'list': `myapi domain list [--filter=all|unassigned|org] [--org <id>] [--json]
@@ -312,6 +527,25 @@ To transfer to a different org: unassign first, then assign to the new org.`,
312
527
 
313
528
  Gets current edge/CDN settings (security level, browser check, cache).
314
529
  Use "myapi domain update-settings" to change them.`,
530
+ 'records': RECORDS_HELP,
531
+ 'email-setup': `myapi domain email-setup <domain> [--subdomain <label>] [--org <id>]
532
+
533
+ Opts in to MyAPI-managed outbound email on a subdomain of the domain.
534
+ The apex is never touched — this is safe to run on a domain whose apex
535
+ email is served by Google Workspace / Microsoft 365 / your existing provider.
536
+
537
+ Defaults the subdomain to "mail" (i.e. mail.<domain>). Pass --subdomain to
538
+ override (e.g. --subdomain=notifications for notifications.<domain>).
539
+
540
+ Backend provisions an SES identity on <subdomain>.<domain> and writes
541
+ DKIM / SPF / DMARC records to the CF zone. Track readiness via:
542
+ myapi domain status <domain> --watch`,
543
+ 'retry-provisioning': `myapi domain retry-provisioning <domain> [--org <id>]
544
+
545
+ Re-runs domain provisioning when status=infra_error and error_detail.retryable=true.
546
+ The CLI surfaces the retry hint automatically when status renders infra_error.
547
+
548
+ After triggering retry, poll: myapi domain status <domain> --watch`,
315
549
  'update-settings': `myapi domain update-settings <domain> [--security=<level>] [--browser-check=on|off] [--purge-cache] [--org <id>]
316
550
 
317
551
  --security=<level> essentially_off | low | medium | high | under_attack (default: medium)
@@ -336,6 +570,9 @@ Subcommands:
336
570
  status Get domain status
337
571
  settings Get edge/CDN settings
338
572
  update-settings Update edge/CDN settings
573
+ records Manage DNS records in the zone (list / get / create / update / delete)
574
+ email-setup Opt in to MyAPI-managed email on a subdomain (default: mail.<domain>)
575
+ retry-provisioning Re-run provisioning when status=infra_error and the failure is retryable
339
576
 
340
577
  Tip: Set defaults with "myapi config set-org <id>" / "set-domain <domain>" to skip flags on every command.`);
341
578
  return;
@@ -359,6 +596,9 @@ Tip: Set defaults with "myapi config set-org <id>" / "set-domain <domain>" to sk
359
596
  case 'status': return status(args[0], flags);
360
597
  case 'settings': return settings(args[0], flags);
361
598
  case 'update-settings': return updateSettings(args[0], flags);
599
+ case 'records': return recordsRun(args[0], args.slice(1), flags);
600
+ case 'email-setup': return emailSetup(args[0], flags);
601
+ case 'retry-provisioning': return retryProvisioning(args[0], flags);
362
602
  default: error(`Unknown subcommand: ${subcommand}. Run "myapi domain --help" for a list of valid subcommands.`);
363
603
  }
364
604
  }
package/dist/index.js CHANGED
@@ -82,12 +82,32 @@ const ERROR_MESSAGES = {
82
82
  INSUFFICIENT_BALANCE: 'Insufficient balance. Run: myapi billing topup <amount>',
83
83
  INVALID_AMOUNT: 'Amount out of range. Maximum single top-up is $100. Run: myapi billing topup <amount>',
84
84
  SERVICE_NOT_LAUNCHED: 'This service is disabled pre-launch. Track availability via: myapi status',
85
+ RECORD_NOT_FOUND: 'DNS record not found in this zone.',
86
+ INVALID_RECORD_TYPE: 'Unsupported record type. Allowed: A, AAAA, CNAME, MX, TXT.',
87
+ MX_PRIORITY_REQUIRED: 'MX records require --priority (typical value: 10).',
88
+ INVALID_TTL: 'Invalid TTL. Use the auto sentinel (1) or a value between 60 and 86400 seconds.',
89
+ INVALID_RECORD_CONTENT: 'Invalid record content for this type.',
90
+ RECORD_LIMIT_EXCEEDED: 'Cloudflare per-zone record limit reached.',
91
+ CF_API_ERROR: 'Cloudflare API error.',
85
92
  // invalid_json_response intentionally absent — the SDK's MyApiError now
86
93
  // builds a useful detailed message for that case (status + URL + body
87
94
  // snippet), and friendlyError(err.code) would override it.
88
95
  };
89
- function friendlyError(code) {
90
- return ERROR_MESSAGES[code] || code;
96
+ function friendlyError(err) {
97
+ const body = err.body ?? {};
98
+ // CF_API_ERROR: the cf_message already includes a "Cloudflare API error"
99
+ // prefix, so use it verbatim (with cf_status if present) instead of doubling.
100
+ if (err.code === 'CF_API_ERROR' && typeof body.cf_message === 'string') {
101
+ return typeof body.cf_status === 'number'
102
+ ? `${body.cf_message} (HTTP ${body.cf_status})`
103
+ : body.cf_message;
104
+ }
105
+ const base = ERROR_MESSAGES[err.code] || err.code;
106
+ // Generic fallback: append the backend's `message` when it adds info beyond
107
+ // the friendly mapping. Future per-service detail fields can be added here.
108
+ if (err.detail && err.detail !== base)
109
+ return `${base} — ${err.detail}`;
110
+ return base;
91
111
  }
92
112
  async function main() {
93
113
  // Shell autocomplete: if invoked by the shell with completion env vars,
@@ -243,7 +263,7 @@ async function main() {
243
263
  }
244
264
  }
245
265
  else
246
- error(friendlyError(err.code) || err.message);
266
+ error(friendlyError(err) || err.message);
247
267
  }
248
268
  else {
249
269
  // Don't JSON.stringify Error instances — that returns "{}" because
@@ -50,9 +50,17 @@ Resolution at register time (highest wins): `--registrant-json` → per-field fl
50
50
  | `myapi domain list [--filter all\|unassigned\|org]` | List domains in your account |
51
51
  | `myapi domain assign <domain>` | Assign domain to your default (or `--org`) org |
52
52
  | `myapi domain unassign <domain>` | Remove domain from its org |
53
- | `myapi domain status <domain>` | Registration + DNS propagation status |
53
+ | `myapi domain import <domain>` | Bring-your-own-domain. Snapshots current DNS, returns nameservers to set at your existing registrar — no registrar credentials needed |
54
+ | `myapi domain status <domain> [--watch]` | Registration + DNS propagation status. `--watch` polls until terminal (10s × 30 → 30s × 60) |
54
55
  | `myapi domain settings <domain>` | View edge/CDN settings |
55
56
  | `myapi domain update-settings <domain>` | Change security level, browser check, purge cache |
57
+ | `myapi domain records list <domain> [--type T]` | List DNS records in the zone |
58
+ | `myapi domain records get <domain> <id>` | Fetch one record |
59
+ | `myapi domain records create <domain> --type T --name n --content c` | Create a record (priority required for MX) |
60
+ | `myapi domain records update <domain> <id> [--content c] [...]` | Update a record (type cannot change) |
61
+ | `myapi domain records delete <domain> <id> --yes` | Delete a record |
62
+ | `myapi domain email-setup <domain> [--subdomain <label>]` | Opt in to MyAPI-managed email on a subdomain (default: `mail.<domain>`). Apex is never touched |
63
+ | `myapi domain retry-provisioning <domain>` | Re-run provisioning when status=infra_error and error_detail.retryable=true |
56
64
  <!-- generated:end -->
57
65
 
58
66
  ## Examples
@@ -73,6 +81,26 @@ myapi domain renew example.com
73
81
  # Tune the edge for AI bot traffic
74
82
  myapi domain update-settings example.com \
75
83
  --security=essentially_off --browser-check=off --purge-cache
84
+
85
+ # Bring an existing domain — works with any registrar (Namecheap, GoDaddy, CF Registrar, etc.)
86
+ myapi domain import example.com
87
+ # → Returns nameservers; set them at your current registrar.
88
+ myapi domain status example.com --watch
89
+ # → Polls until active. Backend live-checks Cloudflare each poll.
90
+
91
+ # Fix a record after import (e.g. clean up SPF)
92
+ myapi domain records list example.com --type TXT
93
+ myapi domain records delete example.com <bad-id> --yes
94
+ myapi domain records create example.com --type TXT --name @ \
95
+ --content 'v=spf1 include:_spf.google.com ~all'
96
+
97
+ # Opt in to MyAPI email on a subdomain (apex Google Workspace stays untouched)
98
+ myapi domain email-setup example.com # → mail.example.com
99
+ myapi domain email-setup example.com --subdomain=notifications
100
+
101
+ # Recover from infra_error
102
+ myapi domain status example.com # CLI prints the retry hint
103
+ myapi domain retry-provisioning example.com
76
104
  ```
77
105
 
78
106
  Security levels: `essentially_off` · `low` · `medium` · `high` · `under_attack`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "1.2.1",
4
+ "version": "1.2.4",
5
5
  "description": "MyAPI command-line interface",
6
6
  "type": "module",
7
7
  "files": [
@@ -27,7 +27,7 @@
27
27
  "lint:changelog": "node ../../scripts/lint-changelog.js"
28
28
  },
29
29
  "dependencies": {
30
- "@myapihq/sdk": "^1.2.1",
30
+ "@myapihq/sdk": "^1.2.4",
31
31
  "omelette": "^0.4.17"
32
32
  },
33
33
  "devDependencies": {