@faable/faable 4.12.0 → 4.14.0

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.
@@ -2,7 +2,7 @@ import { requireApi } from '../../../api/context.js';
2
2
  import { print_json, json_option } from '../../../lib/listing.js';
3
3
  import { log } from '../../../log.js';
4
4
  import { resolve_app_id } from '../resolve_app_id.js';
5
- import { cname_target } from './format.js';
5
+ import { acme_record, cname_target } from './format.js';
6
6
 
7
7
  const domains_add = {
8
8
  command: 'add <fqdn>',
@@ -37,7 +37,11 @@ const domains_add = {
37
37
  // Same shape as `domains check --json`: the record plus the CNAME to
38
38
  // create, which is the whole next step.
39
39
  if (args.json) {
40
- return print_json({ ...domain, expected_cname: cname_target(domain) });
40
+ return print_json({
41
+ ...domain,
42
+ expected_cname: cname_target(domain),
43
+ ...(args.tls ? { prevalidation_cname: acme_record(domain) } : {})
44
+ });
41
45
  }
42
46
  log.info(`🌐 Domain ${domain.fqdn} added to ${app.name} (${app_id}).`);
43
47
  log.info(``);
@@ -45,6 +49,13 @@ const domains_add = {
45
49
  log.info(` ${domain.fqdn} → ${cname_target(domain)}`);
46
50
  log.info(``);
47
51
  log.info(`Faable verifies the record automatically once DNS propagates${args.tls ? ' and then provisions the TLS certificate' : ''}.`);
52
+ if (args.tls) {
53
+ const acme = acme_record(domain);
54
+ log.info(``);
55
+ log.info(`Already serving ${domain.fqdn} somewhere else? Create this record FIRST and leave the CNAME above for later:`);
56
+ log.info(` ${acme.name} → ${acme.target}`);
57
+ log.info(`Faable then issues the certificate before any traffic moves, and switching the CNAME has no TLS gap.`);
58
+ }
48
59
  log.info(`Track it with: faable deploy domains check ${domain.fqdn}`);
49
60
  }
50
61
  };
@@ -2,7 +2,7 @@ import { requireApi } from '../../../api/context.js';
2
2
  import { print_json, json_option } from '../../../lib/listing.js';
3
3
  import { log } from '../../../log.js';
4
4
  import { resolve_app_id } from '../resolve_app_id.js';
5
- import { find_by_fqdn, cname_target, dns_badge } from './format.js';
5
+ import { find_by_fqdn, acme_record, cname_target, dns_badge, tls_line } from './format.js';
6
6
 
7
7
  const domains_check = {
8
8
  command: 'check <fqdn>',
@@ -30,7 +30,11 @@ const domains_check = {
30
30
  }
31
31
  // The record plus the one thing it does not carry: the CNAME to create.
32
32
  if (args.json) {
33
- return print_json({ ...domain, expected_cname: cname_target(domain) });
33
+ return print_json({
34
+ ...domain,
35
+ expected_cname: cname_target(domain),
36
+ ...(domain.tls ? { prevalidation_cname: acme_record(domain) } : {})
37
+ });
34
38
  }
35
39
  const status = domain.status;
36
40
  log.info(`🌐 ${domain.fqdn} — ${dns_badge(domain)}`);
@@ -44,6 +48,13 @@ const domains_check = {
44
48
  if (status?.dns_message) {
45
49
  log.info(` Diagnostic: ${status.dns_message}`);
46
50
  }
51
+ if (domain.tls) {
52
+ const acme = acme_record(domain);
53
+ log.info(` Pre-validation: ${acme.name} → ${acme.target} ${acme.ready
54
+ ? '✅ in place: the certificate is issued without waiting for the CNAME'
55
+ : '(optional; for a domain live elsewhere, so the move has no TLS gap)'}`);
56
+ }
57
+ log.info(` Certificate: ${tls_line(domain)}`);
47
58
  if (status?.dns_checked_at) {
48
59
  log.info(` Last checked: ${status.dns_checked_at}`);
49
60
  }
@@ -19,6 +19,27 @@ const dns_badge = (domain) => {
19
19
  // value); fall back to deriving it from the id so `add` can print
20
20
  // instructions even before the first DNS check populates the status.
21
21
  const cname_target = (domain) => domain.status?.dns_expected?.[0] ?? `${domain.id}.faable.link`;
22
+ // The optional record that gets the certificate issued BEFORE the domain
23
+ // points here: a domain that is live somewhere else moves without a TLS gap.
24
+ // Published by the api in `status.acme_expected`; derived from the id until
25
+ // the first check fills it in, like the CNAME above.
26
+ const acme_record = (domain) => ({
27
+ name: `_acme-challenge.${domain.fqdn}`,
28
+ target: domain.status?.acme_expected?.[0] ?? `${domain.id}.acme.faable.com`,
29
+ ready: domain.status?.acme_state === 'ok'
30
+ });
31
+ // The certificate line of `domains check`: what the edge serves today.
32
+ const tls_line = (domain) => {
33
+ if (!domain.tls)
34
+ return 'off (tls: false)';
35
+ if (domain.status?.tls_state === 'ok') {
36
+ const until = domain.status.tls_expires_at?.slice(0, 10);
37
+ return `✅ served${until ? `, valid until ${until}` : ''}`;
38
+ }
39
+ return domain.status?.acme_state === 'ok' || domain.status?.dns_state === 'ok'
40
+ ? '⏳ being issued — usually a couple of minutes'
41
+ : '⏳ issued once the CNAME (or the pre-validation record) is in place';
42
+ };
22
43
  const find_by_fqdn = (domains, fqdn) => domains.find(d => d.fqdn.toLowerCase() === fqdn.toLowerCase());
23
44
 
24
- export { cname_target, dns_badge, find_by_fqdn };
45
+ export { acme_record, cname_target, dns_badge, find_by_fqdn, tls_line };
package/dist/mcp/tools.js CHANGED
@@ -352,7 +352,7 @@ const TOOLS = [
352
352
  {
353
353
  name: 'check_domain',
354
354
  title: 'Check a domain',
355
- description: 'DNS diagnostic of one custom domain: the CNAME it expects, what DNS answers today, and why it is not verified yet.',
355
+ description: 'DNS diagnostic of one custom domain: the CNAME it expects, what DNS answers today, and why it is not verified yet. `prevalidation_cname.ready` says whether the optional `_acme-challenge` record is in place, so the certificate is issued without waiting for the CNAME; `status.tls_state` is `ok` once the edge serves a valid certificate for the domain (`status.tls_expires_at`).',
356
356
  input: {
357
357
  app,
358
358
  project,
@@ -782,7 +782,7 @@ const TOOLS = [
782
782
  {
783
783
  name: 'add_domain',
784
784
  title: 'Add a custom domain',
785
- description: 'Attach a custom domain to an app. Returns the CNAME record the user must create at their DNS provider; Faable verifies it and issues the certificate on its own.',
785
+ description: "Attach a custom domain to an app. Returns the CNAME record the user must create at their DNS provider; Faable verifies it and issues the certificate on its own. If the domain already serves traffic somewhere else, tell the user to create `prevalidation_cname` (`_acme-challenge.<fqdn>` CNAME) FIRST and switch the main CNAME only once check_domain shows it ready: the certificate then exists before traffic moves, with no TLS gap.",
786
786
  input: { app, project, fqdn: z.string().describe('e.g. www.example.com') },
787
787
  command: ['deploy', 'domains', 'add'],
788
788
  flags: a => [a.fqdn, ...target(a)],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "4.12.0",
3
+ "version": "4.14.0",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",