@faable/faable 1.27.0 → 1.28.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.
- package/dist/api/FaableApi.js +24 -0
- package/dist/commands/deploy/domains/add.js +46 -0
- package/dist/commands/deploy/domains/check.js +51 -0
- package/dist/commands/deploy/domains/format.js +24 -0
- package/dist/commands/deploy/domains/index.js +21 -0
- package/dist/commands/deploy/domains/list.js +45 -0
- package/dist/commands/deploy/domains/rm.js +59 -0
- package/dist/commands/deploy/index.js +2 -0
- package/package.json +1 -1
package/dist/api/FaableApi.js
CHANGED
|
@@ -169,6 +169,30 @@ class FaableApi {
|
|
|
169
169
|
async getMe() {
|
|
170
170
|
return data(this.client.get(`/auth/me`));
|
|
171
171
|
}
|
|
172
|
+
// Domains are team-scoped rows; a CLI user token carries no default team,
|
|
173
|
+
// so every call pins the app's team via `x-faable-team` (same pattern as
|
|
174
|
+
// createSecretsBatch).
|
|
175
|
+
async listDomains(app_id, team) {
|
|
176
|
+
return firstPage(data(this.client.get(`/domain`, {
|
|
177
|
+
params: { app_id },
|
|
178
|
+
headers: { "x-faable-team": team },
|
|
179
|
+
})));
|
|
180
|
+
}
|
|
181
|
+
async createDomain(team, params) {
|
|
182
|
+
return data(this.client.post(`/domain`, params, {
|
|
183
|
+
headers: { "x-faable-team": team },
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
async getDomain(domain_id, team) {
|
|
187
|
+
return data(this.client.get(`/domain/${domain_id}`, {
|
|
188
|
+
headers: { "x-faable-team": team },
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
async deleteDomain(domain_id, team) {
|
|
192
|
+
return data(this.client.delete(`/domain/${domain_id}`, {
|
|
193
|
+
headers: { "x-faable-team": team },
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
export { FaableApi };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { log } from '../../../log.js';
|
|
3
|
+
import { resolve_app_id } from '../resolve_app_id.js';
|
|
4
|
+
import { cname_target } from './format.js';
|
|
5
|
+
|
|
6
|
+
const domains_add = {
|
|
7
|
+
command: 'add <fqdn>',
|
|
8
|
+
describe: 'Add a custom domain to the app',
|
|
9
|
+
builder: yargs => yargs
|
|
10
|
+
.positional('fqdn', {
|
|
11
|
+
type: 'string',
|
|
12
|
+
demandOption: true,
|
|
13
|
+
description: 'Fully qualified domain name (e.g. www.example.com)'
|
|
14
|
+
})
|
|
15
|
+
.option('app', {
|
|
16
|
+
alias: 'a',
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
19
|
+
})
|
|
20
|
+
.option('tls', {
|
|
21
|
+
type: 'boolean',
|
|
22
|
+
default: true,
|
|
23
|
+
description: 'Provision a TLS certificate automatically (default)'
|
|
24
|
+
})
|
|
25
|
+
.example('$0 deploy domains add www.example.com', 'Attach www.example.com to the linked app')
|
|
26
|
+
.showHelpOnFail(false),
|
|
27
|
+
handler: async (args) => {
|
|
28
|
+
const ctx = await requireApi();
|
|
29
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
30
|
+
const app = await ctx.api.getApp(app_id);
|
|
31
|
+
const domain = await ctx.api.createDomain(app.team, {
|
|
32
|
+
fqdn: args.fqdn,
|
|
33
|
+
app_id,
|
|
34
|
+
tls: args.tls
|
|
35
|
+
});
|
|
36
|
+
log.info(`🌐 Domain ${domain.fqdn} added to ${app.name} (${app_id}).`);
|
|
37
|
+
log.info(``);
|
|
38
|
+
log.info(`Now create a CNAME record at your DNS provider:`);
|
|
39
|
+
log.info(` ${domain.fqdn} → ${cname_target(domain)}`);
|
|
40
|
+
log.info(``);
|
|
41
|
+
log.info(`Faable verifies the record automatically once DNS propagates${args.tls ? ' and then provisions the TLS certificate' : ''}.`);
|
|
42
|
+
log.info(`Track it with: faable deploy domains check ${domain.fqdn}`);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export { domains_add };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { log } from '../../../log.js';
|
|
3
|
+
import { resolve_app_id } from '../resolve_app_id.js';
|
|
4
|
+
import { find_by_fqdn, dns_badge, cname_target } from './format.js';
|
|
5
|
+
|
|
6
|
+
const domains_check = {
|
|
7
|
+
command: 'check <fqdn>',
|
|
8
|
+
describe: 'Show the DNS verification status of a domain',
|
|
9
|
+
builder: yargs => yargs
|
|
10
|
+
.positional('fqdn', {
|
|
11
|
+
type: 'string',
|
|
12
|
+
demandOption: true,
|
|
13
|
+
description: 'Domain to check'
|
|
14
|
+
})
|
|
15
|
+
.option('app', {
|
|
16
|
+
alias: 'a',
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
19
|
+
})
|
|
20
|
+
.showHelpOnFail(false),
|
|
21
|
+
handler: async (args) => {
|
|
22
|
+
const ctx = await requireApi();
|
|
23
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
24
|
+
const app = await ctx.api.getApp(app_id);
|
|
25
|
+
const domains = await ctx.api.listDomains(app_id, app.team);
|
|
26
|
+
const domain = find_by_fqdn(domains, args.fqdn);
|
|
27
|
+
if (!domain) {
|
|
28
|
+
throw new Error(`Domain ${args.fqdn} is not attached to ${app_id}. List them with "faable deploy domains list".`);
|
|
29
|
+
}
|
|
30
|
+
const status = domain.status;
|
|
31
|
+
log.info(`🌐 ${domain.fqdn} — ${dns_badge(domain)}`);
|
|
32
|
+
log.info(` Expected CNAME: ${domain.fqdn} → ${cname_target(domain)}`);
|
|
33
|
+
if (status?.dns_observed?.length) {
|
|
34
|
+
log.info(` Observed: ${status.dns_observed.join(', ')}`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
log.info(` Observed: (no CNAME resolved yet)`);
|
|
38
|
+
}
|
|
39
|
+
if (status?.dns_message) {
|
|
40
|
+
log.info(` Diagnostic: ${status.dns_message}`);
|
|
41
|
+
}
|
|
42
|
+
if (status?.dns_checked_at) {
|
|
43
|
+
log.info(` Last checked: ${status.dns_checked_at}`);
|
|
44
|
+
}
|
|
45
|
+
if (!domain.verified) {
|
|
46
|
+
log.info(`Faable re-checks automatically — no action needed beyond the CNAME record.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export { domains_check };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Human summary of a domain's verification state, derived from the DNS
|
|
2
|
+
// verification worker's status. Kept pure for tests.
|
|
3
|
+
const dns_badge = (domain) => {
|
|
4
|
+
if (domain.verified)
|
|
5
|
+
return '✅ verified';
|
|
6
|
+
switch (domain.status?.dns_state) {
|
|
7
|
+
case 'ok':
|
|
8
|
+
return '✅ dns ok';
|
|
9
|
+
case 'misconfigured':
|
|
10
|
+
return '❌ misconfigured';
|
|
11
|
+
case 'error':
|
|
12
|
+
return '❌ dns error';
|
|
13
|
+
default:
|
|
14
|
+
return '⏳ pending verification';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
// The CNAME target the user must configure. The API publishes it in
|
|
18
|
+
// `status.dns_expected` (the dashboard shows `<domain.id>.faable.link` — same
|
|
19
|
+
// value); fall back to deriving it from the id so `add` can print
|
|
20
|
+
// instructions even before the first DNS check populates the status.
|
|
21
|
+
const cname_target = (domain) => domain.status?.dns_expected?.[0] ?? `${domain.id}.faable.link`;
|
|
22
|
+
const find_by_fqdn = (domains, fqdn) => domains.find(d => d.fqdn.toLowerCase() === fqdn.toLowerCase());
|
|
23
|
+
|
|
24
|
+
export { cname_target, dns_badge, find_by_fqdn };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { domains_add } from './add.js';
|
|
2
|
+
import { domains_check } from './check.js';
|
|
3
|
+
import { domains_list } from './list.js';
|
|
4
|
+
import { domains_rm } from './rm.js';
|
|
5
|
+
|
|
6
|
+
const domains = {
|
|
7
|
+
command: 'domains <command>',
|
|
8
|
+
describe: 'Manage custom domains of an app',
|
|
9
|
+
builder: yargs => yargs
|
|
10
|
+
.command(domains_list)
|
|
11
|
+
.command(domains_add)
|
|
12
|
+
.command(domains_check)
|
|
13
|
+
.command(domains_rm)
|
|
14
|
+
.demandCommand(1, 'Specify a domains command: list, add, check or rm'),
|
|
15
|
+
handler: () => {
|
|
16
|
+
// Unreachable: demandCommand(1) either routes to a subcommand or fails
|
|
17
|
+
// through the global .fail() in src/index.ts.
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export { domains };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { log } from '../../../log.js';
|
|
3
|
+
import { resolve_app_id } from '../resolve_app_id.js';
|
|
4
|
+
import { dns_badge, cname_target } from './format.js';
|
|
5
|
+
|
|
6
|
+
const domains_list = {
|
|
7
|
+
command: 'list',
|
|
8
|
+
describe: 'List custom domains of the app',
|
|
9
|
+
builder: yargs => yargs
|
|
10
|
+
.option('app', {
|
|
11
|
+
alias: 'a',
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
14
|
+
})
|
|
15
|
+
.example('$0 deploy domains list', 'Domains of the linked app')
|
|
16
|
+
.showHelpOnFail(false),
|
|
17
|
+
handler: async (args) => {
|
|
18
|
+
const ctx = await requireApi();
|
|
19
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
20
|
+
const app = await ctx.api.getApp(app_id);
|
|
21
|
+
const domains = await ctx.api.listDomains(app_id, app.team);
|
|
22
|
+
if (domains.length === 0) {
|
|
23
|
+
log.info(`🌐 No custom domains for ${app.name} (${app_id}).`);
|
|
24
|
+
log.info(`Add one with: faable deploy domains add <yourdomain.com>. The app is always live at https://${app.url}.`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
log.info(`🌐 ${domains.length} domain(s) for ${app.name} (${app_id}):`);
|
|
28
|
+
const width = Math.max(...domains.map(d => d.fqdn.length));
|
|
29
|
+
for (const domain of domains) {
|
|
30
|
+
const tls = domain.tls ? 'tls' : 'no-tls';
|
|
31
|
+
log.info(` ${domain.fqdn.padEnd(width)} ${dns_badge(domain)} (${tls})`);
|
|
32
|
+
}
|
|
33
|
+
const unverified = domains.filter(d => !d.verified);
|
|
34
|
+
if (unverified.length > 0) {
|
|
35
|
+
log.info(``);
|
|
36
|
+
log.info(`To finish verification, point each domain at Faable with a CNAME:`);
|
|
37
|
+
for (const domain of unverified) {
|
|
38
|
+
log.info(` ${domain.fqdn} → ${cname_target(domain)}`);
|
|
39
|
+
}
|
|
40
|
+
log.info(`Run "faable deploy domains check <fqdn>" to see the diagnostic.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export { domains_list };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import { requireApi } from '../../../api/context.js';
|
|
3
|
+
import { log } from '../../../log.js';
|
|
4
|
+
import { resolve_app_id } from '../resolve_app_id.js';
|
|
5
|
+
import { find_by_fqdn } from './format.js';
|
|
6
|
+
|
|
7
|
+
const domains_rm = {
|
|
8
|
+
command: 'rm <fqdn>',
|
|
9
|
+
describe: 'Remove a custom domain from the app',
|
|
10
|
+
builder: yargs => yargs
|
|
11
|
+
.positional('fqdn', {
|
|
12
|
+
type: 'string',
|
|
13
|
+
demandOption: true,
|
|
14
|
+
description: 'Domain to remove'
|
|
15
|
+
})
|
|
16
|
+
.option('app', {
|
|
17
|
+
alias: 'a',
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
20
|
+
})
|
|
21
|
+
.option('yes', {
|
|
22
|
+
alias: 'y',
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
default: false,
|
|
25
|
+
description: 'Skip the confirmation prompt'
|
|
26
|
+
})
|
|
27
|
+
.example('$0 deploy domains rm www.example.com', 'Detach after confirmation')
|
|
28
|
+
.showHelpOnFail(false),
|
|
29
|
+
handler: async (args) => {
|
|
30
|
+
const ctx = await requireApi();
|
|
31
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
32
|
+
const app = await ctx.api.getApp(app_id);
|
|
33
|
+
const domains = await ctx.api.listDomains(app_id, app.team);
|
|
34
|
+
const domain = find_by_fqdn(domains, args.fqdn);
|
|
35
|
+
if (!domain) {
|
|
36
|
+
throw new Error(`Domain ${args.fqdn} is not attached to ${app_id}. List them with "faable deploy domains list".`);
|
|
37
|
+
}
|
|
38
|
+
if (!args.yes) {
|
|
39
|
+
// In a non-TTY run without --yes, prompts resolves undefined → cancel.
|
|
40
|
+
const { confirm } = await prompts({
|
|
41
|
+
type: 'toggle',
|
|
42
|
+
name: 'confirm',
|
|
43
|
+
message: `Remove domain "${domain.fqdn}" from ${app.name} (${app_id})? Traffic to it will stop being served.`,
|
|
44
|
+
initial: false,
|
|
45
|
+
active: 'yes',
|
|
46
|
+
inactive: 'no'
|
|
47
|
+
});
|
|
48
|
+
if (!confirm) {
|
|
49
|
+
log.info('Cancelled.');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
await ctx.api.deleteDomain(domain.id, app.team);
|
|
54
|
+
log.info(`🗑️ Removed domain ${domain.fqdn} from ${app_id}.`);
|
|
55
|
+
log.info(`The app stays live at https://${app.url}. Remember to delete the CNAME at your DNS provider.`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export { domains_rm };
|
|
@@ -2,6 +2,7 @@ import { requireApi } from '../../api/context.js';
|
|
|
2
2
|
import { Configuration } from '../../lib/Configuration.js';
|
|
3
3
|
import { log } from '../../log.js';
|
|
4
4
|
import { link } from '../link/index.js';
|
|
5
|
+
import { domains } from './domains/index.js';
|
|
5
6
|
import { git_context } from './git_context.js';
|
|
6
7
|
import { propose_release } from './release_version.js';
|
|
7
8
|
import { deploy_remote } from './remote/index.js';
|
|
@@ -17,6 +18,7 @@ const deploy = {
|
|
|
17
18
|
// app_id positional, so `faable deploy <app_id>` keeps working).
|
|
18
19
|
return yargs
|
|
19
20
|
.command(secrets)
|
|
21
|
+
.command(domains)
|
|
20
22
|
.command(link)
|
|
21
23
|
.positional('app_id', {
|
|
22
24
|
type: 'string',
|