@vigilbase/cli 0.2.4 → 0.2.6

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 (32) hide show
  1. package/CLOUDFLARE.md +9 -2
  2. package/COMMANDS.md +5 -5
  3. package/LOCAL.md +87 -12
  4. package/ONE.md +2 -2
  5. package/README.md +13 -10
  6. package/dist/commands/cloudflare.js +79 -13
  7. package/dist/commands/helpers.d.ts +4 -1
  8. package/dist/commands/local.js +44 -23
  9. package/dist/lib/checkup-report-limits.d.ts +16 -0
  10. package/dist/lib/checkup-report-limits.js +16 -0
  11. package/dist/lib/checkup-report.d.ts +118 -0
  12. package/dist/lib/checkup-report.js +503 -0
  13. package/dist/lib/format.d.ts +2 -0
  14. package/dist/lib/format.js +1 -1
  15. package/dist/lib/one-error-hints.d.ts +1 -0
  16. package/dist/lib/one-error-hints.js +10 -0
  17. package/dist/lib/tui.d.ts +4 -0
  18. package/dist/lib/tui.js +2 -2
  19. package/dist/local/cloudflare-checkup/cf/allowlist.js +5 -2
  20. package/dist/local/cloudflare-checkup/collector.js +5 -5
  21. package/dist/local/cloudflare-checkup/contract.d.ts +481 -20
  22. package/dist/local/cloudflare-checkup/contract.js +111 -27
  23. package/dist/local/cloudflare-checkup/email-verification.d.ts +8 -1
  24. package/dist/local/cloudflare-checkup/email-verification.js +0 -5
  25. package/dist/local/cloudflare-checkup/identity-verification.d.ts +11 -0
  26. package/dist/local/cloudflare-checkup/identity-verification.js +65 -0
  27. package/dist/local/cloudflare-checkup/snapshot-file.js +13 -1
  28. package/dist/local/cloudflare-checkup/submit-client.d.ts +6 -3
  29. package/dist/local/cloudflare-checkup/submit-client.js +17 -4
  30. package/dist/local/cloudflare-checkup/terminal-ui.d.ts +10 -3
  31. package/dist/local/cloudflare-checkup/terminal-ui.js +13 -3
  32. package/package.json +1 -1
@@ -3,9 +3,9 @@
3
3
  * ≡ `vigilbase local cloudflare-checkup [run] [options]`, plus the advanced
4
4
  * `login|doctor|accounts|collect|inspect|submit|logout` subcommands.
5
5
  *
6
- * The one command verifies the user's work email (vigilbase.com emails a
7
- * 6-digit code), finds (or, after asking, installs) Cloudflare's cf CLI, signs
8
- * in to Cloudflare with a read-only cf login, lets the user choose the
6
+ * The one command uses the signed-in Vigilbase identity, or verifies a work
7
+ * email with a 6-digit code, then finds (or, after asking, installs) Cloudflare's
8
+ * cf CLI, signs in to Cloudflare with a read-only cf login, lets the user choose the
9
9
  * account, collects a minimised snapshot with a fixed allowlist of read-only
10
10
  * cf commands, shows what it contains, uploads exactly that file to
11
11
  * vigilbase.com after an explicit confirmation and opens the results page.
@@ -17,9 +17,12 @@ import { homedir } from 'node:os';
17
17
  import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
18
18
  import { z } from 'zod';
19
19
  import { CLI_VERSION } from '../version.js';
20
+ import { verifySignedInRequester } from '../local/cloudflare-checkup/identity-verification.js';
21
+ import { localResultPlainLines, readCheckupReport } from '../lib/checkup-report.js';
20
22
  import { resolveConfigDir } from '../lib/config.js';
21
23
  import { isHeadlessEnvironment, openUrlInBrowser } from '../lib/open-url.js';
22
24
  import { shouldShowProgress } from '../lib/progress.js';
25
+ import { UNICODE_SYMBOLS, createStyles } from '../lib/tui.js';
23
26
  import { CF_SUPPORTED, NOT_COLLECTABLE_ZONE_KINDS, PRODUCTION_ORIGIN, RECOMMENDED_CF_SCOPES, SNAPSHOT_COLLECTOR_NAME, SNAPSHOT_LIMITS, SNAPSHOT_SCHEMA_VERSION, } from '../local/cloudflare-checkup/contract.js';
