@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
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { CheckupCancelledError, type CancelSignal, type CancelSignalSource } from '../errors.js';
|
|
1
2
|
export declare const CF_CALL_TIMEOUT_MS = 45000;
|
|
2
3
|
export declare const STDOUT_CAP_BYTES: number;
|
|
3
4
|
/** `rulesets get` bodies can be large managed rulesets. */
|
|
4
5
|
export declare const RULESET_STDOUT_CAP_BYTES: number;
|
|
5
6
|
export declare const STDERR_CAP_BYTES: number;
|
|
7
|
+
/** After a cancel, how long withCfRunner waits for `work` to finish (or undo) what it was doing. */
|
|
8
|
+
export declare const WORK_SETTLE_MS = 2000;
|
|
6
9
|
/**
|
|
7
10
|
* Flags that skip confirmations, switch cf to local simulation or project
|
|
8
11
|
* config, or send a request body. No allowlisted argv contains them; the
|
|
@@ -30,6 +33,9 @@ export declare class CfProcessRunner {
|
|
|
30
33
|
readonly privateDir: string;
|
|
31
34
|
private readonly active;
|
|
32
35
|
private disposed;
|
|
36
|
+
private disposing;
|
|
37
|
+
/** Set by `cancel`: every pending and later call rejects with it. */
|
|
38
|
+
private cancelledBy;
|
|
33
39
|
/** Set once the runner kills its children (abort or shutdown), so those kills are not read as timeouts. */
|
|
34
40
|
private terminating;
|
|
35
41
|
private constructor();
|
|
@@ -46,13 +52,22 @@ export declare class CfProcessRunner {
|
|
|
46
52
|
get activeCount(): number;
|
|
47
53
|
/** SIGTERM every live child, then SIGKILL whatever is still running after 2 s. */
|
|
48
54
|
terminateAll(): Promise<void>;
|
|
55
|
+
/** Stop every child and remove the private directory; every caller waits for the same cleanup. */
|
|
49
56
|
dispose(): Promise<void>;
|
|
57
|
+
/** The run was interrupted by `signal`: calls reject with CANCELLED from now on, then everything is disposed. */
|
|
58
|
+
cancel(signal: CancelSignal): Promise<void>;
|
|
59
|
+
get cancellation(): CheckupCancelledError | null;
|
|
50
60
|
}
|
|
51
61
|
/**
|
|
52
|
-
* Run `work` with a private cf runner; the directory is removed afterwards
|
|
53
|
-
*
|
|
62
|
+
* Run `work` with a private cf runner; the directory is removed afterwards.
|
|
63
|
+
* On SIGINT, SIGTERM or SIGHUP live children get SIGTERM (then SIGKILL), and
|
|
64
|
+
* this rejects with CANCELLED (`CheckupCancelledError`) once the directory is
|
|
65
|
+
* gone and `work` has settled (every cf call rejects by then; whatever else it
|
|
66
|
+
* waits on gets WORK_SETTLE_MS), so no result of a cancelled run is printed and
|
|
67
|
+
* nothing `work` writes lands after CANCELLED. `work` checks `runner.cancellation`
|
|
68
|
+
* before (and after) writing anything.
|
|
54
69
|
*/
|
|
55
|
-
export declare function withCfRunner<T>(entry: string, work: (runner: CfProcessRunner) => Promise<T
|
|
70
|
+
export declare function withCfRunner<T>(entry: string, work: (runner: CfProcessRunner) => Promise<T>, signals?: CancelSignalSource): Promise<T>;
|
|
56
71
|
export type CfFailure = {
|
|
57
72
|
kind: 'node_unsupported';
|
|
58
73
|
} | {
|
|
@@ -12,6 +12,7 @@ import { chmod, mkdtemp, rm } from 'node:fs/promises';
|
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { stripVTControlCharacters } from 'node:util';
|
|
15
|
+
import { CANCEL_SIGNALS, CheckupCancelledError } from '../errors.js';
|
|
15
16
|
import { CF_TOKEN_ENV } from './child-env.js';
|
|
16
17
|
export const CF_CALL_TIMEOUT_MS = 45_000;
|
|
17
18
|
export const STDOUT_CAP_BYTES = 8 * 1024 * 1024;
|
|
@@ -19,6 +20,8 @@ export const STDOUT_CAP_BYTES = 8 * 1024 * 1024;
|
|
|
19
20
|
export const RULESET_STDOUT_CAP_BYTES = 16 * 1024 * 1024;
|
|
20
21
|
export const STDERR_CAP_BYTES = 64 * 1024;
|
|
21
22
|
const KILL_GRACE_MS = 2_000;
|
|
23
|
+
/** After a cancel, how long withCfRunner waits for `work` to finish (or undo) what it was doing. */
|
|
24
|
+
export const WORK_SETTLE_MS = 2_000;
|
|
22
25
|
/**
|
|
23
26
|
* Flags that skip confirmations, switch cf to local simulation or project
|
|
24
27
|
* config, or send a request body. No allowlisted argv contains them; the
|
|
@@ -38,6 +41,9 @@ export class CfProcessRunner {
|
|
|
38
41
|
privateDir;
|
|
39
42
|
active = new Set();
|
|
40
43
|
disposed = false;
|
|
44
|
+
disposing = null;
|
|
45
|
+
/** Set by `cancel`: every pending and later call rejects with it. */
|
|
46
|
+
cancelledBy = null;
|
|
41
47
|
/** Set once the runner kills its children (abort or shutdown), so those kills are not read as timeouts. */
|
|
42
48
|
terminating = false;
|
|
43
49
|
constructor(entry, privateDir) {
|
|
@@ -51,11 +57,13 @@ export class CfProcessRunner {
|
|
|
51
57
|
}
|
|
52
58
|
run(argv, options) {
|
|
53
59
|
assertSafeCfArgv(argv);
|
|
60
|
+
if (this.cancelledBy)
|
|
61
|
+
return Promise.reject(this.cancelledBy);
|
|
54
62
|
if (this.disposed)
|
|
55
63
|
return Promise.resolve(failedToStart());
|
|
56
64
|
const timeoutMs = options.timeoutMs ?? CF_CALL_TIMEOUT_MS;
|
|
57
65
|
const stdoutCap = options.stdoutCapBytes ?? STDOUT_CAP_BYTES;
|
|
58
|
-
return new Promise((resolve) => {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
59
67
|
let child;
|
|
60
68
|
try {
|
|
61
69
|
child = spawn(process.execPath, [this.entry, ...argv], {
|
|
@@ -112,6 +120,10 @@ export class CfProcessRunner {
|
|
|
112
120
|
settled = true;
|
|
113
121
|
clearTimeout(timer);
|
|
114
122
|
this.active.delete(child);
|
|
123
|
+
if (this.cancelledBy) {
|
|
124
|
+
reject(this.cancelledBy);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
115
127
|
// A child killed by a signal we did not send for the stdout cap or a
|
|
116
128
|
// shutdown was killed by the spawn timeout.
|
|
117
129
|
if (signal !== null && !stdoutTooLarge && !this.terminating)
|
|
@@ -136,6 +148,8 @@ export class CfProcessRunner {
|
|
|
136
148
|
*/
|
|
137
149
|
runInteractive(argv, env, options = {}) {
|
|
138
150
|
assertSafeCfArgv(argv);
|
|
151
|
+
if (this.cancelledBy)
|
|
152
|
+
return Promise.reject(this.cancelledBy);
|
|
139
153
|
return new Promise((resolve, reject) => {
|
|
140
154
|
const child = spawn(process.execPath, [this.entry, ...argv], {
|
|
141
155
|
shell: false, cwd: this.privateDir, env, stdio: ['inherit', options.stdoutToStderr ? 2 : 'inherit', 'inherit'], windowsHide: true,
|
|
@@ -147,7 +161,10 @@ export class CfProcessRunner {
|
|
|
147
161
|
});
|
|
148
162
|
child.once('close', (code) => {
|
|
149
163
|
this.active.delete(child);
|
|
150
|
-
|
|
164
|
+
if (this.cancelledBy)
|
|
165
|
+
reject(this.cancelledBy);
|
|
166
|
+
else
|
|
167
|
+
resolve(typeof code === 'number' ? code : 1);
|
|
151
168
|
});
|
|
152
169
|
});
|
|
153
170
|
}
|
|
@@ -178,32 +195,73 @@ export class CfProcessRunner {
|
|
|
178
195
|
}, () => undefined);
|
|
179
196
|
});
|
|
180
197
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
198
|
+
/** Stop every child and remove the private directory; every caller waits for the same cleanup. */
|
|
199
|
+
dispose() {
|
|
200
|
+
this.disposing ??= (async () => {
|
|
201
|
+
this.disposed = true;
|
|
202
|
+
await this.terminateAll();
|
|
203
|
+
await rm(this.privateDir, { recursive: true, force: true });
|
|
204
|
+
})();
|
|
205
|
+
return this.disposing;
|
|
206
|
+
}
|
|
207
|
+
/** The run was interrupted by `signal`: calls reject with CANCELLED from now on, then everything is disposed. */
|
|
208
|
+
cancel(signal) {
|
|
209
|
+
this.cancelledBy ??= new CheckupCancelledError(signal);
|
|
210
|
+
return this.dispose();
|
|
211
|
+
}
|
|
212
|
+
get cancellation() {
|
|
213
|
+
return this.cancelledBy;
|
|
187
214
|
}
|
|
188
215
|
}
|
|
189
216
|
function failedToStart() {
|
|
190
217
|
return { exitCode: null, stdout: '', stderr: '', timedOut: false, stdoutTooLarge: false, spawnFailed: true };
|
|
191
218
|
}
|
|
219
|
+
/** Wait for `promise` to settle, at most `ms`. */
|
|
220
|
+
async function settleWithin(promise, ms) {
|
|
221
|
+
let timer;
|
|
222
|
+
await Promise.race([
|
|
223
|
+
promise.then(() => undefined, () => undefined),
|
|
224
|
+
new Promise((resolve) => { timer = setTimeout(resolve, ms); }),
|
|
225
|
+
]);
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
}
|
|
192
228
|
/**
|
|
193
|
-
* Run `work` with a private cf runner; the directory is removed afterwards
|
|
194
|
-
*
|
|
229
|
+
* Run `work` with a private cf runner; the directory is removed afterwards.
|
|
230
|
+
* On SIGINT, SIGTERM or SIGHUP live children get SIGTERM (then SIGKILL), and
|
|
231
|
+
* this rejects with CANCELLED (`CheckupCancelledError`) once the directory is
|
|
232
|
+
* gone and `work` has settled (every cf call rejects by then; whatever else it
|
|
233
|
+
* waits on gets WORK_SETTLE_MS), so no result of a cancelled run is printed and
|
|
234
|
+
* nothing `work` writes lands after CANCELLED. `work` checks `runner.cancellation`
|
|
235
|
+
* before (and after) writing anything.
|
|
195
236
|
*/
|
|
196
|
-
export async function withCfRunner(entry, work) {
|
|
237
|
+
export async function withCfRunner(entry, work, signals = process) {
|
|
197
238
|
const runner = await CfProcessRunner.create(entry);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
239
|
+
let interrupt = () => undefined;
|
|
240
|
+
const interrupted = new Promise((_resolve, reject) => { interrupt = reject; });
|
|
241
|
+
const listeners = CANCEL_SIGNALS.map((signal) => {
|
|
242
|
+
const listener = () => {
|
|
243
|
+
runner.cancel(signal).catch(() => undefined);
|
|
244
|
+
interrupt(runner.cancellation);
|
|
245
|
+
};
|
|
246
|
+
signals.on(signal, listener);
|
|
247
|
+
return { signal, listener };
|
|
248
|
+
});
|
|
249
|
+
let working;
|
|
202
250
|
try {
|
|
203
|
-
|
|
251
|
+
working = work(runner);
|
|
252
|
+
const result = await Promise.race([working, interrupted]);
|
|
253
|
+
if (runner.cancellation)
|
|
254
|
+
throw runner.cancellation;
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (runner.cancellation && working)
|
|
259
|
+
await settleWithin(working, WORK_SETTLE_MS);
|
|
260
|
+
throw runner.cancellation ?? error;
|
|
204
261
|
}
|
|
205
262
|
finally {
|
|
206
|
-
|
|
263
|
+
for (const { signal, listener } of listeners)
|
|
264
|
+
signals.removeListener(signal, listener);
|
|
207
265
|
await runner.dispose();
|
|
208
266
|
}
|
|
209
267
|
}
|
|
@@ -205,9 +205,9 @@ export declare const RulesetDetailData: z.ZodObject<{
|
|
|
205
205
|
export declare const PageRuleData: z.ZodObject<{
|
|
206
206
|
actions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
207
207
|
id: z.ZodEnum<{
|
|
208
|
-
waf: "waf";
|
|
209
208
|
security_level: "security_level";
|
|
210
209
|
browser_check: "browser_check";
|
|
210
|
+
waf: "waf";
|
|
211
211
|
disable_security: "disable_security";
|
|
212
212
|
disable_performance: "disable_performance";
|
|
213
213
|
}>;
|
|
@@ -560,7 +560,6 @@ export declare const SnapshotEntrySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
560
560
|
kind: z.ZodLiteral<"zone_setting">;
|
|
561
561
|
zoneId: z.ZodString;
|
|
562
562
|
settingId: z.ZodEnum<{
|
|
563
|
-
waf: "waf";
|
|
564
563
|
ssl: "ssl";
|
|
565
564
|
always_use_https: "always_use_https";
|
|
566
565
|
min_tls_version: "min_tls_version";
|
|
@@ -575,6 +574,7 @@ export declare const SnapshotEntrySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
575
574
|
minify: "minify";
|
|
576
575
|
early_hints: "early_hints";
|
|
577
576
|
rocket_loader: "rocket_loader";
|
|
577
|
+
waf: "waf";
|
|
578
578
|
automatic_https_rewrites: "automatic_https_rewrites";
|
|
579
579
|
security_header: "security_header";
|
|
580
580
|
opportunistic_encryption: "opportunistic_encryption";
|
|
@@ -790,9 +790,9 @@ export declare const SnapshotEntrySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
790
790
|
data: z.ZodArray<z.ZodObject<{
|
|
791
791
|
actions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
792
792
|
id: z.ZodEnum<{
|
|
793
|
-
waf: "waf";
|
|
794
793
|
security_level: "security_level";
|
|
795
794
|
browser_check: "browser_check";
|
|
795
|
+
waf: "waf";
|
|
796
796
|
disable_security: "disable_security";
|
|
797
797
|
disable_performance: "disable_performance";
|
|
798
798
|
}>;
|
|
@@ -1263,7 +1263,6 @@ declare const SnapshotObjectSchema: z.ZodObject<{
|
|
|
1263
1263
|
kind: z.ZodLiteral<"zone_setting">;
|
|
1264
1264
|
zoneId: z.ZodString;
|
|
1265
1265
|
settingId: z.ZodEnum<{
|
|
1266
|
-
waf: "waf";
|
|
1267
1266
|
ssl: "ssl";
|
|
1268
1267
|
always_use_https: "always_use_https";
|
|
1269
1268
|
min_tls_version: "min_tls_version";
|
|
@@ -1278,6 +1277,7 @@ declare const SnapshotObjectSchema: z.ZodObject<{
|
|
|
1278
1277
|
minify: "minify";
|
|
1279
1278
|
early_hints: "early_hints";
|
|
1280
1279
|
rocket_loader: "rocket_loader";
|
|
1280
|
+
waf: "waf";
|
|
1281
1281
|
automatic_https_rewrites: "automatic_https_rewrites";
|
|
1282
1282
|
security_header: "security_header";
|
|
1283
1283
|
opportunistic_encryption: "opportunistic_encryption";
|
|
@@ -1493,9 +1493,9 @@ declare const SnapshotObjectSchema: z.ZodObject<{
|
|
|
1493
1493
|
data: z.ZodArray<z.ZodObject<{
|
|
1494
1494
|
actions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
1495
1495
|
id: z.ZodEnum<{
|
|
1496
|
-
waf: "waf";
|
|
1497
1496
|
security_level: "security_level";
|
|
1498
1497
|
browser_check: "browser_check";
|
|
1498
|
+
waf: "waf";
|
|
1499
1499
|
disable_security: "disable_security";
|
|
1500
1500
|
disable_performance: "disable_performance";
|
|
1501
1501
|
}>;
|
|
@@ -1962,7 +1962,6 @@ export declare const CloudflareCheckupSnapshotV1Schema: z.ZodObject<{
|
|
|
1962
1962
|
kind: z.ZodLiteral<"zone_setting">;
|
|
1963
1963
|
zoneId: z.ZodString;
|
|
1964
1964
|
settingId: z.ZodEnum<{
|
|
1965
|
-
waf: "waf";
|
|
1966
1965
|
ssl: "ssl";
|
|
1967
1966
|
always_use_https: "always_use_https";
|
|
1968
1967
|
min_tls_version: "min_tls_version";
|
|
@@ -1977,6 +1976,7 @@ export declare const CloudflareCheckupSnapshotV1Schema: z.ZodObject<{
|
|
|
1977
1976
|
minify: "minify";
|
|
1978
1977
|
early_hints: "early_hints";
|
|
1979
1978
|
rocket_loader: "rocket_loader";
|
|
1979
|
+
waf: "waf";
|
|
1980
1980
|
automatic_https_rewrites: "automatic_https_rewrites";
|
|
1981
1981
|
security_header: "security_header";
|
|
1982
1982
|
opportunistic_encryption: "opportunistic_encryption";
|
|
@@ -2192,9 +2192,9 @@ export declare const CloudflareCheckupSnapshotV1Schema: z.ZodObject<{
|
|
|
2192
2192
|
data: z.ZodArray<z.ZodObject<{
|
|
2193
2193
|
actions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
2194
2194
|
id: z.ZodEnum<{
|
|
2195
|
-
waf: "waf";
|
|
2196
2195
|
security_level: "security_level";
|
|
2197
2196
|
browser_check: "browser_check";
|
|
2197
|
+
waf: "waf";
|
|
2198
2198
|
disable_security: "disable_security";
|
|
2199
2199
|
disable_performance: "disable_performance";
|
|
2200
2200
|
}>;
|
|
@@ -2637,6 +2637,12 @@ export declare function createRuleBodyEncoder(): RuleBodyEncoder;
|
|
|
2637
2637
|
export interface IpPseudonymizer {
|
|
2638
2638
|
/** Pseudonymise one A/AAAA content string; memoised by the exact string. */
|
|
2639
2639
|
pseudonymize(content: string): ProjectionResult<string>;
|
|
2640
|
+
/**
|
|
2641
|
+
* Pseudonymise one record list, all or nothing: when one content fails, the
|
|
2642
|
+
* numbering is left as it was, so later pseudonyms never skip a number
|
|
2643
|
+
* (`validateSnapshotSemantics` rejects a gap).
|
|
2644
|
+
*/
|
|
2645
|
+
pseudonymizeAll(contents: readonly string[]): ProjectionResult<string[]>;
|
|
2640
2646
|
}
|
|
2641
2647
|
/**
|
|
2642
2648
|
* Per-prefix sequential pseudonyms, applied in zone list order and record API
|
|
@@ -2672,7 +2678,8 @@ export interface AssembleSnapshotInput {
|
|
|
2672
2678
|
* failure in list order) and keeps no `zone_ruleset` entries, so the scanner
|
|
2673
2679
|
* never falls back to rule-less heads (874-891).
|
|
2674
2680
|
* 2. IP pseudonymisation over `zone_dns_records` in zone list order and record
|
|
2675
|
-
* order; a list with unrepresentable content becomes `collection_failed
|
|
2681
|
+
* order; a list with unrepresentable content becomes `collection_failed`
|
|
2682
|
+
* and uses up no pseudonym numbers.
|
|
2676
2683
|
* 3. Canonical entry order (`expectedEntryKeys`); unexpected entries go last.
|
|
2677
2684
|
* 4. Rule bodies encoded and deduplicated in that order.
|
|
2678
2685
|
* The result is not validated: parse it with `CloudflareCheckupSnapshotV1Schema`.
|
|
@@ -2697,9 +2704,9 @@ export declare const LocalSessionResponseSchema: z.ZodObject<{
|
|
|
2697
2704
|
requester: z.ZodString;
|
|
2698
2705
|
expiresAt: z.ZodISODateTime;
|
|
2699
2706
|
state: z.ZodEnum<{
|
|
2700
|
-
completed: "completed";
|
|
2701
2707
|
awaiting_upload: "awaiting_upload";
|
|
2702
2708
|
processing: "processing";
|
|
2709
|
+
completed: "completed";
|
|
2703
2710
|
}>;
|
|
2704
2711
|
accepts: z.ZodObject<{
|
|
2705
2712
|
schemaVersions: z.ZodArray<z.ZodString>;
|
|
@@ -579,12 +579,60 @@ export function validateSnapshotSemantics(snapshot, ctx) {
|
|
|
579
579
|
if (!(started <= finished) || finished - started > SNAPSHOT_LIMITS.maxCollectionHours * 3_600_000) {
|
|
580
580
|
issue('collection_window', ['collector', 'finishedAt']);
|
|
581
581
|
}
|
|
582
|
-
// 6.
|
|
582
|
+
// 6. DNS contents are `per_prefix_sequential/1` pseudonyms: each prefix
|
|
583
|
+
// numbers its addresses 1, 2, 3, … with no gap, so an address that is not a
|
|
584
|
+
// pseudonym (a real one, or numbers past the count) is rejected.
|
|
585
|
+
const numbersByPrefix = new Map();
|
|
586
|
+
entries.forEach((entry, i) => {
|
|
587
|
+
if (entry.kind !== 'zone_dns_records' || !entry.result.ok)
|
|
588
|
+
return;
|
|
589
|
+
entry.result.data.forEach((record, r) => {
|
|
590
|
+
const path = ['entries', i, 'result', 'data', r, 'content'];
|
|
591
|
+
const pseudonym = parsePseudonym(record.content);
|
|
592
|
+
if (pseudonym === null) {
|
|
593
|
+
issue('ip_not_pseudonymized', path);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const numbers = numbersByPrefix.get(pseudonym.prefix) ?? new Map();
|
|
597
|
+
if (!numbers.has(pseudonym.n))
|
|
598
|
+
numbers.set(pseudonym.n, path);
|
|
599
|
+
numbersByPrefix.set(pseudonym.prefix, numbers);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
for (const numbers of numbersByPrefix.values()) {
|
|
603
|
+
for (const [n, path] of numbers)
|
|
604
|
+
if (n > numbers.size)
|
|
605
|
+
issue('ip_not_pseudonymized', path);
|
|
606
|
+
}
|
|
607
|
+
// 7. Credential tripwire over every string (and record key) in the snapshot.
|
|
583
608
|
walkStrings(snapshot, [], (value, path) => {
|
|
584
609
|
if (containsSecretLikeValue(value))
|
|
585
610
|
issue('secret_like_value', path, 'secret_like_value');
|
|
586
611
|
});
|
|
587
612
|
}
|
|
613
|
+
const PSEUDONYM_V4_PART_RE = /^(?:0|[1-9][0-9]{0,2})$/;
|
|
614
|
+
const PSEUDONYM_V6_N_RE = /^[1-9a-f][0-9a-f]{0,3}$/;
|
|
615
|
+
/**
|
|
616
|
+
* The family and prefix (`createIpPseudonymizer`'s counter key) and the number
|
|
617
|
+
* `n` of a pseudonym, or null when `content` is not one: IPv4 `a.b.${n >> 8}.${n & 255}`
|
|
618
|
+
* or IPv6 `${g0}:${g1}:${g2}:ffff:${n.toString(16)}`, with 1 <= n <= 65535.
|
|
619
|
+
*/
|
|
620
|
+
function parsePseudonym(content) {
|
|
621
|
+
if (content.includes(':')) {
|
|
622
|
+
const groups = content.split(':');
|
|
623
|
+
if (groups.length !== 5 || groups[3] !== 'ffff' || !PSEUDONYM_V6_N_RE.test(groups[4]))
|
|
624
|
+
return null;
|
|
625
|
+
return { prefix: `6|${groups.slice(0, 3).join(':')}`, n: parseInt(groups[4], 16) };
|
|
626
|
+
}
|
|
627
|
+
const parts = content.split('.');
|
|
628
|
+
if (parts.length !== 4 || !PSEUDONYM_V4_PART_RE.test(parts[2]) || !PSEUDONYM_V4_PART_RE.test(parts[3]))
|
|
629
|
+
return null;
|
|
630
|
+
const high = Number(parts[2]);
|
|
631
|
+
const low = Number(parts[3]);
|
|
632
|
+
if (high > 255 || low > 255 || high + low === 0)
|
|
633
|
+
return null;
|
|
634
|
+
return { prefix: `4|${parts[0]}.${parts[1]}`, n: high * 256 + low };
|
|
635
|
+
}
|
|
588
636
|
/**
|
|
589
637
|
* The `wafAnalysis.issues` one reference to a rule body produces under a
|
|
590
638
|
* ruleset phase, and the UTF-8 bytes of the rule text those issues print
|
|
@@ -1375,7 +1423,7 @@ const MAX_PSEUDONYMS_PER_PREFIX = 65_535;
|
|
|
1375
1423
|
export function createIpPseudonymizer() {
|
|
1376
1424
|
const memo = new Map();
|
|
1377
1425
|
const counters = new Map();
|
|
1378
|
-
|
|
1426
|
+
const pseudonymizer = {
|
|
1379
1427
|
pseudonymize(content) {
|
|
1380
1428
|
const known = memo.get(content);
|
|
1381
1429
|
if (known !== undefined)
|
|
@@ -1411,7 +1459,29 @@ export function createIpPseudonymizer() {
|
|
|
1411
1459
|
memo.set(content, pseudonym);
|
|
1412
1460
|
return { ok: true, data: pseudonym };
|
|
1413
1461
|
},
|
|
1462
|
+
pseudonymizeAll(contents) {
|
|
1463
|
+
const savedCounters = new Map(counters);
|
|
1464
|
+
const added = [];
|
|
1465
|
+
const out = [];
|
|
1466
|
+
for (const content of contents) {
|
|
1467
|
+
const known = memo.has(content);
|
|
1468
|
+
const pseudonym = pseudonymizer.pseudonymize(content);
|
|
1469
|
+
if (!pseudonym.ok) {
|
|
1470
|
+
for (const key of added)
|
|
1471
|
+
memo.delete(key);
|
|
1472
|
+
counters.clear();
|
|
1473
|
+
for (const [key, n] of savedCounters)
|
|
1474
|
+
counters.set(key, n);
|
|
1475
|
+
return pseudonym;
|
|
1476
|
+
}
|
|
1477
|
+
if (!known)
|
|
1478
|
+
added.push(content);
|
|
1479
|
+
out.push(pseudonym.data);
|
|
1480
|
+
}
|
|
1481
|
+
return { ok: true, data: out };
|
|
1482
|
+
},
|
|
1414
1483
|
};
|
|
1484
|
+
return pseudonymizer;
|
|
1415
1485
|
}
|
|
1416
1486
|
/**
|
|
1417
1487
|
* Build the snapshot from collected entries. In order:
|
|
@@ -1420,7 +1490,8 @@ export function createIpPseudonymizer() {
|
|
|
1420
1490
|
* failure in list order) and keeps no `zone_ruleset` entries, so the scanner
|
|
1421
1491
|
* never falls back to rule-less heads (874-891).
|
|
1422
1492
|
* 2. IP pseudonymisation over `zone_dns_records` in zone list order and record
|
|
1423
|
-
* order; a list with unrepresentable content becomes `collection_failed
|
|
1493
|
+
* order; a list with unrepresentable content becomes `collection_failed`
|
|
1494
|
+
* and uses up no pseudonym numbers.
|
|
1424
1495
|
* 3. Canonical entry order (`expectedEntryKeys`); unexpected entries go last.
|
|
1425
1496
|
* 4. Rule bodies encoded and deduplicated in that order.
|
|
1426
1497
|
* The result is not validated: parse it with `CloudflareCheckupSnapshotV1Schema`.
|
|
@@ -1463,20 +1534,15 @@ export function assembleSnapshot(input) {
|
|
|
1463
1534
|
continue;
|
|
1464
1535
|
const records = entry.result.data;
|
|
1465
1536
|
const invalidContent = records.some((record) => !pseudonymFormatOk(record.content));
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
const pseudonym = pseudonymizer.pseudonymize(record.content);
|
|
1470
|
-
if (!pseudonym.ok) {
|
|
1471
|
-
failed = pseudonym.detail;
|
|
1472
|
-
break;
|
|
1473
|
-
}
|
|
1474
|
-
mapped.push({ ...record, content: pseudonym.data });
|
|
1475
|
-
}
|
|
1537
|
+
const pseudonyms = invalidContent
|
|
1538
|
+
? { ok: false, detail: 'invalid_output' }
|
|
1539
|
+
: pseudonymizer.pseudonymizeAll(records.map((record) => record.content));
|
|
1476
1540
|
pseudonymized.set(entry, {
|
|
1477
1541
|
kind: 'zone_dns_records',
|
|
1478
1542
|
zoneId,
|
|
1479
|
-
result:
|
|
1543
|
+
result: pseudonyms.ok
|
|
1544
|
+
? { ok: true, data: records.map((record, r) => ({ ...record, content: pseudonyms.data[r] })) }
|
|
1545
|
+
: collectionFailedResult(pseudonyms.detail),
|
|
1480
1546
|
});
|
|
1481
1547
|
}
|
|
1482
1548
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const LOCAL_ERROR_CODES: readonly ["NODE_UNSUPPORTED", "CF_NOT_FOUND", "CF_WRONG_BINARY", "CF_UNSUPPORTED_VERSION", "CF_ALLOWLIST_MISMATCH", "CF_INSTALL_FAILED", "CF_AUTH_REQUIRED", "CF_PERMISSION_DENIED", "CF_RATE_LIMITED", "CF_ERROR", "ACCOUNT_SELECTION_REQUIRED", "ACCOUNT_NOT_VISIBLE", "ACCOUNT_TOO_LARGE", "ACCOUNT_MISMATCH", "OUTPUT_EXISTS", "SNAPSHOT_INVALID", "SNAPSHOT_TOO_LARGE", "SNAPSHOT_STALE", "SNAPSHOT_SECRET_LIKE_VALUE", "EMAIL_INVALID", "EMAIL_CODE_INVALID", "VERIFICATION_INVALID", "VERIFICATION_EXPIRED", "SESSION_INVALID", "SESSION_EXPIRED", "SESSION_USED", "SESSION_BUSY", "SESSION_CLOSED", "SUBMIT_ORIGIN_UNTRUSTED", "CONFIRMATION_REQUIRED", "CLI_UPGRADE_REQUIRED", "LOCAL_MODE_DISABLED", "VIGILBASE_RATE_LIMITED", "VIGILBASE_CHALLENGED", "SUBMIT_FAILED"];
|
|
1
|
+
export declare const LOCAL_ERROR_CODES: readonly ["NODE_UNSUPPORTED", "CWD_UNAVAILABLE", "CF_NOT_FOUND", "CF_WRONG_BINARY", "CF_UNSUPPORTED_VERSION", "CF_ALLOWLIST_MISMATCH", "CF_INSTALL_FAILED", "CF_AUTH_REQUIRED", "CF_PERMISSION_DENIED", "CF_RATE_LIMITED", "CF_ERROR", "CANCELLED", "ACCOUNT_SELECTION_REQUIRED", "ACCOUNT_NOT_VISIBLE", "ACCOUNT_TOO_LARGE", "ACCOUNT_MISMATCH", "OUTPUT_EXISTS", "SNAPSHOT_INVALID", "SNAPSHOT_TOO_LARGE", "SNAPSHOT_STALE", "SNAPSHOT_SECRET_LIKE_VALUE", "EMAIL_INVALID", "EMAIL_CODE_INVALID", "VERIFICATION_INVALID", "VERIFICATION_EXPIRED", "SESSION_INVALID", "SESSION_EXPIRED", "SESSION_USED", "SESSION_BUSY", "SESSION_CLOSED", "SUBMIT_ORIGIN_UNTRUSTED", "CONFIRMATION_REQUIRED", "CLI_UPGRADE_REQUIRED", "LOCAL_MODE_DISABLED", "VIGILBASE_RATE_LIMITED", "VIGILBASE_CHALLENGED", "SUBMIT_FAILED"];
|
|
2
2
|
export type LocalCheckupErrorCode = (typeof LOCAL_ERROR_CODES)[number];
|
|
3
3
|
/** LOCAL_MODE_DISABLED: the browser checkup on the origin the CLI talks to (vigilbase.com, or a loopback dev server). */
|
|
4
4
|
export declare function browserCheckupHint(origin: string): string;
|
|
@@ -12,6 +12,20 @@ export declare const LOCAL_ERROR_HINTS: Record<LocalCheckupErrorCode, string>;
|
|
|
12
12
|
export declare const ENV_TOKEN_AUTH_HINT = "Set CLOUDFLARE_API_TOKEN in this shell to a valid read-only Cloudflare API token (read -rs CLOUDFLARE_API_TOKEN && export CLOUDFLARE_API_TOKEN) and run the same command again, or unset it (or use --cf-auth profile) to sign in with your browser instead.";
|
|
13
13
|
/** SNAPSHOT_INVALID for an `inspect` or `submit` path with no file: nothing to collect again, so how to make one. */
|
|
14
14
|
export declare const SNAPSHOT_MISSING_HINT = "Check the file path. To collect a snapshot into a file, run `npx @vigilbase/cli cloudflare-checkup collect --out <file>`.";
|
|
15
|
+
/** SNAPSHOT_INVALID for a file that cannot be read: its permissions, not its contents. */
|
|
16
|
+
export declare const SNAPSHOT_UNREADABLE_HINT = "Check the permissions of the file and of its directories, or copy it somewhere you can read it.";
|
|
17
|
+
/**
|
|
18
|
+
* A snapshot file that cannot be uploaded, from a command that only reads it
|
|
19
|
+
* (inspect, submit, or the re-read before the one command uploads): running
|
|
20
|
+
* that command again reads the same file, so how to collect a fresh one.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SNAPSHOT_INVALID_FILE_HINT = "The file is not a valid snapshot. Collect a fresh one with `npx @vigilbase/cli cloudflare-checkup` (or `npx @vigilbase/cli cloudflare-checkup collect --out <file>`).";
|
|
23
|
+
export declare const SNAPSHOT_STALE_FILE_HINT = "Snapshots are accepted for 24 hours. Collect a fresh one with `npx @vigilbase/cli cloudflare-checkup` (or `npx @vigilbase/cli cloudflare-checkup collect --out <file>`).";
|
|
24
|
+
export declare const SNAPSHOT_SECRET_FILE_HINT = "Remove the credential-like value from your Cloudflare settings. Collect a fresh one with `npx @vigilbase/cli cloudflare-checkup` (or `npx @vigilbase/cli cloudflare-checkup collect --out <file>`).";
|
|
25
|
+
/** CF_NOT_FOUND from the advanced commands (and `--cf-bin`), which never install cf. */
|
|
26
|
+
export declare const CF_NOT_FOUND_ADVANCED_HINT = "Run `npx @vigilbase/cli cloudflare-checkup` without --cf-bin (it can install cf), install cf with `npm install --global cf@1.0.0-beta.13`, or pass --cf-bin <path> to a cf package.";
|
|
27
|
+
/** CF_WRONG_BINARY when a cf package was found but `cf --version` fails. */
|
|
28
|
+
export declare const CF_DAMAGED_HINT = "Reinstall cf with `npm install --global cf@1.0.0-beta.13`, or run `npx @vigilbase/cli cloudflare-checkup` without --cf-bin and let it install the tested cf.";
|
|
15
29
|
/** CF_PERMISSION_DENIED in env mode: the token lacks a read permission; the browser sign-in is the other way out. */
|
|
16
30
|
export declare const ENV_TOKEN_PERMISSION_HINT = "The API token in CLOUDFLARE_API_TOKEN cannot read everything the checkup needs. Add the read permissions to it, or unset it (or use --cf-auth profile) to sign in with your browser instead, then run the same command again.";
|
|
17
31
|
export declare class LocalCheckupError extends Error {
|
|
@@ -29,3 +43,22 @@ export declare class LocalCheckupError extends Error {
|
|
|
29
43
|
get isRateLimited(): boolean;
|
|
30
44
|
}
|
|
31
45
|
export declare function isLocalCheckupError(error: unknown): error is LocalCheckupError;
|
|
46
|
+
/** The signals that cancel a cf run or the cf install, and the exit code each gives (128 + its number). */
|
|
47
|
+
export declare const CANCEL_EXIT_CODES: Readonly<{
|
|
48
|
+
readonly SIGINT: 130;
|
|
49
|
+
readonly SIGTERM: 143;
|
|
50
|
+
readonly SIGHUP: 129;
|
|
51
|
+
}>;
|
|
52
|
+
export type CancelSignal = keyof typeof CANCEL_EXIT_CODES;
|
|
53
|
+
export declare const CANCEL_SIGNALS: CancelSignal[];
|
|
54
|
+
/** Where cancellation is heard: the process, or a plain emitter in tests. */
|
|
55
|
+
export type CancelSignalSource = {
|
|
56
|
+
on(event: CancelSignal, listener: () => void): unknown;
|
|
57
|
+
removeListener(event: CancelSignal, listener: () => void): unknown;
|
|
58
|
+
};
|
|
59
|
+
/** CANCELLED: cf (or npm) was stopped and its files removed; `runCli` exits with `exitCode` and prints no result. */
|
|
60
|
+
export declare class CheckupCancelledError extends LocalCheckupError {
|
|
61
|
+
readonly signal: CancelSignal;
|
|
62
|
+
readonly exitCode: (typeof CANCEL_EXIT_CODES)[CancelSignal];
|
|
63
|
+
constructor(signal: CancelSignal);
|
|
64
|
+
}
|
|
@@ -11,8 +11,8 @@ import { CF_TOKEN_ENV } from './cf/child-env.js';
|
|
|
11
11
|
import { PRODUCTION_ORIGIN } from './contract.js';
|
|
12
12
|
export const LOCAL_ERROR_CODES = [
|
|
13
13
|
// Environment and cf
|
|
14
|
-
'NODE_UNSUPPORTED', 'CF_NOT_FOUND', 'CF_WRONG_BINARY', 'CF_UNSUPPORTED_VERSION', 'CF_ALLOWLIST_MISMATCH',
|
|
15
|
-
'CF_INSTALL_FAILED', 'CF_AUTH_REQUIRED', 'CF_PERMISSION_DENIED', 'CF_RATE_LIMITED', 'CF_ERROR',
|
|
14
|
+
'NODE_UNSUPPORTED', 'CWD_UNAVAILABLE', 'CF_NOT_FOUND', 'CF_WRONG_BINARY', 'CF_UNSUPPORTED_VERSION', 'CF_ALLOWLIST_MISMATCH',
|
|
15
|
+
'CF_INSTALL_FAILED', 'CF_AUTH_REQUIRED', 'CF_PERMISSION_DENIED', 'CF_RATE_LIMITED', 'CF_ERROR', 'CANCELLED',
|
|
16
16
|
// Account
|
|
17
17
|
'ACCOUNT_SELECTION_REQUIRED', 'ACCOUNT_NOT_VISIBLE', 'ACCOUNT_TOO_LARGE', 'ACCOUNT_MISMATCH',
|
|
18
18
|
// Files and snapshots
|
|
@@ -26,6 +26,8 @@ export const LOCAL_ERROR_CODES = [
|
|
|
26
26
|
'VIGILBASE_RATE_LIMITED', 'VIGILBASE_CHALLENGED', 'SUBMIT_FAILED',
|
|
27
27
|
];
|
|
28
28
|
const INSTALL_CF = '`npm install --global cf@1.0.0-beta.13`';
|
|
29
|
+
/** How to get a fresh snapshot, for hints of the commands that only read one (inspect, submit). */
|
|
30
|
+
const COLLECT_FRESH = 'Collect a fresh one with `npx @vigilbase/cli cloudflare-checkup` (or `npx @vigilbase/cli cloudflare-checkup collect --out <file>`).';
|
|
29
31
|
/** LOCAL_MODE_DISABLED: the browser checkup on the origin the CLI talks to (vigilbase.com, or a loopback dev server). */
|
|
30
32
|
export function browserCheckupHint(origin) {
|
|
31
33
|
return `Run the Cloudflare Checkup in your browser instead: ${origin}/tools/cloudflare-checkup`;
|
|
@@ -33,6 +35,7 @@ export function browserCheckupHint(origin) {
|
|
|
33
35
|
/** One fix per code; shown after the error and in `--json` as `hint`. */
|
|
34
36
|
export const LOCAL_ERROR_HINTS = {
|
|
35
37
|
NODE_UNSUPPORTED: 'Install Node.js 22.18 or newer from https://nodejs.org (cf runs under the same Node), then run the same command again.',
|
|
38
|
+
CWD_UNAVAILABLE: 'Change to a directory that exists and that you can read (for example `cd ~`), then run the same command again.',
|
|
36
39
|
CF_NOT_FOUND: `Run the same command again in a terminal and let it install cf (add --yes when no terminal is available), or install it yourself with ${INSTALL_CF}.`,
|
|
37
40
|
CF_WRONG_BINARY: `Another program named cf (for example the Cloud Foundry CLI, or a pnpm or Volta shim) was found. Run the same command again without --cf-bin and let it install cf, or pass --cf-bin "$(npm root -g)/cf".`,
|
|
38
41
|
CF_UNSUPPORTED_VERSION: `Run the same command again without --cf-bin and let it install the tested cf 1.0.0-beta.13, or install it yourself with ${INSTALL_CF}.`,
|
|
@@ -45,13 +48,13 @@ export const LOCAL_ERROR_HINTS = {
|
|
|
45
48
|
CF_ERROR: 'Run the same command again in a few minutes. If it persists, contact help@vigilbase.com.',
|
|
46
49
|
ACCOUNT_SELECTION_REQUIRED: 'Run the same command again in a terminal to choose the account, or add --account-id <ACCOUNT_ID> with one of the IDs listed.',
|
|
47
50
|
ACCOUNT_NOT_VISIBLE: 'Check that your Cloudflare user can read the account and its zones, then run the same command again.',
|
|
48
|
-
ACCOUNT_TOO_LARGE:
|
|
51
|
+
ACCOUNT_TOO_LARGE: `Local collection supports up to 100 zones. ${browserCheckupHint(PRODUCTION_ORIGIN)}`,
|
|
49
52
|
ACCOUNT_MISMATCH: 'This checkup is for a different Cloudflare account. Run the same command again without --account-id to start a new checkup.',
|
|
50
53
|
OUTPUT_EXISTS: 'Choose another --out path, or pass --overwrite to replace the file.',
|
|
51
54
|
SNAPSHOT_INVALID: 'The file is not a valid snapshot. Run the same command again to collect a fresh one.',
|
|
52
|
-
SNAPSHOT_TOO_LARGE:
|
|
55
|
+
SNAPSHOT_TOO_LARGE: `The snapshot is over the upload limit. ${browserCheckupHint(PRODUCTION_ORIGIN)}`,
|
|
53
56
|
SNAPSHOT_STALE: 'Snapshots are accepted for 24 hours. Run the same command again to collect a fresh one.',
|
|
54
|
-
SNAPSHOT_SECRET_LIKE_VALUE: 'The snapshot contains a value that looks like a credential. Remove it from
|
|
57
|
+
SNAPSHOT_SECRET_LIKE_VALUE: 'The snapshot contains a value that looks like a credential. Remove it from your Cloudflare settings (a name, description or other text field), then run the same command again.',
|
|
55
58
|
EMAIL_INVALID: 'Use your work email address (personal and disposable addresses are not accepted), then run the same command again.',
|
|
56
59
|
EMAIL_CODE_INVALID: 'Run the same command again to get a new code, and enter the 6 digits from the newest email.',
|
|
57
60
|
VERIFICATION_INVALID: 'Run the same command again to get a new code by email.',
|
|
@@ -68,6 +71,7 @@ export const LOCAL_ERROR_HINTS = {
|
|
|
68
71
|
VIGILBASE_RATE_LIMITED: 'Too many requests to vigilbase.com. Wait as long as the error says (a few minutes if it says nothing), then run the same command again.',
|
|
69
72
|
VIGILBASE_CHALLENGED: 'vigilbase.com asked for a browser challenge. Try another network, or contact help@vigilbase.com.',
|
|
70
73
|
SUBMIT_FAILED: 'Run the same command again in a few minutes. If it persists, contact help@vigilbase.com.',
|
|
74
|
+
CANCELLED: 'Nothing was uploaded. Run the same command again when you are ready.',
|
|
71
75
|
};
|
|
72
76
|
/**
|
|
73
77
|
* CF_AUTH_REQUIRED in env mode (`--cf-auth env`, or `auto` with the variable
|
|
@@ -77,6 +81,20 @@ export const LOCAL_ERROR_HINTS = {
|
|
|
77
81
|
export const ENV_TOKEN_AUTH_HINT = `Set ${CF_TOKEN_ENV} in this shell to a valid read-only Cloudflare API token (read -rs ${CF_TOKEN_ENV} && export ${CF_TOKEN_ENV}) and run the same command again, or unset it (or use --cf-auth profile) to sign in with your browser instead.`;
|
|
78
82
|
/** SNAPSHOT_INVALID for an `inspect` or `submit` path with no file: nothing to collect again, so how to make one. */
|
|
79
83
|
export const SNAPSHOT_MISSING_HINT = 'Check the file path. To collect a snapshot into a file, run `npx @vigilbase/cli cloudflare-checkup collect --out <file>`.';
|
|
84
|
+
/** SNAPSHOT_INVALID for a file that cannot be read: its permissions, not its contents. */
|
|
85
|
+
export const SNAPSHOT_UNREADABLE_HINT = 'Check the permissions of the file and of its directories, or copy it somewhere you can read it.';
|
|
86
|
+
/**
|
|
87
|
+
* A snapshot file that cannot be uploaded, from a command that only reads it
|
|
88
|
+
* (inspect, submit, or the re-read before the one command uploads): running
|
|
89
|
+
* that command again reads the same file, so how to collect a fresh one.
|
|
90
|
+
*/
|
|
91
|
+
export const SNAPSHOT_INVALID_FILE_HINT = `The file is not a valid snapshot. ${COLLECT_FRESH}`;
|
|
92
|
+
export const SNAPSHOT_STALE_FILE_HINT = `Snapshots are accepted for 24 hours. ${COLLECT_FRESH}`;
|
|
93
|
+
export const SNAPSHOT_SECRET_FILE_HINT = `Remove the credential-like value from your Cloudflare settings. ${COLLECT_FRESH}`;
|
|
94
|
+
/** CF_NOT_FOUND from the advanced commands (and `--cf-bin`), which never install cf. */
|
|
95
|
+
export const CF_NOT_FOUND_ADVANCED_HINT = `Run \`npx @vigilbase/cli cloudflare-checkup\` without --cf-bin (it can install cf), install cf with ${INSTALL_CF}, or pass --cf-bin <path> to a cf package.`;
|
|
96
|
+
/** CF_WRONG_BINARY when a cf package was found but `cf --version` fails. */
|
|
97
|
+
export const CF_DAMAGED_HINT = `Reinstall cf with ${INSTALL_CF}, or run \`npx @vigilbase/cli cloudflare-checkup\` without --cf-bin and let it install the tested cf.`;
|
|
80
98
|
/** CF_PERMISSION_DENIED in env mode: the token lacks a read permission; the browser sign-in is the other way out. */
|
|
81
99
|
export const ENV_TOKEN_PERMISSION_HINT = `The API token in ${CF_TOKEN_ENV} cannot read everything the checkup needs. Add the read permissions to it, or unset it (or use --cf-auth profile) to sign in with your browser instead, then run the same command again.`;
|
|
82
100
|
export class LocalCheckupError extends Error {
|
|
@@ -103,3 +121,16 @@ export class LocalCheckupError extends Error {
|
|
|
103
121
|
export function isLocalCheckupError(error) {
|
|
104
122
|
return error instanceof LocalCheckupError;
|
|
105
123
|
}
|
|
124
|
+
/** The signals that cancel a cf run or the cf install, and the exit code each gives (128 + its number). */
|
|
125
|
+
export const CANCEL_EXIT_CODES = Object.freeze({ SIGINT: 130, SIGTERM: 143, SIGHUP: 129 });
|
|
126
|
+
export const CANCEL_SIGNALS = Object.keys(CANCEL_EXIT_CODES);
|
|
127
|
+
/** CANCELLED: cf (or npm) was stopped and its files removed; `runCli` exits with `exitCode` and prints no result. */
|
|
128
|
+
export class CheckupCancelledError extends LocalCheckupError {
|
|
129
|
+
signal;
|
|
130
|
+
exitCode;
|
|
131
|
+
constructor(signal) {
|
|
132
|
+
super('CANCELLED', 'Cancelled.');
|
|
133
|
+
this.signal = signal;
|
|
134
|
+
this.exitCode = CANCEL_EXIT_CODES[signal];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -5,6 +5,12 @@ import type { EntryCounts } from './snapshot-file.js';
|
|
|
5
5
|
* (`...` on an ASCII-only console).
|
|
6
6
|
*/
|
|
7
7
|
export declare function displayText(value: string, max: number, ellipsis?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* A file name the user passed, in a line of plain output: controls, line
|
|
10
|
+
* breaks and invisible format characters become visible escapes (`\n`,
|
|
11
|
+
* `\u001b`), so a crafted name cannot add or rewrite lines.
|
|
12
|
+
*/
|
|
13
|
+
export declare function displayFileName(value: string): string;
|
|
8
14
|
/** `0123…cdef` (`0123...cdef` with the ASCII `ellipsis` of the terminal UI). */
|
|
9
15
|
export declare function maskId(id: string, ellipsis?: string): string;
|
|
10
16
|
/** `9f2c…a71e` */
|
|
@@ -14,6 +14,15 @@ export function displayText(value, max, ellipsis = '…') {
|
|
|
14
14
|
const flat = sanitizeTerminal(value.replace(/[\s\u0085\u2028\u2029]+/g, ' ')).trim();
|
|
15
15
|
return flat.length > max ? `${flat.slice(0, Math.max(0, max - ellipsis.length))}${ellipsis}` : flat;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* A file name the user passed, in a line of plain output: controls, line
|
|
19
|
+
* breaks and invisible format characters become visible escapes (`\n`,
|
|
20
|
+
* `\u001b`), so a crafted name cannot add or rewrite lines.
|
|
21
|
+
*/
|
|
22
|
+
export function displayFileName(value) {
|
|
23
|
+
const named = { '\n': '\\n', '\r': '\\r', '\t': '\\t' };
|
|
24
|
+
return value.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, (char) => named[char] ?? `\\u${char.codePointAt(0).toString(16).padStart(4, '0')}`);
|
|
25
|
+
}
|
|
17
26
|
/** `0123…cdef` (`0123...cdef` with the ASCII `ellipsis` of the terminal UI). */
|
|
18
27
|
export function maskId(id, ellipsis = '…') {
|
|
19
28
|
return id.length > 8 ? `${id.slice(0, 4)}${ellipsis}${id.slice(-4)}` : id;
|