@vigilbase/cli 0.2.2 → 0.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.
- package/CLOUDFLARE.md +2 -2
- package/COMMANDS.md +128 -42
- package/LOCAL.md +99 -38
- package/ONE.md +1 -1
- package/README.md +217 -66
- package/SECURITY.md +1 -1
- package/dist/auth/acl.d.ts +6 -0
- package/dist/auth/acl.js +45 -0
- package/dist/auth/device-auth.d.ts +3 -0
- package/dist/auth/device-auth.js +13 -3
- package/dist/auth/provider.js +4 -4
- package/dist/auth/refresh.d.ts +5 -4
- package/dist/auth/refresh.js +82 -41
- package/dist/auth/store.d.ts +25 -0
- package/dist/auth/store.js +91 -11
- package/dist/bin.js +13 -1
- package/dist/cli.js +205 -81
- package/dist/commands/account.js +11 -7
- package/dist/commands/auth.d.ts +6 -0
- package/dist/commands/auth.js +75 -24
- package/dist/commands/billing.js +2 -2
- package/dist/commands/cloudflare.d.ts +1 -1
- package/dist/commands/cloudflare.js +31 -17
- package/dist/commands/helpers.js +3 -3
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +2 -1
- package/dist/commands/legacy-validation.d.ts +20 -0
- package/dist/commands/legacy-validation.js +77 -3
- package/dist/commands/local.js +186 -65
- package/dist/commands/one.js +16 -13
- package/dist/commands/public-tools.js +66 -35
- package/dist/commands/runs.js +5 -5
- package/dist/commands/security.d.ts +1 -1
- package/dist/commands/security.js +41 -37
- package/dist/commands/watchdog.js +5 -5
- package/dist/lib/banner.d.ts +5 -11
- package/dist/lib/banner.js +11 -31
- package/dist/lib/client.d.ts +9 -0
- package/dist/lib/client.js +31 -2
- package/dist/lib/cloudflare-client.d.ts +2 -2
- package/dist/lib/config.d.ts +16 -0
- package/dist/lib/config.js +77 -19
- package/dist/lib/format.d.ts +5 -1
- package/dist/lib/format.js +110 -35
- package/dist/lib/help.js +57 -6
- package/dist/lib/network.d.ts +12 -3
- package/dist/lib/network.js +73 -9
- package/dist/lib/one-client.d.ts +2 -0
- package/dist/lib/one-client.js +86 -12
- package/dist/lib/open-url.js +2 -1
- package/dist/lib/platform.d.ts +2 -0
- package/dist/lib/platform.js +5 -0
- package/dist/lib/progress.d.ts +18 -3
- package/dist/lib/progress.js +72 -10
- package/dist/lib/system-trust.d.ts +2 -0
- package/dist/lib/system-trust.js +6 -0
- package/dist/lib/tui.d.ts +1 -1
- package/dist/lib/tui.js +5 -7
- package/dist/local/cloudflare-checkup/cf/child-env.js +2 -4
- package/dist/local/cloudflare-checkup/cf/install.d.ts +4 -2
- package/dist/local/cloudflare-checkup/cf/install.js +55 -24
- package/dist/local/cloudflare-checkup/cf/locate.d.ts +2 -0
- package/dist/local/cloudflare-checkup/cf/locate.js +10 -1
- package/dist/local/cloudflare-checkup/cf/run-cf.d.ts +18 -3
- package/dist/local/cloudflare-checkup/cf/run-cf.js +75 -17
- package/dist/local/cloudflare-checkup/contract.d.ts +16 -9
- package/dist/local/cloudflare-checkup/contract.js +80 -14
- package/dist/local/cloudflare-checkup/errors.d.ts +34 -1
- package/dist/local/cloudflare-checkup/errors.js +36 -5
- package/dist/local/cloudflare-checkup/output.d.ts +6 -0
- package/dist/local/cloudflare-checkup/output.js +9 -0
- package/dist/local/cloudflare-checkup/snapshot-file.d.ts +6 -2
- package/dist/local/cloudflare-checkup/snapshot-file.js +25 -11
- package/dist/local/cloudflare-checkup/submit-client.js +7 -1
- package/dist/types.d.ts +6 -1
- package/esm-only.cjs +3 -0
- package/package.json +7 -4
package/dist/commands/local.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* cf owns the Cloudflare login: this process never reads, prints, stores or
|
|
13
13
|
* uploads a Cloudflare credential. See LOCAL.md.
|
|
14
14
|
*/
|
|
15
|
-
import { access, constants, stat } from 'node:fs/promises';
|
|
15
|
+
import { access, constants, lstat, rm, stat } from 'node:fs/promises';
|
|
16
16
|
import { homedir } from 'node:os';
|
|
17
17
|
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
18
18
|
import { z } from 'zod';
|
|
@@ -21,9 +21,9 @@ import { resolveConfigDir } from '../lib/config.js';
|
|
|
21
21
|
import { isHeadlessEnvironment, openUrlInBrowser } from '../lib/open-url.js';
|
|
22
22
|
import { shouldShowProgress } from '../lib/progress.js';
|
|
23
23
|
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
|
-
import { ENV_TOKEN_AUTH_HINT, LOCAL_ERROR_HINTS, LocalCheckupError } from '../local/cloudflare-checkup/errors.js';
|
|
24
|
+
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
25
|
import { CF_TOKEN_ENV, buildChildEnv, childEnvVariableNames, hasEnvToken, missingEnvTokenMessage, strippedTokenNotice, } from '../local/cloudflare-checkup/cf/child-env.js';
|
|
26
|
-
import { assertLocalRuntimeSupported, checkCfVersion, isNodeSupported, locateCf } from '../local/cloudflare-checkup/cf/locate.js';
|
|
26
|
+
import { assertLocalRuntimeSupported, checkCfVersion, currentDirectory, isNodeSupported, locateCf } from '../local/cloudflare-checkup/cf/locate.js';
|
|
27
27
|
import { CfInstallError, MANAGED_CF, ensurePrivateDir, findManagedCf, installManagedCf, managedCfRoot, removeManagedCf } from '../local/cloudflare-checkup/cf/install.js';
|
|
28
28
|
import { withCfRunner } from '../local/cloudflare-checkup/cf/run-cf.js';
|
|
29
29
|
import { CRITICAL_TEMPLATES, DEFAULT_PROFILE, PREFLIGHT_ACCOUNT_ID, TEMPLATES, TEMPLATE_IDS, dryRunMatches, loginArgv, logoutArgv, preflightChecks, templateDisplay, validateProfileName, versionArgv, whoamiArgv, } from '../local/cloudflare-checkup/cf/allowlist.js';
|
|
@@ -34,9 +34,9 @@ import { accountName, countEntries, countEntriesByKind, dataCategories, loadSnap
|
|
|
34
34
|
import { confirmDefaultYes, confirmUpload, hasInteractiveInput, promptLine } from '../local/cloudflare-checkup/prompts.js';
|
|
35
35
|
import { SAME_ADDRESS_WAIT, emailLocale, parseEmailFlag, verifyEmail } from '../local/cloudflare-checkup/email-verification.js';
|
|
36
36
|
import { SubmitClient, VerificationClient, assertServerAccepts, displayRequester, isLoopbackOrigin, resolveSubmitOrigin, } from '../local/cloudflare-checkup/submit-client.js';
|
|
37
|
-
import { displayText, formatBytes, formatClock, formatDuration, maskId, readsSummary, shortSha, startCountsProgress } from '../local/cloudflare-checkup/output.js';
|
|
37
|
+
import { displayFileName, displayText, formatBytes, formatClock, formatDuration, maskId, readsSummary, shortSha, startCountsProgress } from '../local/cloudflare-checkup/output.js';
|
|
38
38
|
import { CheckupTui } from '../local/cloudflare-checkup/terminal-ui.js';
|
|
39
|
-
import { EXIT,
|
|
39
|
+
import { EXIT, ok, outputJson } from './helpers.js';
|
|
40
40
|
/**
|
|
41
41
|
* How the page, the docs and the hints spell the command: the bare package,
|
|
42
42
|
* no version. vigilbase.com refuses CLIs older than it accepts, and that
|
|
@@ -63,17 +63,22 @@ uploads it.
|
|
|
63
63
|
Options (the default command, also called run):
|
|
64
64
|
--email <address> Your work email (asked for when omitted)
|
|
65
65
|
--account-id <id> Skip the account picker
|
|
66
|
-
--out <file>
|
|
67
|
-
--overwrite
|
|
66
|
+
--out <file> Where to save the snapshot (default:
|
|
67
|
+
<config dir>/checkups/…); --overwrite
|
|
68
|
+
replaces an existing file
|
|
68
69
|
--yes Install cf and upload without asking
|
|
69
70
|
(--confirm answers only the upload question)
|
|
70
71
|
--no-open Print the results link; do not open a browser
|
|
71
72
|
--cf-auth auto|profile|env|default
|
|
72
|
-
auto (default): env when
|
|
73
|
-
in this shell
|
|
73
|
+
auto (default): env when
|
|
74
|
+
${CF_TOKEN_ENV} is set in this shell
|
|
75
|
+
(cf reads it itself), else profile
|
|
74
76
|
--profile <name> cf profile (default ${DEFAULT_PROFILE})
|
|
75
77
|
--cf-bin <path> Use this cf package; never install one
|
|
76
|
-
--max-requests-per-5min <n> Cloudflare
|
|
78
|
+
--max-requests-per-5min <n> Cloudflare requests per 5 minutes,
|
|
79
|
+
100..1100 (default 900)
|
|
80
|
+
--base-url <origin> Upload origin: ${PRODUCTION_ORIGIN}
|
|
81
|
+
(default) or a loopback origin
|
|
77
82
|
--timeout <milliseconds> · --json
|
|
78
83
|
|
|
79
84
|
Without a terminal, add --yes: the email (unless --email is given) and the
|
|
@@ -82,18 +87,22 @@ code are read from stdin, one line each, after their prompts on stderr.
|
|
|
82
87
|
Advanced commands:
|
|
83
88
|
login Create or refresh the read-only cf profile
|
|
84
89
|
[--profile] [--no-open] [--dry-run] [--cf-bin]
|
|
85
|
-
doctor Check Node, cf, the offline command mapping
|
|
90
|
+
doctor Check Node, cf, the offline command mapping
|
|
91
|
+
and cf auth
|
|
86
92
|
accounts List the Cloudflare account IDs cf can read
|
|
87
|
-
collect --out <file> Collect only
|
|
93
|
+
collect --out <file> Collect only [--overwrite] [--account-id];
|
|
94
|
+
--dry-run shows every cf command (offline)
|
|
88
95
|
inspect <file> Validate a snapshot and show what it contains
|
|
89
|
-
submit <file> Verify your email, then upload a
|
|
90
|
-
[--email] [--yes] [--no-open]
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
submit <file> Verify your email, then upload a snapshot
|
|
97
|
+
[--email] [--yes|--confirm] [--no-open]
|
|
98
|
+
[--base-url] [--timeout]
|
|
99
|
+
logout Delete the read-only cf profile and the cf
|
|
100
|
+
this CLI installed [--profile] [--cf-bin]
|
|
93
101
|
|
|
94
102
|
Results go to the verified email address. Uploads go only to
|
|
95
103
|
${PRODUCTION_ORIGIN} (or a loopback --base-url).
|
|
96
|
-
Exit codes: 0 success, 1 error, 2 rate limited
|
|
104
|
+
Exit codes: 0 success, 1 error, 2 rate limited, 130 cancelled (Ctrl-C).
|
|
105
|
+
See LOCAL.md for details.`;
|
|
97
106
|
const CF_FLAGS = ['profile', 'cf-auth', 'cf-bin'];
|
|
98
107
|
const COLLECT_FLAGS = [...CF_FLAGS, 'account-id', 'out', 'isOverwrite', 'max-requests-per-5min'];
|
|
99
108
|
const SUBMIT_FLAGS = ['email', 'isNoOpen', 'isConfirm', 'isYes', 'base-url', 'timeout'];
|
|
@@ -107,15 +116,18 @@ const ALLOWED_FLAGS = {
|
|
|
107
116
|
submit: [...SUBMIT_FLAGS, 'isJson'],
|
|
108
117
|
logout: ['profile', 'cf-bin', 'isJson'],
|
|
109
118
|
};
|
|
110
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Validate flags before any spawn, file or network access. Every refused flag
|
|
121
|
+
* (including the global origin flags, which local commands do not take) is
|
|
122
|
+
* named with the subcommand.
|
|
123
|
+
*/
|
|
111
124
|
function assertLocalFlags(ctx, subcommand) {
|
|
112
125
|
const allowed = ALLOWED_FLAGS[subcommand];
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
126
|
+
for (const [key, value] of Object.entries(ctx.values)) {
|
|
127
|
+
if (value === undefined || value === false || ['isHelp', 'isVersion'].includes(key) || allowed.includes(key))
|
|
128
|
+
continue;
|
|
129
|
+
const flag = key.startsWith('is') ? key.slice(2).replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() : key;
|
|
130
|
+
throw new Error(`--${flag} is not supported for cloudflare-checkup ${subcommand}.`);
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
133
|
const accountIdSchema = z.string()
|
|
@@ -167,7 +179,8 @@ function parseLocalOptions(ctx, subcommand, args) {
|
|
|
167
179
|
authAuto: requested === 'auto',
|
|
168
180
|
...(cfBin !== undefined ? { cfBin } : {}),
|
|
169
181
|
...(values['account-id'] !== undefined ? { accountId: parse(accountIdSchema, values['account-id']) } : {}),
|
|
170
|
-
|
|
182
|
+
// A relative --out never reaches resolve()'s implicit process.cwd(), which fails raw in a deleted directory.
|
|
183
|
+
...(out !== undefined ? { out: isAbsolute(out) ? resolve(out) : resolve(currentDirectory(), out) } : {}),
|
|
171
184
|
maxRequests: parseRequestBudget(values['max-requests-per-5min']),
|
|
172
185
|
// Environment variables never choose the submit origin.
|
|
173
186
|
...(subcommand === 'submit' || subcommand === 'run' ? { origin: resolveSubmitOrigin(values['base-url']) } : {}),
|
|
@@ -296,13 +309,16 @@ function warn(ctx, warnings, warning) {
|
|
|
296
309
|
if (ctx.mode !== 'json')
|
|
297
310
|
ctx.writer.stderr(`Warning: ${warning.message}`);
|
|
298
311
|
}
|
|
312
|
+
/** `--cf-auth env` needs the token variable in this shell (presence only; the value is never read). */
|
|
313
|
+
function assertEnvToken(ctx, options) {
|
|
314
|
+
if (options.auth.authMode === 'env' && !hasEnvToken(ctx.env)) {
|
|
315
|
+
throw new LocalCheckupError('CF_AUTH_REQUIRED', missingEnvTokenMessage(), { hint: ENV_TOKEN_AUTH_HINT });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
299
318
|
/** Env-mode presence check and the profile-mode notice; the value is never read. */
|
|
300
319
|
function checkAuthEnvironment(ctx, options, warnings) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
throw new LocalCheckupError('CF_AUTH_REQUIRED', missingEnvTokenMessage(), { hint: ENV_TOKEN_AUTH_HINT });
|
|
304
|
-
}
|
|
305
|
-
else if (hasEnvToken(ctx.env)) {
|
|
320
|
+
assertEnvToken(ctx, options);
|
|
321
|
+
if (options.auth.authMode !== 'env' && hasEnvToken(ctx.env)) {
|
|
306
322
|
warn(ctx, warnings, { code: 'ENV_TOKEN_NOT_USED', message: strippedTokenNotice(options.auth.authMode) });
|
|
307
323
|
}
|
|
308
324
|
}
|
|
@@ -311,13 +327,29 @@ async function managedDirFor(options) {
|
|
|
311
327
|
return null;
|
|
312
328
|
return findManagedCf(options.configDir);
|
|
313
329
|
}
|
|
314
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Find cf (filesystem only, nothing is spawned). Only the one command without
|
|
332
|
+
* `--cf-bin` can install cf, so everywhere else CF_NOT_FOUND names the one
|
|
333
|
+
* command, npm and `--cf-bin` instead.
|
|
334
|
+
*/
|
|
335
|
+
async function locate(ctx, options, managedDir) {
|
|
315
336
|
const managed = managedDir === undefined ? await managedDirFor(options) : managedDir;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
337
|
+
try {
|
|
338
|
+
return await locateCf({
|
|
339
|
+
...(options.cfBin !== undefined ? { cfBin: options.cfBin } : {}),
|
|
340
|
+
...(managed ? { managedDir: managed } : {}),
|
|
341
|
+
env: ctx.env,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
if (error instanceof LocalCheckupError && error.code === 'CF_NOT_FOUND' && (options.subcommand !== 'run' || options.cfBin !== undefined)) {
|
|
346
|
+
throw new LocalCheckupError('CF_NOT_FOUND', error.message, { hint: CF_NOT_FOUND_ADVANCED_HINT });
|
|
347
|
+
}
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function locateAndWarn(ctx, options, warnings, managedDir) {
|
|
352
|
+
const location = await locate(ctx, options, managedDir);
|
|
321
353
|
if (location.insideCwd) {
|
|
322
354
|
warn(ctx, warnings, { code: 'CF_INSIDE_CWD', message: 'The cf package found lies inside the current directory; make sure it is the copy you installed.' });
|
|
323
355
|
}
|
|
@@ -331,7 +363,7 @@ async function checkVersion(ctx, runner, location, authMode, warnings) {
|
|
|
331
363
|
if (result.exitCode !== 0) {
|
|
332
364
|
if (/cf requires Node\.js/.test(result.stderr))
|
|
333
365
|
throw new LocalCheckupError('NODE_UNSUPPORTED', 'cf refused to run under this Node.js version.');
|
|
334
|
-
throw new LocalCheckupError('CF_WRONG_BINARY', 'cf --version failed; the package may be damaged.
|
|
366
|
+
throw new LocalCheckupError('CF_WRONG_BINARY', 'cf --version failed; the package may be damaged.', { hint: CF_DAMAGED_HINT });
|
|
335
367
|
}
|
|
336
368
|
const prepared = checkCfVersion(result.stdout, location.packageVersion);
|
|
337
369
|
if (!prepared.tested) {
|
|
@@ -505,6 +537,9 @@ async function writeToTarget(target, accountId, startedAt, text, overwrite) {
|
|
|
505
537
|
async function runLogin(ctx, options) {
|
|
506
538
|
const argv = loginArgv(options.auth.profile, RECOMMENDED_CF_SCOPES, { noBrowser: Boolean(ctx.values.isNoOpen) });
|
|
507
539
|
if (ctx.values.isDryRun) {
|
|
540
|
+
// A --cf-bin that a real login would refuse is refused here too (filesystem only, nothing runs).
|
|
541
|
+
if (options.cfBin !== undefined)
|
|
542
|
+
await locate(ctx, options);
|
|
508
543
|
if (ctx.mode === 'json')
|
|
509
544
|
outputJson(ctx, { success: true, dryRun: true, argv: ['cf', ...argv] });
|
|
510
545
|
else
|
|
@@ -571,8 +606,8 @@ async function runLogout(ctx, options) {
|
|
|
571
606
|
});
|
|
572
607
|
}
|
|
573
608
|
catch (error) {
|
|
574
|
-
// A cf that cannot run must not stop a broken managed install from being removed (unless --cf-bin named it).
|
|
575
|
-
if (options.cfBin !== undefined || !(error instanceof LocalCheckupError))
|
|
609
|
+
// A cf that cannot run must not stop a broken managed install from being removed (unless --cf-bin named it); Ctrl-C stops logout.
|
|
610
|
+
if (options.cfBin !== undefined || !(error instanceof LocalCheckupError) || error.code === 'CANCELLED')
|
|
576
611
|
throw error;
|
|
577
612
|
cfProblem = error;
|
|
578
613
|
}
|
|
@@ -645,6 +680,8 @@ async function runDoctor(ctx, options) {
|
|
|
645
680
|
authenticated: false,
|
|
646
681
|
scopes: 'unknown',
|
|
647
682
|
};
|
|
683
|
+
/** Whether the sign-in was looked at; without a cf that runs it cannot be. */
|
|
684
|
+
let authChecked = false;
|
|
648
685
|
let runtimeOk = true;
|
|
649
686
|
try {
|
|
650
687
|
assertLocalRuntimeSupported();
|
|
@@ -678,6 +715,7 @@ async function runDoctor(ctx, options) {
|
|
|
678
715
|
if (preflight.mismatched.length > 0) {
|
|
679
716
|
problems.push({ code: 'CF_ALLOWLIST_MISMATCH', message: `${preflight.mismatched.length} of ${preflight.total} command templates do not map in this cf release.` });
|
|
680
717
|
}
|
|
718
|
+
authChecked = true;
|
|
681
719
|
try {
|
|
682
720
|
checkAuthEnvironment(ctx, options, warnings);
|
|
683
721
|
auth = doctorAuth(await createScheduler(ctx, runner, options.auth, DOCTOR_BUDGET).start(), options.auth.authMode, options.auth.profile);
|
|
@@ -692,7 +730,7 @@ async function runDoctor(ctx, options) {
|
|
|
692
730
|
}
|
|
693
731
|
const success = nodeSupported && cf.supported && commands.mismatched.length === 0 && auth.authenticated && problems.length === 0;
|
|
694
732
|
if (ctx.mode === 'json') {
|
|
695
|
-
outputJson(ctx, { success, node: { version: process.versions.node, supported: nodeSupported }, cf, auth, commands, warnings, problems });
|
|
733
|
+
outputJson(ctx, { success, node: { version: process.versions.node, supported: nodeSupported }, cf, auth: { ...auth, checked: authChecked }, commands, warnings, problems });
|
|
696
734
|
return success ? EXIT.OK : EXIT.ERROR;
|
|
697
735
|
}
|
|
698
736
|
const mark = (isOk) => (isOk ? '✓' : '✗');
|
|
@@ -700,7 +738,8 @@ async function runDoctor(ctx, options) {
|
|
|
700
738
|
ctx.writer.stdout(`${mark(cf.supported)} cf ${cf.version ?? (cf.found ? '(version check failed)' : 'not found')}${cf.path ? ` ${cf.path}` : ''}${cf.supported && !cf.tested ? ' (untested)' : ''}`);
|
|
701
739
|
ctx.writer.stdout(`${mark(commands.mismatched.length === 0 && commands.verified > 0)} ${commands.verified} command templates mapped offline${commands.mismatched.length ? `; not mapped: ${commands.mismatched.join(', ')}` : ''}`);
|
|
702
740
|
const authLabel = options.auth.authMode === 'profile' ? `profile "${options.auth.profile}"` : options.auth.authMode === 'env' ? 'API token from this shell' : 'default profile';
|
|
703
|
-
|
|
741
|
+
const authState = auth.authenticated ? 'signed in' : authChecked ? 'not signed in' : `not checked (${cf.found ? 'cf could not run' : 'cf not found'})`;
|
|
742
|
+
ctx.writer.stdout(`${mark(auth.authenticated)} cf auth (${authLabel}): ${authState}${auth.expiresAt ? ` · expires ${formatClock(auth.expiresAt)}` : ''}`);
|
|
704
743
|
if (auth.authenticated && auth.scopes === 'unknown')
|
|
705
744
|
ctx.writer.stdout(' scopes: unknown (API token permissions cannot be introspected)');
|
|
706
745
|
if (auth.missingRecommendedScopes?.length)
|
|
@@ -748,6 +787,15 @@ const REQUEST_FORMULA = 'about 19 + extra pages + zones × (33 + rulesets per zo
|
|
|
748
787
|
async function runCollectDryRun(ctx, options) {
|
|
749
788
|
assertLocalRuntimeSupported();
|
|
750
789
|
const warnings = [];
|
|
790
|
+
// What a real collect would say about the shell token, as warnings: a dry run makes no authenticated call.
|
|
791
|
+
try {
|
|
792
|
+
checkAuthEnvironment(ctx, options, warnings);
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
if (!(error instanceof LocalCheckupError))
|
|
796
|
+
throw error;
|
|
797
|
+
warn(ctx, warnings, { code: error.code, message: `${error.message} Without it, a real collect would stop here.` });
|
|
798
|
+
}
|
|
751
799
|
const location = await locateAndWarn(ctx, options, warnings);
|
|
752
800
|
const { prepared, preflight } = await withCfRunner(location.entry, async (runner) => ({
|
|
753
801
|
prepared: await checkVersion(ctx, runner, location, options.auth.authMode, warnings),
|
|
@@ -799,13 +847,19 @@ async function runCollectDryRun(ctx, options) {
|
|
|
799
847
|
/**
|
|
800
848
|
* Advisory pre-flight for --out, before any cf spawn, so a typo cannot throw
|
|
801
849
|
* away a whole collection. writeSnapshotFile stays the race-safe final guard.
|
|
850
|
+
* The entry itself is looked at (lstat): any symlink, even a dangling one, is
|
|
851
|
+
* an existing file, which --overwrite replaces without following it.
|
|
802
852
|
*/
|
|
803
853
|
async function assertOutWritable(out, overwrite) {
|
|
804
|
-
const existing = await
|
|
854
|
+
const existing = await lstat(out).catch(() => null);
|
|
855
|
+
// With --overwrite, the final write also refuses a link to a directory (or another non-file).
|
|
856
|
+
const linked = existing?.isSymbolicLink() && overwrite ? await stat(out).catch(() => null) : null;
|
|
857
|
+
const entry = linked ?? existing;
|
|
858
|
+
if (entry && !entry.isFile() && !entry.isSymbolicLink()) {
|
|
859
|
+
throw new LocalCheckupError('OUTPUT_EXISTS', `--out names a ${entry.isDirectory() ? 'directory' : 'special file'}; it must name a regular file. Nothing was collected.`, { hint: 'Choose a file path, for example <directory>/snapshot.json.' });
|
|
860
|
+
}
|
|
805
861
|
if (existing && !overwrite)
|
|
806
862
|
throw new LocalCheckupError('OUTPUT_EXISTS', 'The --out file already exists; nothing was collected.');
|
|
807
|
-
if (existing && !existing.isFile())
|
|
808
|
-
throw new Error('--out must name a regular file; nothing was collected.');
|
|
809
863
|
const dir = dirname(out);
|
|
810
864
|
const parent = await stat(dir).catch(() => null);
|
|
811
865
|
if (!parent || !parent.isDirectory())
|
|
@@ -828,10 +882,40 @@ async function prepareOutput(ctx, options, overwrite) {
|
|
|
828
882
|
await ensurePrivateDir(dir);
|
|
829
883
|
}
|
|
830
884
|
catch {
|
|
831
|
-
throw
|
|
885
|
+
throw checkupsDirError(ctx, dir);
|
|
832
886
|
}
|
|
833
887
|
return { kind: 'default', dir };
|
|
834
888
|
}
|
|
889
|
+
function checkupsDirError(ctx, dir) {
|
|
890
|
+
return new Error(`Could not create ${displayPath(dir, ctx.env)} for the snapshot; pass --out <file> instead.`);
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Before the email: prepareOutput's checks without writing anything. The
|
|
894
|
+
* checkups directory exists as a real directory this user can write to, or
|
|
895
|
+
* its nearest existing parent is one, so it can be created later.
|
|
896
|
+
*/
|
|
897
|
+
async function assertOutputPossible(ctx, options, overwrite) {
|
|
898
|
+
if (options.out !== undefined)
|
|
899
|
+
return assertOutWritable(options.out, overwrite);
|
|
900
|
+
const dir = join(options.configDir, 'checkups');
|
|
901
|
+
const writable = (target) => access(target, constants.W_OK | constants.X_OK).then(() => true, () => false);
|
|
902
|
+
const leaf = await lstat(dir).catch(() => null);
|
|
903
|
+
if (leaf) {
|
|
904
|
+
if (!leaf.isDirectory() || !(await writable(dir)))
|
|
905
|
+
throw checkupsDirError(ctx, dir);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
for (let current = dirname(dir);; current = dirname(current)) {
|
|
909
|
+
const info = await stat(current).catch(() => null);
|
|
910
|
+
if (info) {
|
|
911
|
+
if (!info.isDirectory() || !(await writable(current)))
|
|
912
|
+
throw checkupsDirError(ctx, dir);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (dirname(current) === current)
|
|
916
|
+
throw checkupsDirError(ctx, dir);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
835
919
|
/**
|
|
836
920
|
* Why a located cf cannot be used, found before any authenticated call: none
|
|
837
921
|
* at all, a different program, an unsupported version, or (after `--version`
|
|
@@ -1069,7 +1153,14 @@ async function collectSnapshot(ctx, options, session) {
|
|
|
1069
1153
|
message: `The snapshot is larger than ${SNAPSHOT_LIMITS.defaultServerMaxBytes / 1024 / 1024} MiB, the default upload limit.`,
|
|
1070
1154
|
});
|
|
1071
1155
|
}
|
|
1156
|
+
// A cancelled run writes no snapshot, and undoes one that was being written when it was cancelled.
|
|
1157
|
+
if (runner.cancellation)
|
|
1158
|
+
throw runner.cancellation;
|
|
1072
1159
|
const written = await writeToTarget(target, accountId, startedAt, text, overwrite);
|
|
1160
|
+
if (runner.cancellation) {
|
|
1161
|
+
await rm(written.path, { force: true }).catch(() => undefined);
|
|
1162
|
+
throw runner.cancellation;
|
|
1163
|
+
}
|
|
1073
1164
|
return { ...written, snapshot, version: prepared.version, cfInvocations: scheduler.invocations };
|
|
1074
1165
|
}
|
|
1075
1166
|
finally {
|
|
@@ -1162,16 +1253,24 @@ function clientOptions(ctx, origin) {
|
|
|
1162
1253
|
}
|
|
1163
1254
|
/**
|
|
1164
1255
|
* Before the email is asked for (or `--email` is used): vigilbase.com says
|
|
1165
|
-
* whether local checkups are on and which CLI
|
|
1166
|
-
* so a switched-off local mode (LOCAL_MODE_DISABLED) or an old CLI
|
|
1256
|
+
* whether local checkups are on and which CLI, snapshot schema and size it
|
|
1257
|
+
* accepts, so a switched-off local mode (LOCAL_MODE_DISABLED) or an old CLI
|
|
1167
1258
|
* (CLI_UPGRADE_REQUIRED) never costs an emailed code. Without a usable answer
|
|
1168
|
-
* the run goes on, and the session check after the email decides as before.
|
|
1259
|
+
* (null) the run goes on, and the session check after the email decides as before.
|
|
1169
1260
|
*/
|
|
1170
1261
|
async function checkAvailability(ctx, origin, schemaVersion) {
|
|
1171
1262
|
const client = new VerificationClient(clientOptions(ctx, origin));
|
|
1172
|
-
const availability = await busy(ctx,
|
|
1263
|
+
const availability = await busy(ctx, `Checking ${new URL(origin).host}`, () => client.availability());
|
|
1173
1264
|
if (availability)
|
|
1174
1265
|
assertServerAccepts(availability, CLI_VERSION, schemaVersion);
|
|
1266
|
+
return availability ?? null;
|
|
1267
|
+
}
|
|
1268
|
+
/** The size and zone limits vigilbase.com announced (availability) or set for the checkup (session). */
|
|
1269
|
+
function assertWithinLimits(server, loaded) {
|
|
1270
|
+
const { maxBytes, maxZones } = server.accepts;
|
|
1271
|
+
if (loaded.bytes.byteLength > maxBytes || zoneCount(loaded.snapshot) > maxZones) {
|
|
1272
|
+
throw new LocalCheckupError('SNAPSHOT_TOO_LARGE', `The snapshot is over the vigilbase.com limit (${formatBytes(maxBytes)}, ${maxZones} zones). Nothing was uploaded.`);
|
|
1273
|
+
}
|
|
1175
1274
|
}
|
|
1176
1275
|
/**
|
|
1177
1276
|
* The email verification (prompts on stderr, answers from piped stdin
|
|
@@ -1308,9 +1407,12 @@ async function runOneCommand(ctx, options) {
|
|
|
1308
1407
|
else
|
|
1309
1408
|
say(ctx, BANNER);
|
|
1310
1409
|
assertLocalRuntimeSupported();
|
|
1311
|
-
//
|
|
1312
|
-
|
|
1313
|
-
|
|
1410
|
+
// What can be checked on this machine never costs an emailed code (collectSnapshot checks it again):
|
|
1411
|
+
// the token for --cf-auth env, the --out path or the checkups directory, and a --cf-bin (nothing runs).
|
|
1412
|
+
assertEnvToken(ctx, options);
|
|
1413
|
+
await assertOutputPossible(ctx, options, Boolean(ctx.values.isOverwrite));
|
|
1414
|
+
if (options.cfBin !== undefined)
|
|
1415
|
+
await locate(ctx, options);
|
|
1314
1416
|
// 1-3. Availability, then the email, then the checkup it started are checked before cf is touched.
|
|
1315
1417
|
await checkAvailability(ctx, origin, SNAPSHOT_SCHEMA_VERSION);
|
|
1316
1418
|
const checkup = new CheckupSession(ctx, origin, await verifyRequester(ctx, options, origin));
|
|
@@ -1367,6 +1469,14 @@ async function runInspect(ctx, options) {
|
|
|
1367
1469
|
const name = accountName(snapshot);
|
|
1368
1470
|
const categories = dataCategories(snapshot);
|
|
1369
1471
|
const byKind = countEntriesByKind(snapshot.entries);
|
|
1472
|
+
const freshness = snapshotFreshness(snapshot);
|
|
1473
|
+
const warnings = [];
|
|
1474
|
+
if (loaded.bytes.byteLength > SNAPSHOT_LIMITS.defaultServerMaxBytes) {
|
|
1475
|
+
warn(ctx, warnings, {
|
|
1476
|
+
code: 'SNAPSHOT_OVER_DEFAULT_UPLOAD_LIMIT',
|
|
1477
|
+
message: `The snapshot is larger than ${SNAPSHOT_LIMITS.defaultServerMaxBytes / 1024 / 1024} MiB, the default upload limit.`,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1370
1480
|
if (ctx.mode === 'json') {
|
|
1371
1481
|
outputJson(ctx, {
|
|
1372
1482
|
success: true,
|
|
@@ -1380,20 +1490,25 @@ async function runInspect(ctx, options) {
|
|
|
1380
1490
|
zoneCount: zoneCount(snapshot),
|
|
1381
1491
|
collectedAt: snapshot.collector.finishedAt,
|
|
1382
1492
|
collector: snapshot.collector,
|
|
1493
|
+
uploadable: freshness === 'fresh',
|
|
1494
|
+
...(freshness !== 'fresh' ? { notUploadableReason: freshness } : {}),
|
|
1383
1495
|
},
|
|
1384
1496
|
entries: { byKind },
|
|
1385
1497
|
dataCategories: categories,
|
|
1498
|
+
warnings,
|
|
1386
1499
|
});
|
|
1387
1500
|
return ok();
|
|
1388
1501
|
}
|
|
1389
1502
|
const counts = countEntries(snapshot.entries);
|
|
1390
1503
|
const { collector } = snapshot;
|
|
1391
1504
|
const lines = [
|
|
1392
|
-
`Snapshot ${options.file} ${formatBytes(loaded.bytes.byteLength)} sha256 ${loaded.sha256}`,
|
|
1505
|
+
`Snapshot ${displayFileName(options.file)} ${formatBytes(loaded.bytes.byteLength)} sha256 ${loaded.sha256}`,
|
|
1393
1506
|
`Schema ${snapshot.schemaVersion} (valid)`,
|
|
1394
1507
|
`Account ${snapshot.accountId}${name !== null ? ` ${name} (as reported)` : ''}`,
|
|
1395
1508
|
`Zones ${zoneCount(snapshot)}`,
|
|
1396
1509
|
`Collected ${collector.finishedAt} by ${collector.name} ${collector.version} with cf ${collector.cf.version} (${collector.authSource}, ${collector.platform}, ${collector.cfInvocations} cf calls)`,
|
|
1510
|
+
...(freshness === 'older_than_24_hours' ? ['Upload no longer accepted (collected more than 24 hours ago)']
|
|
1511
|
+
: freshness === 'dated_in_the_future' ? ['Upload not accepted (dated in the future)'] : []),
|
|
1397
1512
|
`Reads ${counts.total} · ${counts.ok} ok · ${counts.httpError} HTTP error · ${counts.notCollected} not collectable with cf · ${counts.collectionFailed} collection failed`,
|
|
1398
1513
|
...Object.entries(byKind).map(([kind, kindCounts]) => {
|
|
1399
1514
|
const notOk = kindCounts.total - kindCounts.ok;
|
|
@@ -1416,9 +1531,10 @@ function runSubmit(ctx, options) {
|
|
|
1416
1531
|
async function submitFile(ctx, options) {
|
|
1417
1532
|
const origin = options.origin ?? PRODUCTION_ORIGIN;
|
|
1418
1533
|
const loaded = await loadSnapshotFile(options.file);
|
|
1419
|
-
|
|
1534
|
+
// Running submit again reads the same file, so the hint says how to collect a fresh one.
|
|
1535
|
+
assertFresh(loaded.snapshot, SNAPSHOT_STALE_FILE_HINT);
|
|
1420
1536
|
const zones = zoneCount(loaded.snapshot);
|
|
1421
|
-
const head = `Snapshot ${options.file} ${formatBytes(loaded.bytes.byteLength)} sha256 ${shortSha(loaded.sha256)} account ${maskId(loaded.snapshot.accountId)} ${zones} zone${zones === 1 ? '' : 's'} collected ${formatClock(loaded.snapshot.collector.finishedAt)}`;
|
|
1537
|
+
const head = `Snapshot ${displayFileName(options.file)} ${formatBytes(loaded.bytes.byteLength)} sha256 ${shortSha(loaded.sha256)} account ${maskId(loaded.snapshot.accountId)} ${zones} zone${zones === 1 ? '' : 's'} collected ${formatClock(loaded.snapshot.collector.finishedAt)}`;
|
|
1422
1538
|
if (ctx.tui) {
|
|
1423
1539
|
ctx.tui.intro();
|
|
1424
1540
|
const { ellipsis } = ctx.tui;
|
|
@@ -1434,8 +1550,10 @@ async function submitFile(ctx, options) {
|
|
|
1434
1550
|
ctx.writer.stderr(head);
|
|
1435
1551
|
else
|
|
1436
1552
|
ctx.writer.stdout(head);
|
|
1437
|
-
// The file, then vigilbase.com's availability, are checked before the email is verified, so
|
|
1438
|
-
await checkAvailability(ctx, origin, loaded.snapshot.schemaVersion);
|
|
1553
|
+
// The file, then vigilbase.com's availability and limits, are checked before the email is verified, so none costs a code.
|
|
1554
|
+
const availability = await checkAvailability(ctx, origin, loaded.snapshot.schemaVersion);
|
|
1555
|
+
if (availability)
|
|
1556
|
+
assertWithinLimits(availability, loaded);
|
|
1439
1557
|
const checkup = new CheckupSession(ctx, origin, await verifyRequester(ctx, options, origin));
|
|
1440
1558
|
const session = await busy(ctx, 'Checking the checkup', () => checkup.getSession());
|
|
1441
1559
|
const result = await uploadLoaded(ctx, { checkup, session, origin, loaded });
|
|
@@ -1443,11 +1561,17 @@ async function submitFile(ctx, options) {
|
|
|
1443
1561
|
outputJson(ctx, result);
|
|
1444
1562
|
return ok();
|
|
1445
1563
|
}
|
|
1446
|
-
/**
|
|
1447
|
-
function
|
|
1564
|
+
/** vigilbase.com accepts snapshots finished in the last 24 h (5 min clock skew). */
|
|
1565
|
+
function snapshotFreshness(snapshot, now = Date.now()) {
|
|
1448
1566
|
const finished = Date.parse(snapshot.collector.finishedAt);
|
|
1449
|
-
if (
|
|
1450
|
-
|
|
1567
|
+
if (finished > now + SNAPSHOT_LIMITS.maxFutureSkewSeconds * 1_000)
|
|
1568
|
+
return 'dated_in_the_future';
|
|
1569
|
+
return finished >= now - SNAPSHOT_LIMITS.maxAgeHours * 3_600_000 ? 'fresh' : 'older_than_24_hours';
|
|
1570
|
+
}
|
|
1571
|
+
/** Refused locally, before anything is sent. */
|
|
1572
|
+
function assertFresh(snapshot, hint) {
|
|
1573
|
+
if (snapshotFreshness(snapshot) !== 'fresh') {
|
|
1574
|
+
throw new LocalCheckupError('SNAPSHOT_STALE', 'The snapshot was collected more than 24 hours ago (or is dated in the future).', hint !== undefined ? { hint } : {});
|
|
1451
1575
|
}
|
|
1452
1576
|
}
|
|
1453
1577
|
function numberAt(value, ...path) {
|
|
@@ -1484,10 +1608,7 @@ async function uploadLoaded(ctx, input) {
|
|
|
1484
1608
|
if (session.accountId !== null && session.accountId !== snapshot.accountId) {
|
|
1485
1609
|
throw new LocalCheckupError('ACCOUNT_MISMATCH', `This checkup is bound to Cloudflare account ${maskId(session.accountId)}, but the snapshot is for ${maskId(snapshot.accountId)}. Nothing was uploaded.`);
|
|
1486
1610
|
}
|
|
1487
|
-
|
|
1488
|
-
if (loaded.bytes.byteLength > session.accepts.maxBytes || zones > session.accepts.maxZones) {
|
|
1489
|
-
throw new LocalCheckupError('SNAPSHOT_TOO_LARGE', `The snapshot is over the vigilbase.com limit (${formatBytes(session.accepts.maxBytes)}, ${session.accepts.maxZones} zones). Nothing was uploaded.`);
|
|
1490
|
-
}
|
|
1611
|
+
assertWithinLimits(session, loaded);
|
|
1491
1612
|
const requester = displayRequester(session.requester);
|
|
1492
1613
|
const question = `Results go to ${requester}. Upload ${formatBytes(loaded.bytes.byteLength)} to ${origin}?`;
|
|
1493
1614
|
const flag = ctx.values.isYes ? '--yes' : '--confirm';
|
package/dist/commands/one.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { executeOneOperation, formatOnePayload } from '../lib/one-operations.js';
|
|
3
3
|
import { assertCommandFlags, ok, outputJson, requireOneClient, withSpinner } from './helpers.js';
|
|
4
|
-
import { missingArguments } from './legacy-validation.js';
|
|
4
|
+
import { InvalidArgumentError, inRange, isoDateTime, missingArguments, oneOf, textValue, unknownSubcommand } from './legacy-validation.js';
|
|
5
5
|
const regions = ['ams', 'nyc', 'lon', 'sgp', 'iad', 'sfo', 'fra', 'dub', 'tok', 'syd', 'cpt', 'gru', 'dxb', 'ewr', 'lhr', 'jnb', 'atl', 'blr', 'bom', 'cdg', 'del', 'dfw', 'hnl', 'icn', 'itm', 'lax', 'mad', 'man', 'mel', 'mex', 'mia', 'mxp', 'nrt', 'ord', 'sao', 'scl', 'sea', 'sjc', 'sto', 'tlv', 'waw', 'yto'];
|
|
6
6
|
function flag(ctx, name, max = 2048) {
|
|
7
7
|
const value = ctx.values[name];
|
|
8
|
-
return value === undefined ? undefined :
|
|
8
|
+
return value === undefined ? undefined : textValue(`--${name}`, String(value), max);
|
|
9
9
|
}
|
|
10
10
|
function integer(ctx, name, min, max) {
|
|
11
11
|
const value = flag(ctx, name);
|
|
12
12
|
if (value === undefined)
|
|
13
13
|
return undefined;
|
|
14
14
|
if (!/^\d+$/.test(value))
|
|
15
|
-
throw new
|
|
16
|
-
return
|
|
15
|
+
throw new InvalidArgumentError(`--${name} must be an integer.`);
|
|
16
|
+
return inRange(`--${name}`, Number(value), min, max);
|
|
17
17
|
}
|
|
18
18
|
function uuid(value) {
|
|
19
19
|
const result = z.string().uuid().safeParse(value);
|
|
@@ -89,7 +89,7 @@ export function buildOneOperation(ctx) {
|
|
|
89
89
|
if (before && cursor)
|
|
90
90
|
throw new Error('--before and --cursor are mutually exclusive.');
|
|
91
91
|
if (before)
|
|
92
|
-
|
|
92
|
+
isoDateTime('--before', before);
|
|
93
93
|
return operation(`/api/customers/${uuid(ctx.rest[1])}/vigil/investigations`, {
|
|
94
94
|
limit: integer(ctx, 'limit', 1, 100), cursor, before, issueId: flag(ctx, 'issue-id', 256),
|
|
95
95
|
});
|
|
@@ -100,13 +100,13 @@ export function buildOneOperation(ctx) {
|
|
|
100
100
|
}
|
|
101
101
|
if (command === 'contracts') {
|
|
102
102
|
check(ctx, 2, ['status']);
|
|
103
|
-
return operation(`/api/customers/${uuid(ctx.rest[1])}/contracts`, { status:
|
|
103
|
+
return operation(`/api/customers/${uuid(ctx.rest[1])}/contracts`, { status: oneOf('--status', ['draft', 'pending', 'active', 'expired', 'terminated'], flag(ctx, 'status')) });
|
|
104
104
|
}
|
|
105
105
|
if (command === 'invoices') {
|
|
106
106
|
check(ctx, 2, ['status', 'limit']);
|
|
107
107
|
return operation(`/api/customers/${uuid(ctx.rest[1])}/stripe/invoices`, {
|
|
108
108
|
limit: integer(ctx, 'limit', 1, 100),
|
|
109
|
-
status:
|
|
109
|
+
status: oneOf('--status', ['draft', 'open', 'paid', 'uncollectible', 'void'], flag(ctx, 'status')),
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -149,8 +149,8 @@ export function buildOneOperation(ctx) {
|
|
|
149
149
|
if (command === 'status')
|
|
150
150
|
return operation(path, { light: ctx.values.isLight ? true : undefined });
|
|
151
151
|
return operation(path, {
|
|
152
|
-
range:
|
|
153
|
-
region:
|
|
152
|
+
range: oneOf('--range', ['24h', '7d', '30d'], flag(ctx, 'range')),
|
|
153
|
+
region: oneOf('--region', regions, flag(ctx, 'region')),
|
|
154
154
|
target_id: ctx.values['target-id'] === undefined ? undefined : uuid(ctx.values['target-id']),
|
|
155
155
|
});
|
|
156
156
|
}
|
|
@@ -162,7 +162,7 @@ export function buildOneOperation(ctx) {
|
|
|
162
162
|
customerId: ctx.values['customer-id'] === undefined ? undefined : uuid(ctx.values['customer-id']),
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
|
-
throw
|
|
165
|
+
throw unknownSubcommand(ctx.command, command);
|
|
166
166
|
}
|
|
167
167
|
export const handleOne = async (ctx) => {
|
|
168
168
|
const operation = buildOneOperation(ctx);
|
|
@@ -190,11 +190,14 @@ export const ONE_HELP = {
|
|
|
190
190
|
watchdog-scans <customer-id> --limit 1..100 --offset 0..1000000 --target-id UUID
|
|
191
191
|
reports <customer-id> Recon reports; --limit 1..100
|
|
192
192
|
report <customer-id> <report-id> Recon report detail
|
|
193
|
-
investigations <customer-id> --limit 1..100 --
|
|
193
|
+
investigations <customer-id> --limit 1..100 --issue-id TEXT
|
|
194
|
+
--cursor TEXT or --before ISO
|
|
194
195
|
investigation <customer-id> <id> --revision 1..2147483647
|
|
195
|
-
contracts <customer-id> --status draft
|
|
196
|
+
contracts <customer-id> --status <status>, one of draft, pending,
|
|
197
|
+
active, expired, terminated
|
|
196
198
|
billing <customer-id> Billing overview
|
|
197
|
-
invoices <customer-id> --limit 1..100 --status
|
|
199
|
+
invoices <customer-id> --limit 1..100 --status <status>, one of
|
|
200
|
+
draft, open, paid, uncollectible, void
|
|
198
201
|
subscriptions <customer-id> Stripe subscriptions
|
|
199
202
|
projects <customer-id> Linked projects (internal operators)
|
|
200
203
|
Customer, target, report, and investigation IDs must be UUIDs. Investigations require internal Vigil access.
|