24
27
  import { CF_DAMAGED_HINT, CF_NOT_FOUND_ADVANCED_HINT, ENV_TOKEN_AUTH_HINT, LOCAL_ERROR_HINTS, LocalCheckupError, SNAPSHOT_STALE_FILE_HINT, } from '../local/cloudflare-checkup/errors.js';
25
28
  import { CF_TOKEN_ENV, buildChildEnv, childEnvVariableNames, hasEnvToken, missingEnvTokenMessage, strippedTokenNotice, } from '../local/cloudflare-checkup/cf/child-env.js';
@@ -53,15 +56,17 @@ Runs the Vigilbase Cloudflare Checkup on this machine, in one command:
53
56
 
54
57
  ${NPX_COMMAND} cloudflare-checkup
55
58
 
56
- It asks for your work email and emails you a 6-digit code to enter, installs
57
- Cloudflare's cf CLI if it is missing (asks first), signs in to Cloudflare with
59
+ It uses your Vigilbase login, skipping email prompts and codes. Signed out,
60
+ it verifies your work email with a 6-digit code. It installs Cloudflare's cf
61
+ CLI if it is missing (asks first), signs in to Cloudflare with
58
62
  a read-only cf login, lets you choose the account, collects a snapshot, shows
59
63
  what it contains, uploads it only after you confirm and opens the results.
60
64
  cf owns your Cloudflare login; this CLI never reads, prints, stores or
61
65
  uploads it.
62
66
 
63
67
  Options (the default command, also called run):
64
- --email <address> Your work email (asked for when omitted)
68
+ --email <address> Override the signed-in email; a different
69
+ address requires email verification
65
70
  --account-id <id> Skip the account picker
66
71
  --out <file> Where to save the snapshot (default:
67
72
  <config dir>/checkups/…); --overwrite
@@ -81,8 +86,8 @@ Options (the default command, also called run):
81
86
  (default) or a loopback origin
82
87
  --timeout <milliseconds> · --json
83
88
 
84
- Without a terminal, add --yes: the email (unless --email is given) and the
85
- code are read from stdin, one line each, after their prompts on stderr.
89
+ Without a terminal, add --yes. When email verification is needed, the email
90
+ (unless --email is given) and code are read from stdin after their prompts.
86
91
 
87
92
  Advanced commands:
88
93
  login Create or refresh the read-only cf profile
@@ -93,7 +98,7 @@ Advanced commands:
93
98
  collect --out <file> Collect only [--overwrite] [--account-id];
94
99
  --dry-run shows every cf command (offline)
95
100
  inspect <file> Validate a snapshot and show what it contains
96
- submit <file> Verify your email, then upload a snapshot
101
+ submit <file> Identify yourself, then upload a snapshot
97
102
  [--email] [--yes|--confirm] [--no-open]
98
103
  [--base-url] [--timeout]
99
104
  logout Delete the read-only cf profile and the cf
@@ -1272,14 +1277,23 @@ function assertWithinLimits(server, loaded) {
1272
1277
  throw new LocalCheckupError('SNAPSHOT_TOO_LARGE', `The snapshot is over the vigilbase.com limit (${formatBytes(maxBytes)}, ${maxZones} zones). Nothing was uploaded.`);
1273
1278
  }
1274
1279
  }
1275
- /**
1276
- * The email verification (prompts on stderr, answers from piped stdin
1277
- * without a terminal), then `✓ Verified j***@acme.com`. No address is sent
1278
- * before one is entered, and cf is not touched before it is verified.
1279
- */
1280
+ /** Use the live signed-in identity, or verify an address when no matching login is available. */
1280
1281
  async function verifyRequester(ctx, options, origin) {
1282
+ const client = new VerificationClient(clientOptions(ctx, origin));
1283
+ const configDir = options.configDir;
1284
+ const identity = configDir ? await busy(ctx, 'Using your Vigilbase identity', () => verifySignedInRequester({
1285
+ config: { ...ctx.config, configDir }, client, locale: emailLocale(ctx.env), email: options.email,
1286
+ warn: (message) => ctx.writer.stderr(`Warning: ${message}`),
1287
+ })) : null;
1288
+ if (identity) {
1289
+ if (ctx.tui)
1290
+ ctx.tui.step(`Signed in as ${identity.requester}`);
1291
+ else
1292
+ say(ctx, `✓ Signed in as ${identity.requester}`);
1293
+ return identity;
1294
+ }
1281
1295
  const verified = await verifyEmail({
1282
- client: new VerificationClient(clientOptions(ctx, origin)),
1296
+ client,
1283
1297
  io: promptIO(ctx),
1284
1298
  locale: emailLocale(ctx.env),
1285
1299
  ...(options.email !== undefined ? { email: options.email } : {}),
@@ -1293,10 +1307,10 @@ async function verifyRequester(ctx, options, origin) {
1293
1307
  return verified;
1294
1308
  }
1295
1309
  /**
1296
- * The checkup the verified email started. A session that expired (a long
1297
- * collection, or a slow answer to the upload question) is renewed once with
1298
- * the token and code still in memory; vigilbase.com mints the next session
1299
- * code for the same checkup. The snapshot file is kept either way.
1310
+ * The checkup the verified requester started. An expired session is renewed
1311
+ * once, using a fresh identity proof or the email token and code still in
1312
+ * memory. vigilbase.com mints the next session code for the same checkup.
1313
+ * The snapshot file is kept either way.
1300
1314
  */
1301
1315
  class CheckupSession {
1302
1316
  ctx;
@@ -1323,7 +1337,7 @@ class CheckupSession {
1323
1337
  }
1324
1338
  catch (error) {
1325
1339
  if (error instanceof LocalCheckupError && (error.code === 'VERIFICATION_EXPIRED' || error.code === 'VERIFICATION_INVALID')) {
1326
- throw new LocalCheckupError('SESSION_EXPIRED', 'The checkup session expired, and the code from the email can no longer renew it.', error.status !== undefined ? { status: error.status } : {});
1340
+ throw new LocalCheckupError('SESSION_EXPIRED', 'The checkup session expired, and its identity verification can no longer renew it.', error.status !== undefined ? { status: error.status } : {});
1327
1341
  }
1328
1342
  throw error;
1329
1343
  }
@@ -1365,7 +1379,7 @@ function noteSavedSnapshot(ctx, error, path, origin) {
1365
1379
  if (ctx.tui)
1366
1380
  ctx.tui.uploadLater(submit);
1367
1381
  else
1368
- ctx.writer.stderr(`The snapshot stays saved. To upload it later without collecting again, run ${submit} (it verifies your email again).`);
1382
+ ctx.writer.stderr(`The snapshot stays saved. To upload it later without collecting again, run ${submit} (it checks your identity again).`);
1369
1383
  }
1370
1384
  /** Session checks that need no upload: CLI version, single use, and the bound account. */
1371
1385
  function assertSessionUsable(session, accountId) {
@@ -1515,7 +1529,7 @@ async function runInspect(ctx, options) {
1515
1529
  return ` ${kind.padEnd(34)} ${String(kindCounts.total).padStart(5)}${notOk ? ` (${notOk} not ok)` : ''}`;
1516
1530
  }),
1517
1531
  'Free text and identifiers in this file:',
1518
- ` member emails ${categories.memberEmails} · access-rule values ${categories.accessRuleValues} · non-proxied DNS names ${categories.dnsHostnames} · pseudonymised IPs ${categories.pseudonymizedIps} · rule descriptions ${categories.ruleDescriptions}`,
1532
+ ` member emails ${categories.memberEmails} · access-rule values ${categories.accessRuleValues} · DNS and certificate hostnames ${categories.dnsHostnames} · pseudonymised IPs ${categories.pseudonymizedIps} · rule descriptions ${categories.ruleDescriptions}`,
1519
1533
  'No credentials are included, and origin IPs are pseudonymised.',
1520
1534
  ];
1521
1535
  for (const line of lines)
@@ -1635,6 +1649,10 @@ async function uploadLoaded(ctx, input) {
1635
1649
  ].filter((part) => part !== null);
1636
1650
  const summary = `${result.replayed ? '✓ Already uploaded (same file); nothing changed.' : '✓ Uploaded.'} Free summary: ${parts.join(' · ')}`;
1637
1651
  const emailed = result.resultsUrl !== undefined ? `A link has also been emailed to ${requester}.` : `A link to the results has been emailed to ${requester}.`;
1652
+ // The free checks the server sent (names and zone counts, never zone names) and its premium count.
1653
+ const report = readCheckupReport(result.report);
1654
+ // Appended after the lines above, which stay as they were for scripts.
1655
+ const more = report ? localResultPlainLines(report, { style: createStyles(false), symbols: UNICODE_SYMBOLS, color: false, columns: null }) : [];
1638
1656
  if (ctx.tui) {
1639
1657
  const opening = result.resultsUrl !== undefined && openResults(ctx, result.resultsUrl, origin);
1640
1658
  ctx.tui.uploaded({
@@ -1643,7 +1661,8 @@ async function uploadLoaded(ctx, input) {
1643
1661
  ...(warnings !== undefined ? { warnings } : {}),
1644
1662
  ...(failedChecks !== undefined ? { failed: failedChecks } : {}),
1645
1663
  ...(result.resultsUrl !== undefined ? { resultsUrl: result.resultsUrl } : {}),
1646
- }, [summary, ...(result.resultsUrl !== undefined ? [`Results: ${result.resultsUrl}`] : []), emailed]);
1664
+ ...(report ? { report } : {}),
1665
+ }, [summary, ...(result.resultsUrl !== undefined ? [`Results: ${result.resultsUrl}`] : []), emailed, ...more]);
1647
1666
  }
1648
1667
  else {
1649
1668
  ctx.writer.stdout(summary);
@@ -1652,6 +1671,8 @@ async function uploadLoaded(ctx, input) {
1652
1671
  openResults(ctx, result.resultsUrl, origin);
1653
1672
  }
1654
1673
  ctx.writer.stdout(emailed);
1674
+ for (const line of more)
1675
+ ctx.writer.stdout(line);
1655
1676
  }
1656
1677
  }
1657
1678
  return {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The Cloudflare Checkup list caps the CLI prints, from `CHECKUP_REPORT_LIMITS`
3
+ * in packages/tools/src/pdf/cloudflare-checkup-report-limits.ts, so the
4
+ * terminal cuts long lists where the web report, the PDF and the emails do.
5
+ * The CLI is published to npm and cannot depend on the private
6
+ * `@vigilbase/tools` package; checkup-report-limits.test.ts keeps these values
7
+ * equal to the source.
8
+ */
9
+ export declare const CLI_CHECKUP_LIMITS: {
10
+ /** Top priorities of `cf checkup result` and `cf checkup` (`priorities.cli`). */
11
+ readonly priorities: 8;
12
+ /** Their domain table rows (`cliDomains`). */
13
+ readonly domains: 15;
14
+ /** Top findings on the local checkup's result card (`priorities.cliCard`). */
15
+ readonly card: 5;
16
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The Cloudflare Checkup list caps the CLI prints, from `CHECKUP_REPORT_LIMITS`
3
+ * in packages/tools/src/pdf/cloudflare-checkup-report-limits.ts, so the
4
+ * terminal cuts long lists where the web report, the PDF and the emails do.
5
+ * The CLI is published to npm and cannot depend on the private
6
+ * `@vigilbase/tools` package; checkup-report-limits.test.ts keeps these values
7
+ * equal to the source.
8
+ */
9
+ export const CLI_CHECKUP_LIMITS = {
10
+ /** Top priorities of `cf checkup result` and `cf checkup` (`priorities.cli`). */
11
+ priorities: 8,
12
+ /** Their domain table rows (`cliDomains`). */
13
+ domains: 15,
14
+ /** Top findings on the local checkup's result card (`priorities.cliCard`). */
15
+ card: 5,
16
+ };
@@ -0,0 +1,118 @@
1
+ import { type Styles, type Symbols } from './tui.js';
2
+ export type CheckupCounts = {
3
+ total: number | null;
4
+ passed: number | null;
5
+ warnings: number | null;
6
+ failed: number | null;
7
+ skipped: number | null;
8
+ };
9
+ /** One check ID's failing or warning instances, across the account and its zones. */
10
+ export type CheckupFinding = {
11
+ name: string;
12
+ /** The server's label (`critical`, `high`, …), as sent. */
13
+ severity: string | null;
14
+ /** `failed` (fail or error) or `warning`; null for a legacy report's issue, which has no status. */
15
+ status: 'failed' | 'warning' | null;
16
+ /** Distinct zones with this finding. */
17
+ zones: number;
18
+ /** An account-level check. */
19
+ account: boolean;
20
+ /** The first line of the server's recommendation. */
21
+ recommendation: string | null;
22
+ };
23
+ export type CheckupZoneRow = {
24
+ name: string;
25
+ plan: string | null;
26
+ status: string | null;
27
+ grade: string | null;
28
+ score: number | null;
29
+ passed: number | null;
30
+ warnings: number | null;
31
+ failed: number | null;
32
+ /** A legacy report's issue count (it has no pass/warning/fail summary). */
33
+ issues: number | null;
34
+ };
35
+ export type CheckupReportView = {
36
+ accountName: string | null;
37
+ zoneCount: number | null;
38
+ scannedZones: number | null;
39
+ grade: string | null;
40
+ score: number | null;
41
+ summary: CheckupCounts | null;
42
+ zones: CheckupZoneRow[];
43
+ /** Ordered: failed before warnings, then by the server's severity, then the widest. */
44
+ findings: CheckupFinding[];
45
+ /** Premium checks the locked projection holds back. */
46
+ lockedPremiumCount: number;
47
+ /** The results page, only when it is an https://vigilbase.com URL. */
48
+ shareableUrl: string | null;
49
+ scannedAt: string | null;
50
+ };
51
+ /**
52
+ * The view of a checkup result (`CheckupResult`, its locked projection, or a
53
+ * legacy One report), or null when `value` is not an object. Malformed parts
54
+ * are left out rather than failing the whole view.
55
+ */
56
+ export declare function readCheckupReport(value: unknown): CheckupReportView | null;
57
+ /** How the lines are drawn: styles (identity without colour), symbols (ASCII on legacy consoles) and the width to fit, if any. */
58
+ export type ReportPalette = {
59
+ style: Styles;
60
+ symbols: Symbols;
61
+ /** Colours on: the bar shows warnings and failed checks in their colours. */
62
+ color: boolean;
63
+ /** Terminal columns; null for plain output (pipes, files), which is never cut. */
64
+ columns: number | null;
65
+ };
66
+ export declare function joinParts(palette: ReportPalette, parts: readonly string[]): string;
67
+ /** `account-wide`, `on 2 domains`, or '' (a legacy issue without a zone list). */
68
+ export declare function findingScope(finding: CheckupFinding): string;
69
+ /** A grade in the colour of its band, bold: A green, B and C yellow, D and F red (the server's letter, coloured). */
70
+ export declare function styleGrade(style: Styles, grade: string): string;
71
+ /**
72
+ * Cells per segment for a stacked bar: proportional, summing to `width`, and
73
+ * at least one cell for every non-zero count while the width allows.
74
+ */
75
+ export declare function segmentWidths(values: readonly number[], width: number): number[];
76
+ /**
77
+ * `██████░░`: passed, then warnings and failed (yellow and red with colour),
78
+ * then checks with any other status. Without colour, everything but passed is
79
+ * the empty cell, so the bar still reads as the share that passed.
80
+ */
81
+ export declare function checksBar(palette: ReportPalette, summary: CheckupCounts, width: number): string | null;
82
+ /**
83
+ * Finding lines, aligned: `✗ critical Account MFA Enforcement account-wide`.
84
+ * `indent` prefixes every line; with `recommendations`, the first line of the
85
+ * recommendation follows each finding, dim, under its name.
86
+ */
87
+ export declare function findingLines(palette: ReportPalette, findings: readonly CheckupFinding[], options: {
88
+ indent: string;
89
+ recommendations?: boolean;
90
+ }): string[];
91
+ /** `+87 more checks in the full report` */
92
+ export declare function lockedTeaser(palette: ReportPalette, locked: number): string | null;
93
+ /** Findings shown at the end of a local checkup. */
94
+ export declare const LOCAL_FINDINGS_SHOWN: 5;
95
+ /**
96
+ * The result card of the terminal UI, under `└ ✓ Done. …`: the free checks as
97
+ * a bar, the top findings (failed, then warnings; names and zone counts, never
98
+ * zone names) and how many premium checks the full report adds. Lines are
99
+ * indented to the end of the timeline and fit the width.
100
+ */
101
+ export declare function localResultCard(palette: ReportPalette, view: CheckupReportView): string[];
102
+ /**
103
+ * The same, as plain lines appended after the plain result lines:
104
+ * `Top findings:`, one indented line each, then the premium teaser. Empty when
105
+ * there is nothing to add.
106
+ */
107
+ export declare function localResultPlainLines(view: CheckupReportView, palette: ReportPalette): string[];
108
+ /** Priorities shown by `cf checkup result` and `cf checkup`. */
109
+ export declare const PRIORITIES_SHOWN: 8;
110
+ /** Domain rows shown before `… N more domains`. */
111
+ export declare const ZONES_SHOWN: 15;
112
+ /**
113
+ * The body of `cf checkup result` and `cf checkup` in text mode, after their
114
+ * `checkup` and `grade` lines: the account, the counts (a bar on a terminal),
115
+ * the domains, the top priorities with the first line of each recommendation,
116
+ * the premium teaser of a locked result and the full report link.
117
+ */
118
+ export declare function checkupReportLines(palette: ReportPalette, view: CheckupReportView): string[];