@vigilbase/cli 0.1.4 → 0.2.1

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 (191) hide show
  1. package/CLOUDFLARE.md +3 -4
  2. package/COMMANDS.md +158 -83
  3. package/LOCAL.md +721 -0
  4. package/ONE.md +2 -4
  5. package/README.md +341 -123
  6. package/SECURITY.md +2 -2
  7. package/dist/auth/device-auth.d.ts +0 -1
  8. package/dist/auth/device-auth.js +13 -8
  9. package/dist/auth/index.d.ts +4 -2
  10. package/dist/auth/index.js +3 -2
  11. package/dist/auth/provider.d.ts +18 -0
  12. package/dist/auth/provider.js +71 -0
  13. package/dist/auth/refresh.d.ts +29 -0
  14. package/dist/auth/refresh.js +199 -0
  15. package/dist/auth/store.d.ts +103 -4
  16. package/dist/auth/store.js +400 -74
  17. package/dist/auth/token-metadata.d.ts +9 -0
  18. package/dist/auth/token-metadata.js +21 -0
  19. package/dist/auth/types.d.ts +8 -2
  20. package/dist/auth/types.js +0 -1
  21. package/dist/bin.d.ts +0 -1
  22. package/dist/bin.js +0 -1
  23. package/dist/cli.d.ts +4 -2
  24. package/dist/cli.js +45 -15
  25. package/dist/commands/account.d.ts +0 -1
  26. package/dist/commands/account.js +0 -1
  27. package/dist/commands/auth.d.ts +0 -1
  28. package/dist/commands/auth.js +82 -22
  29. package/dist/commands/billing.d.ts +0 -1
  30. package/dist/commands/billing.js +0 -1
  31. package/dist/commands/cloudflare.d.ts +1 -2
  32. package/dist/commands/cloudflare.js +6 -6
  33. package/dist/commands/helpers.d.ts +15 -1
  34. package/dist/commands/helpers.js +2 -3
  35. package/dist/commands/index.d.ts +3 -2
  36. package/dist/commands/index.js +6 -2
  37. package/dist/commands/legacy-validation.d.ts +0 -1
  38. package/dist/commands/legacy-validation.js +0 -1
  39. package/dist/commands/local.d.ts +35 -0
  40. package/dist/commands/local.js +1543 -0
  41. package/dist/commands/one.d.ts +0 -1
  42. package/dist/commands/one.js +6 -7
  43. package/dist/commands/public-tools.d.ts +0 -1
  44. package/dist/commands/public-tools.js +55 -17
  45. package/dist/commands/runs.d.ts +0 -1
  46. package/dist/commands/runs.js +0 -1
  47. package/dist/commands/security.d.ts +1 -2
  48. package/dist/commands/security.js +1 -2
  49. package/dist/commands/watchdog.d.ts +0 -1
  50. package/dist/commands/watchdog.js +0 -1
  51. package/dist/index.d.ts +3 -3
  52. package/dist/index.js +1 -2
  53. package/dist/lib/banner.d.ts +0 -1
  54. package/dist/lib/banner.js +0 -1
  55. package/dist/lib/client.d.ts +4 -2
  56. package/dist/lib/client.js +42 -18
  57. package/dist/lib/cloudflare-client.d.ts +0 -1
  58. package/dist/lib/cloudflare-client.js +0 -1
  59. package/dist/lib/config.d.ts +31 -3
  60. package/dist/lib/config.js +72 -13
  61. package/dist/lib/format.d.ts +2 -1
  62. package/dist/lib/format.js +27 -7
  63. package/dist/lib/help.d.ts +1 -2
  64. package/dist/lib/help.js +37 -17
  65. package/dist/lib/network.d.ts +10 -1
  66. package/dist/lib/network.js +33 -5
  67. package/dist/lib/one-client.d.ts +5 -3
  68. package/dist/lib/one-client.js +49 -3
  69. package/dist/lib/one-operations.d.ts +0 -1
  70. package/dist/lib/one-operations.js +0 -1
  71. package/dist/lib/open-url.d.ts +0 -1
  72. package/dist/lib/open-url.js +0 -1
  73. package/dist/lib/platform.d.ts +2 -0
  74. package/dist/lib/platform.js +4 -0
  75. package/dist/lib/progress.d.ts +0 -1
  76. package/dist/lib/progress.js +0 -1
  77. package/dist/lib/security-client.d.ts +1 -2
  78. package/dist/lib/security-client.js +0 -1
  79. package/dist/lib/skill-fetch.d.ts +0 -1
  80. package/dist/lib/skill-fetch.js +0 -1
  81. package/dist/lib/system-trust.d.ts +0 -1
  82. package/dist/lib/system-trust.js +0 -1
  83. package/dist/lib/tui.d.ts +308 -0
  84. package/dist/lib/tui.js +846 -0
  85. package/dist/local/cloudflare-checkup/cf/allowlist.d.ts +94 -0
  86. package/dist/local/cloudflare-checkup/cf/allowlist.js +183 -0
  87. package/dist/local/cloudflare-checkup/cf/auth-lifecycle.d.ts +112 -0
  88. package/dist/local/cloudflare-checkup/cf/auth-lifecycle.js +312 -0
  89. package/dist/local/cloudflare-checkup/cf/child-env.d.ts +31 -0
  90. package/dist/local/cloudflare-checkup/cf/child-env.js +80 -0
  91. package/dist/local/cloudflare-checkup/cf/install.d.ts +105 -0
  92. package/dist/local/cloudflare-checkup/cf/install.js +308 -0
  93. package/dist/local/cloudflare-checkup/cf/locate.d.ts +43 -0
  94. package/dist/local/cloudflare-checkup/cf/locate.js +227 -0
  95. package/dist/local/cloudflare-checkup/cf/run-cf.d.ts +88 -0
  96. package/dist/local/cloudflare-checkup/cf/run-cf.js +274 -0
  97. package/dist/local/cloudflare-checkup/collector.d.ts +47 -0
  98. package/dist/local/cloudflare-checkup/collector.js +295 -0
  99. package/dist/local/cloudflare-checkup/contract.d.ts +2804 -0
  100. package/dist/local/cloudflare-checkup/contract.js +1620 -0
  101. package/dist/local/cloudflare-checkup/email-verification.d.ts +124 -0
  102. package/dist/local/cloudflare-checkup/email-verification.js +211 -0
  103. package/dist/local/cloudflare-checkup/errors.d.ts +29 -0
  104. package/dist/local/cloudflare-checkup/errors.js +103 -0
  105. package/dist/local/cloudflare-checkup/output.d.ts +23 -0
  106. package/dist/local/cloudflare-checkup/output.js +88 -0
  107. package/dist/local/cloudflare-checkup/prompts.d.ts +40 -0
  108. package/dist/local/cloudflare-checkup/prompts.js +139 -0
  109. package/dist/local/cloudflare-checkup/rate-budget.d.ts +41 -0
  110. package/dist/local/cloudflare-checkup/rate-budget.js +79 -0
  111. package/dist/local/cloudflare-checkup/snapshot-file.d.ts +52 -0
  112. package/dist/local/cloudflare-checkup/snapshot-file.js +185 -0
  113. package/dist/local/cloudflare-checkup/submit-client.d.ts +112 -0
  114. package/dist/local/cloudflare-checkup/submit-client.js +395 -0
  115. package/dist/local/cloudflare-checkup/terminal-ui.d.ts +170 -0
  116. package/dist/local/cloudflare-checkup/terminal-ui.js +403 -0
  117. package/dist/types.d.ts +25 -3
  118. package/dist/types.js +16 -1
  119. package/dist/version.d.ts +0 -1
  120. package/dist/version.js +0 -1
  121. package/package.json +12 -12
  122. package/dist/auth/device-auth.d.ts.map +0 -1
  123. package/dist/auth/device-auth.js.map +0 -1
  124. package/dist/auth/index.d.ts.map +0 -1
  125. package/dist/auth/index.js.map +0 -1
  126. package/dist/auth/store.d.ts.map +0 -1
  127. package/dist/auth/store.js.map +0 -1
  128. package/dist/auth/types.d.ts.map +0 -1
  129. package/dist/auth/types.js.map +0 -1
  130. package/dist/bin.d.ts.map +0 -1
  131. package/dist/bin.js.map +0 -1
  132. package/dist/cli.d.ts.map +0 -1
  133. package/dist/cli.js.map +0 -1
  134. package/dist/commands/account.d.ts.map +0 -1
  135. package/dist/commands/account.js.map +0 -1
  136. package/dist/commands/auth.d.ts.map +0 -1
  137. package/dist/commands/auth.js.map +0 -1
  138. package/dist/commands/billing.d.ts.map +0 -1
  139. package/dist/commands/billing.js.map +0 -1
  140. package/dist/commands/cloudflare.d.ts.map +0 -1
  141. package/dist/commands/cloudflare.js.map +0 -1
  142. package/dist/commands/helpers.d.ts.map +0 -1
  143. package/dist/commands/helpers.js.map +0 -1
  144. package/dist/commands/index.d.ts.map +0 -1
  145. package/dist/commands/index.js.map +0 -1
  146. package/dist/commands/legacy-validation.d.ts.map +0 -1
  147. package/dist/commands/legacy-validation.js.map +0 -1
  148. package/dist/commands/one.d.ts.map +0 -1
  149. package/dist/commands/one.js.map +0 -1
  150. package/dist/commands/public-tools.d.ts.map +0 -1
  151. package/dist/commands/public-tools.js.map +0 -1
  152. package/dist/commands/runs.d.ts.map +0 -1
  153. package/dist/commands/runs.js.map +0 -1
  154. package/dist/commands/security.d.ts.map +0 -1
  155. package/dist/commands/security.js.map +0 -1
  156. package/dist/commands/watchdog.d.ts.map +0 -1
  157. package/dist/commands/watchdog.js.map +0 -1
  158. package/dist/index.d.ts.map +0 -1
  159. package/dist/index.js.map +0 -1
  160. package/dist/lib/banner.d.ts.map +0 -1
  161. package/dist/lib/banner.js.map +0 -1
  162. package/dist/lib/client.d.ts.map +0 -1
  163. package/dist/lib/client.js.map +0 -1
  164. package/dist/lib/cloudflare-client.d.ts.map +0 -1
  165. package/dist/lib/cloudflare-client.js.map +0 -1
  166. package/dist/lib/config.d.ts.map +0 -1
  167. package/dist/lib/config.js.map +0 -1
  168. package/dist/lib/format.d.ts.map +0 -1
  169. package/dist/lib/format.js.map +0 -1
  170. package/dist/lib/help.d.ts.map +0 -1
  171. package/dist/lib/help.js.map +0 -1
  172. package/dist/lib/network.d.ts.map +0 -1
  173. package/dist/lib/network.js.map +0 -1
  174. package/dist/lib/one-client.d.ts.map +0 -1
  175. package/dist/lib/one-client.js.map +0 -1
  176. package/dist/lib/one-operations.d.ts.map +0 -1
  177. package/dist/lib/one-operations.js.map +0 -1
  178. package/dist/lib/open-url.d.ts.map +0 -1
  179. package/dist/lib/open-url.js.map +0 -1
  180. package/dist/lib/progress.d.ts.map +0 -1
  181. package/dist/lib/progress.js.map +0 -1
  182. package/dist/lib/security-client.d.ts.map +0 -1
  183. package/dist/lib/security-client.js.map +0 -1
  184. package/dist/lib/skill-fetch.d.ts.map +0 -1
  185. package/dist/lib/skill-fetch.js.map +0 -1
  186. package/dist/lib/system-trust.d.ts.map +0 -1
  187. package/dist/lib/system-trust.js.map +0 -1
  188. package/dist/types.d.ts.map +0 -1
  189. package/dist/types.js.map +0 -1
  190. package/dist/version.d.ts.map +0 -1
  191. package/dist/version.js.map +0 -1
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Prompts and confirmations of local checkups.
3
+ *
4
+ * Every prompt goes to stderr, so `--json` keeps stdout one JSON object. On a
5
+ * terminal, answers are typed at a visible prompt. Without one, only the email
6
+ * verification reads its answers (the work email without `--email`, and the
7
+ * emailed code) from piped stdin, one line each, so a script can write the code
8
+ * once it sees the prompt; every other question needs a flag (`--yes`,
9
+ * `--account-id`). What is read is never echoed in errors.
10
+ */
11
+ import { createInterface } from 'node:readline/promises';
12
+ import { LocalCheckupError } from './errors.js';
13
+ /** One piped answer is at most this long (an email address is at most 254 bytes). */
14
+ export const MAX_ANSWER_BYTES = 256;
15
+ function isTty(stream) {
16
+ return Boolean(stream && stream.isTTY);
17
+ }
18
+ export function hasInteractiveInput(stdin) {
19
+ return isTty(stdin);
20
+ }
21
+ /**
22
+ * A visible one-line prompt on a TTY (account picker, confirmations): the
23
+ * trimmed answer, or null when the input ended (Ctrl-D) before one.
24
+ */
25
+ export async function promptLine(io, label) {
26
+ if (!isTty(io.stdin)) {
27
+ throw new LocalCheckupError('CONFIRMATION_REQUIRED', 'No terminal is available for this prompt.', { hint: 'Run the command in a terminal, or pass the answer as a flag (--account-id, --yes).' });
28
+ }
29
+ if (io.stdin.readableEnded)
30
+ return null;
31
+ const rl = createInterface({ input: io.stdin, output: io.stderr, terminal: false });
32
+ // readline's question() never settles once the input ends; the interface's 'close' does.
33
+ const ended = new Promise((resolve) => rl.once('close', () => resolve(null)));
34
+ try {
35
+ const answer = await Promise.race([rl.question(label).catch(() => null), ended]);
36
+ if (answer !== null)
37
+ return answer.trim();
38
+ // Ctrl-D leaves the cursor on the prompt line.
39
+ io.stderr.write('\n');
40
+ return null;
41
+ }
42
+ finally {
43
+ rl.close();
44
+ }
45
+ }
46
+ /**
47
+ * One line of piped input, without consuming the next one: whatever follows
48
+ * the newline is pushed back onto the stream for the next prompt. Bytes past
49
+ * `MAX_ANSWER_BYTES` are dropped up to the newline, so an overlong line comes
50
+ * back too long and fails validation. Null at the end of the input.
51
+ */
52
+ export function readPipedLine(stdin) {
53
+ const stream = stdin;
54
+ if (stream.readableEnded)
55
+ return Promise.resolve(null);
56
+ return new Promise((resolve, reject) => {
57
+ let kept = Buffer.alloc(0);
58
+ let settled = false;
59
+ const finish = (error, line, rest) => {
60
+ if (settled)
61
+ return;
62
+ settled = true;
63
+ stream.removeListener('readable', onReadable);
64
+ stream.removeListener('end', onEnd);
65
+ stream.removeListener('error', onError);
66
+ if (rest && rest.byteLength > 0)
67
+ stream.unshift(rest);
68
+ if (error)
69
+ reject(error);
70
+ else
71
+ resolve(line === null ? null : line.toString('utf8').replace(/\r$/, ''));
72
+ };
73
+ const onReadable = () => {
74
+ let chunk;
75
+ while (!settled && (chunk = stream.read()) !== null) {
76
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
77
+ const newline = bytes.indexOf(0x0a);
78
+ const head = newline >= 0 ? bytes.subarray(0, newline) : bytes;
79
+ if (kept.byteLength <= MAX_ANSWER_BYTES)
80
+ kept = Buffer.concat([kept, head.subarray(0, MAX_ANSWER_BYTES + 1 - kept.byteLength)]);
81
+ if (newline >= 0)
82
+ finish(null, kept, bytes.subarray(newline + 1));
83
+ }
84
+ };
85
+ const onEnd = () => finish(null, kept.byteLength > 0 ? kept : null);
86
+ const onError = (error) => finish(error, null);
87
+ stream.on('readable', onReadable);
88
+ stream.on('end', onEnd);
89
+ stream.on('error', onError);
90
+ onReadable();
91
+ });
92
+ }
93
+ /**
94
+ * A visible prompt for the email verification. On a terminal it is
95
+ * `promptLine`; without one the label still goes to stderr and the answer is
96
+ * one line of piped stdin. Null when the input ended without an answer.
97
+ */
98
+ export async function promptAnswer(io, label) {
99
+ if (isTty(io.stdin))
100
+ return promptLine(io, label);
101
+ io.stderr.write(label);
102
+ const line = io.stdin ? await readPipedLine(io.stdin) : null;
103
+ // A piped answer is not echoed by a terminal; end the prompt line.
104
+ io.stderr.write('\n');
105
+ return line === null ? null : line.trim();
106
+ }
107
+ /**
108
+ * `[y/N]` upload confirmation: an explicit `y` is required. `--yes` (or its
109
+ * alias `--confirm`) answers yes; without a terminal and without them it is
110
+ * CONFIRMATION_REQUIRED.
111
+ */
112
+ export async function confirmUpload(io, question, options) {
113
+ if (options.isConfirmed) {
114
+ io.stderr.write(`${question} [y/N] y (${options.flag ?? '--yes'})\n`);
115
+ return true;
116
+ }
117
+ if (!isTty(io.stdin)) {
118
+ throw new LocalCheckupError('CONFIRMATION_REQUIRED', 'Uploading needs confirmation, and no terminal is available to ask.');
119
+ }
120
+ const answer = await promptLine(io, `${question} [y/N] `);
121
+ if (answer === null)
122
+ throw inputEnded('Uploading needs confirmation, and the input ended before an answer.');
123
+ return /^y(?:es)?$/i.test(answer);
124
+ }
125
+ /**
126
+ * `[Y/n]` question where Enter means yes (installing cf). The caller handles
127
+ * `--yes` and the no-terminal case; an input that ends before an answer is
128
+ * CONFIRMATION_REQUIRED.
129
+ */
130
+ export async function confirmDefaultYes(io, question) {
131
+ const answer = await promptLine(io, `${question} [Y/n] `);
132
+ if (answer === null)
133
+ throw inputEnded('The question needs an answer, and the input ended before one.');
134
+ return answer === '' || /^y(?:es)?$/i.test(answer);
135
+ }
136
+ /** Ctrl-D at a confirmation: nothing was confirmed. */
137
+ function inputEnded(message) {
138
+ return new LocalCheckupError('CONFIRMATION_REQUIRED', message, { hint: 'Run the same command again and answer the question, or add --yes.' });
139
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Cloudflare request budget (spec §F5).
3
+ *
4
+ * Cloudflare allows 1,200 API requests per user per rolling 5 minutes, shared
5
+ * with the user's dashboard; going over blocks every API call for 5 minutes.
6
+ * The default spends at most 900 per rolling 5 minutes (configurable 100 to
7
+ * 1,100). A token bucket with capacity `burst` that refills at
8
+ * `(limit − burst) / 300 s` never admits more than `limit` calls in any
9
+ * 300-second window: at most `burst` from the full bucket plus the refill.
10
+ * Local `--version` and `--dry-run` calls are free.
11
+ */
12
+ export declare const DEFAULT_REQUESTS_PER_5MIN = 900;
13
+ export declare const MIN_REQUESTS_PER_5MIN = 100;
14
+ export declare const MAX_REQUESTS_PER_5MIN = 1100;
15
+ export declare const BUDGET_BURST = 8;
16
+ export declare const BUDGET_WINDOW_MS = 300000;
17
+ export type Clock = {
18
+ now(): number;
19
+ sleep(ms: number): Promise<void>;
20
+ };
21
+ export declare const systemClock: Clock;
22
+ export declare function parseRequestBudget(raw: string | undefined): number;
23
+ export declare class RequestBudget {
24
+ readonly limit: number;
25
+ private readonly clock;
26
+ readonly burst: number;
27
+ private tokens;
28
+ private updatedAt;
29
+ private readonly refillPerMs;
30
+ private queue;
31
+ constructor(limit?: number, clock?: Clock, burst?: number);
32
+ private refill;
33
+ /** Tokens available now (fractional). */
34
+ available(): number;
35
+ /** Wait for one request slot. Callers are served in order. */
36
+ acquire(): Promise<void>;
37
+ /** Seconds the bucket needs for `requests` calls, ignoring network latency. */
38
+ estimateSeconds(requests: number): number;
39
+ }
40
+ /** Seconds a full bucket needs to admit `requests` calls at `limit` per 5 minutes. */
41
+ export declare function estimateBudgetSeconds(limit: number, requests: number, burst?: number): number;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Cloudflare request budget (spec §F5).
3
+ *
4
+ * Cloudflare allows 1,200 API requests per user per rolling 5 minutes, shared
5
+ * with the user's dashboard; going over blocks every API call for 5 minutes.
6
+ * The default spends at most 900 per rolling 5 minutes (configurable 100 to
7
+ * 1,100). A token bucket with capacity `burst` that refills at
8
+ * `(limit − burst) / 300 s` never admits more than `limit` calls in any
9
+ * 300-second window: at most `burst` from the full bucket plus the refill.
10
+ * Local `--version` and `--dry-run` calls are free.
11
+ */
12
+ export const DEFAULT_REQUESTS_PER_5MIN = 900;
13
+ export const MIN_REQUESTS_PER_5MIN = 100;
14
+ export const MAX_REQUESTS_PER_5MIN = 1_100;
15
+ export const BUDGET_BURST = 8;
16
+ export const BUDGET_WINDOW_MS = 300_000;
17
+ export const systemClock = {
18
+ now: () => Date.now(),
19
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
20
+ };
21
+ export function parseRequestBudget(raw) {
22
+ if (raw === undefined)
23
+ return DEFAULT_REQUESTS_PER_5MIN;
24
+ const value = Number(raw);
25
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < MIN_REQUESTS_PER_5MIN || value > MAX_REQUESTS_PER_5MIN) {
26
+ throw new Error(`--max-requests-per-5min must be an integer between ${MIN_REQUESTS_PER_5MIN} and ${MAX_REQUESTS_PER_5MIN}.`);
27
+ }
28
+ return value;
29
+ }
30
+ export class RequestBudget {
31
+ limit;
32
+ clock;
33
+ burst;
34
+ tokens;
35
+ updatedAt;
36
+ refillPerMs;
37
+ queue = Promise.resolve();
38
+ constructor(limit = DEFAULT_REQUESTS_PER_5MIN, clock = systemClock, burst = BUDGET_BURST) {
39
+ this.limit = limit;
40
+ this.clock = clock;
41
+ this.burst = burst;
42
+ if (!(limit > burst))
43
+ throw new Error('The request budget must exceed the burst size.');
44
+ this.tokens = burst;
45
+ this.updatedAt = clock.now();
46
+ this.refillPerMs = (limit - burst) / BUDGET_WINDOW_MS;
47
+ }
48
+ refill() {
49
+ const now = this.clock.now();
50
+ this.tokens = Math.min(this.burst, this.tokens + (now - this.updatedAt) * this.refillPerMs);
51
+ this.updatedAt = now;
52
+ }
53
+ /** Tokens available now (fractional). */
54
+ available() {
55
+ this.refill();
56
+ return this.tokens;
57
+ }
58
+ /** Wait for one request slot. Callers are served in order. */
59
+ acquire() {
60
+ const next = this.queue.then(async () => {
61
+ this.refill();
62
+ while (this.tokens < 1) {
63
+ await this.clock.sleep(Math.max(1, Math.ceil((1 - this.tokens) / this.refillPerMs)));
64
+ this.refill();
65
+ }
66
+ this.tokens -= 1;
67
+ });
68
+ this.queue = next.catch(() => undefined);
69
+ return next;
70
+ }
71
+ /** Seconds the bucket needs for `requests` calls, ignoring network latency. */
72
+ estimateSeconds(requests) {
73
+ return estimateBudgetSeconds(this.limit, requests, this.burst);
74
+ }
75
+ }
76
+ /** Seconds a full bucket needs to admit `requests` calls at `limit` per 5 minutes. */
77
+ export function estimateBudgetSeconds(limit, requests, burst = BUDGET_BURST) {
78
+ return Math.max(0, ((requests - burst) * BUDGET_WINDOW_MS) / (limit - burst) / 1000);
79
+ }
@@ -0,0 +1,52 @@
1
+ import { type CloudflareCheckupSnapshotV1, type SnapshotEntry } from './contract.js';
2
+ import { LocalCheckupError } from './errors.js';
3
+ export declare function sha256Hex(bytes: Uint8Array | string): string;
4
+ /** Write the snapshot; refuses an existing file unless `overwrite` (atomic rename). */
5
+ export declare function writeSnapshotFile(path: string, text: string, options: {
6
+ overwrite: boolean;
7
+ }): Promise<{
8
+ bytes: number;
9
+ sha256: string;
10
+ }>;
11
+ export type LoadedSnapshot = {
12
+ path: string;
13
+ bytes: Buffer;
14
+ sha256: string;
15
+ snapshot: CloudflareCheckupSnapshotV1;
16
+ };
17
+ /** Map a failed contract parse to the CLI error (no values, issue codes only). */
18
+ export declare function snapshotParseError(issues: ReadonlyArray<{
19
+ code?: string;
20
+ message?: string;
21
+ path?: ReadonlyArray<PropertyKey>;
22
+ params?: unknown;
23
+ }>): LocalCheckupError;
24
+ /** Read, hash and validate a snapshot file against the vendored contract. */
25
+ export declare function loadSnapshotFile(path: string): Promise<LoadedSnapshot>;
26
+ export type EntryCounts = {
27
+ total: number;
28
+ ok: number;
29
+ httpError: number;
30
+ notCollected: number;
31
+ collectionFailed: number;
32
+ };
33
+ export declare function countEntries(entries: readonly SnapshotEntry[]): EntryCounts;
34
+ export type EntryKindCounts = Record<string, {
35
+ total: number;
36
+ ok: number;
37
+ httpError: number;
38
+ notCollected: number;
39
+ collectionFailed: number;
40
+ }>;
41
+ export declare function countEntriesByKind(entries: readonly SnapshotEntry[]): EntryKindCounts;
42
+ /** Counts of every free-text or identifying category the snapshot carries (`inspect`). */
43
+ export type DataCategories = {
44
+ memberEmails: number;
45
+ accessRuleValues: number;
46
+ dnsHostnames: number;
47
+ pseudonymizedIps: number;
48
+ ruleDescriptions: number;
49
+ };
50
+ export declare function dataCategories(snapshot: CloudflareCheckupSnapshotV1): DataCategories;
51
+ export declare function zoneCount(snapshot: CloudflareCheckupSnapshotV1): number;
52
+ export declare function accountName(snapshot: CloudflareCheckupSnapshotV1): string | null;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Snapshot files (spec §F7).
3
+ *
4
+ * Both modes write the bytes to a `wx` 0600 temp file in the same directory
5
+ * first, so `--out` never holds a partial snapshot. The default mode publishes
6
+ * it with `link(2)`, which never replaces an existing file (deviation from
7
+ * §F7's direct `open(path, 'wx')`; on filesystems without hard links it falls
8
+ * back to that, removing the file if the write fails). `--overwrite` renames
9
+ * the temp file over the target. `submit` uploads the exact bytes of the file
10
+ * the user inspected.
11
+ */
12
+ import { createHash, randomBytes } from 'node:crypto';
13
+ import { link, open, readFile, rename, rm, stat } from 'node:fs/promises';
14
+ import { basename, dirname, join } from 'node:path';
15
+ import { CloudflareCheckupSnapshotV1Schema, SNAPSHOT_LIMITS, SNAPSHOT_SCHEMA_VERSION, expandRuleRuns, snapshotIssueList, snapshotRejectReason, } from './contract.js';
16
+ import { LocalCheckupError } from './errors.js';
17
+ export function sha256Hex(bytes) {
18
+ return createHash('sha256').update(bytes).digest('hex');
19
+ }
20
+ /** `link(2)` errors from filesystems without hard links (FAT/exFAT, some SMB mounts). */
21
+ const NO_HARD_LINK_CODES = new Set(['EPERM', 'ENOTSUP', 'EOPNOTSUPP', 'ENOSYS']);
22
+ /** Create `path` exclusively (0600), write and fsync; a partially written file is removed. */
23
+ async function writeExclusive(path, bytes) {
24
+ // no-dd-sa:javascript-node-security/detect-non-literal-fs-filename — the user's --out path; created exclusively, never followed.
25
+ const handle = await open(path, 'wx', 0o600);
26
+ try {
27
+ try {
28
+ await handle.writeFile(bytes);
29
+ await handle.sync();
30
+ }
31
+ finally {
32
+ await handle.close();
33
+ }
34
+ }
35
+ catch (error) {
36
+ // This process created the file (`wx`), so removing it cannot touch anyone else's data.
37
+ await rm(path, { force: true }).catch(() => undefined);
38
+ throw error;
39
+ }
40
+ }
41
+ /** Give the complete temp file its final name without ever replacing an existing file. */
42
+ async function publishExclusive(temp, path, bytes) {
43
+ try {
44
+ await link(temp, path);
45
+ }
46
+ catch (error) {
47
+ if (!NO_HARD_LINK_CODES.has(error.code ?? ''))
48
+ throw error;
49
+ await writeExclusive(path, bytes);
50
+ }
51
+ }
52
+ /** Write the snapshot; refuses an existing file unless `overwrite` (atomic rename). */
53
+ export async function writeSnapshotFile(path, text, options) {
54
+ const bytes = Buffer.from(text, 'utf8');
55
+ if (options.overwrite) {
56
+ const existing = await stat(path).catch(() => null);
57
+ if (existing && !existing.isFile())
58
+ throw new Error('--out must name a regular file.');
59
+ }
60
+ const temp = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString('hex')}.tmp`);
61
+ try {
62
+ await writeExclusive(temp, bytes);
63
+ if (options.overwrite)
64
+ await rename(temp, path);
65
+ else
66
+ await publishExclusive(temp, path, bytes);
67
+ }
68
+ catch (error) {
69
+ const code = error.code;
70
+ if (!options.overwrite && code === 'EEXIST') {
71
+ throw new LocalCheckupError('OUTPUT_EXISTS', 'The --out file already exists; it was not modified.');
72
+ }
73
+ throw new Error(`Could not write the snapshot file (${code ?? 'error'}).`);
74
+ }
75
+ finally {
76
+ // A no-op after rename; drops the temp name after link.
77
+ await rm(temp, { force: true }).catch(() => undefined);
78
+ }
79
+ return { bytes: bytes.byteLength, sha256: sha256Hex(bytes) };
80
+ }
81
+ /** Map a failed contract parse to the CLI error (no values, issue codes only). */
82
+ export function snapshotParseError(issues) {
83
+ const reason = snapshotRejectReason(issues);
84
+ const codes = [...new Set(snapshotIssueList(issues).map((issue) => issue.code))].slice(0, 5).join(', ');
85
+ if (reason === 'secret_like_value') {
86
+ return new LocalCheckupError('SNAPSHOT_SECRET_LIKE_VALUE', 'The snapshot contains a value that looks like a credential, so it will not be written or uploaded.');
87
+ }
88
+ return new LocalCheckupError('SNAPSHOT_INVALID', `The snapshot does not match ${SNAPSHOT_SCHEMA_VERSION} (${codes || 'invalid'}).`);
89
+ }
90
+ /** Read, hash and validate a snapshot file against the vendored contract. */
91
+ export async function loadSnapshotFile(path) {
92
+ const info = await stat(path).catch(() => null);
93
+ if (!info || !info.isFile())
94
+ throw new LocalCheckupError('SNAPSHOT_INVALID', 'The snapshot file does not exist or is not a regular file.');
95
+ if (info.size > SNAPSHOT_LIMITS.maxBytes) {
96
+ throw new LocalCheckupError('SNAPSHOT_TOO_LARGE', `The snapshot is larger than the ${SNAPSHOT_LIMITS.maxBytes / 1024 / 1024} MiB contract limit.`);
97
+ }
98
+ // no-dd-sa:javascript-node-security/detect-non-literal-fs-filename — the user's own snapshot path.
99
+ const bytes = await readFile(path);
100
+ if (bytes.byteLength > SNAPSHOT_LIMITS.maxBytes)
101
+ throw new LocalCheckupError('SNAPSHOT_TOO_LARGE', 'The snapshot is larger than the contract limit.');
102
+ let raw;
103
+ try {
104
+ raw = JSON.parse(bytes.toString('utf8'));
105
+ }
106
+ catch {
107
+ throw new LocalCheckupError('SNAPSHOT_INVALID', 'The snapshot file is not valid JSON.');
108
+ }
109
+ const parsed = CloudflareCheckupSnapshotV1Schema.safeParse(raw);
110
+ if (!parsed.success)
111
+ throw snapshotParseError(parsed.error.issues);
112
+ return { path, bytes, sha256: sha256Hex(bytes), snapshot: parsed.data };
113
+ }
114
+ export function countEntries(entries) {
115
+ const counts = { total: entries.length, ok: 0, httpError: 0, notCollected: 0, collectionFailed: 0 };
116
+ for (const entry of entries) {
117
+ const result = entry.result;
118
+ if (result.ok)
119
+ counts.ok++;
120
+ else if (result.reason === 'http_error')
121
+ counts.httpError++;
122
+ else if (result.reason === 'not_collected')
123
+ counts.notCollected++;
124
+ else
125
+ counts.collectionFailed++;
126
+ }
127
+ return counts;
128
+ }
129
+ export function countEntriesByKind(entries) {
130
+ const byKind = {};
131
+ for (const entry of entries) {
132
+ byKind[entry.kind] ??= { total: 0, ok: 0, httpError: 0, notCollected: 0, collectionFailed: 0 };
133
+ const counts = byKind[entry.kind];
134
+ const result = entry.result;
135
+ counts.total++;
136
+ if (result.ok)
137
+ counts.ok++;
138
+ else if (result.reason === 'http_error')
139
+ counts.httpError++;
140
+ else if (result.reason === 'not_collected')
141
+ counts.notCollected++;
142
+ else
143
+ counts.collectionFailed++;
144
+ }
145
+ return byKind;
146
+ }
147
+ export function dataCategories(snapshot) {
148
+ const categories = { memberEmails: 0, accessRuleValues: 0, dnsHostnames: 0, pseudonymizedIps: 0, ruleDescriptions: 0 };
149
+ const ips = new Set();
150
+ for (const entry of snapshot.entries) {
151
+ if (!entry.result.ok)
152
+ continue;
153
+ switch (entry.kind) {
154
+ case 'account_members':
155
+ categories.memberEmails += entry.result.data.filter((member) => member.email !== undefined).length;
156
+ break;
157
+ case 'account_access_rules':
158
+ case 'zone_access_rules':
159
+ categories.accessRuleValues += entry.result.data.filter((rule) => rule.configuration?.value !== undefined).length;
160
+ break;
161
+ case 'zone_dns_records':
162
+ for (const record of entry.result.data) {
163
+ if (record.name !== undefined)
164
+ categories.dnsHostnames++;
165
+ ips.add(record.content);
166
+ }
167
+ break;
168
+ default:
169
+ break;
170
+ }
171
+ }
172
+ categories.pseudonymizedIps = ips.size;
173
+ for (const runs of Object.values(snapshot.ruleBodies)) {
174
+ categories.ruleDescriptions += expandRuleRuns(runs).filter((rule) => rule.description !== undefined || rule.ref !== undefined).length;
175
+ }
176
+ return categories;
177
+ }
178
+ export function zoneCount(snapshot) {
179
+ const zones = snapshot.entries.find((entry) => entry.kind === 'zones');
180
+ return zones && zones.kind === 'zones' ? zones.result.data.length : 0;
181
+ }
182
+ export function accountName(snapshot) {
183
+ const account = snapshot.entries.find((entry) => entry.kind === 'account');
184
+ return account && account.kind === 'account' && account.result.ok ? account.result.data.name : null;
185
+ }
@@ -0,0 +1,112 @@
1
+ import { type LocalAvailabilityResponse, type LocalRequestBody, type LocalRequestResponse, type LocalSessionResponse, type LocalVerifyBody, type LocalVerifyResponse } from './contract.js';
2
+ /**
3
+ * `GET local-session`. The account is chosen in the CLI and bound by the
4
+ * uploaded snapshot, so `accountId` is `null` until then; runs created before
5
+ * that change still carry their bound account.
6
+ */
7
+ export type LocalSession = LocalSessionResponse['session'];
8
+ /** `GET local-session/availability`: local checkups are on, with the oldest CLI and the snapshots vigilbase.com accepts. */
9
+ export type LocalAvailability = LocalAvailabilityResponse;
10
+ /** `POST local-session/request`: the token naming the run that awaits its emailed code. */
11
+ export type PendingVerification = LocalRequestResponse['verification'];
12
+ /** `POST local-session/verify`: the session code for the upload (kept in memory only). */
13
+ export type VerifiedSession = LocalVerifyResponse['session'];
14
+ export declare const GET_TIMEOUT_MS = 30000;
15
+ /** The availability check is skipped (the run goes on) when vigilbase.com does not answer it within this. */
16
+ export declare const AVAILABILITY_TIMEOUT_MS = 10000;
17
+ export declare const POST_TIMEOUT_MS = 120000;
18
+ export declare const MAX_SUBMIT_RESPONSE_BYTES: number;
19
+ export declare const POST_RETRY_DELAYS_MS: readonly [2000, 5000];
20
+ /** A verify that failed on the network or with a 503 is sent once more, after this pause. */
21
+ export declare const VERIFY_RETRY_DELAYS_MS: readonly [2000];
22
+ /** True for http(s) loopback origins (local development), the only ones besides production that `--base-url` accepts. */
23
+ export declare function isLoopbackOrigin(origin: string): boolean;
24
+ /**
25
+ * The submit origin. Only `https://vigilbase.com` and loopback origins are
26
+ * accepted; there is no `*.vigilbase.com` wildcard (other subdomains are
27
+ * different services). Environment variables are never consulted.
28
+ */
29
+ export declare function resolveSubmitOrigin(explicit: string | undefined): string;
30
+ export type LocalClientOptions = {
31
+ origin: string;
32
+ cliVersion: string;
33
+ userAgent: string;
34
+ /** Overrides every request timeout (`--timeout`). */
35
+ timeoutMs?: number;
36
+ sleep?: (ms: number) => Promise<void>;
37
+ /** Injectable for tests; the global `fetch` (looked up per request) otherwise. */
38
+ fetch?: typeof fetch;
39
+ };
40
+ export type SubmitClientOptions = LocalClientOptions & {
41
+ code: string;
42
+ };
43
+ /**
44
+ * The results page from an upload response, as printable ASCII (`URL.href`
45
+ * percent-encodes everything else), or undefined when it is not a plain
46
+ * http(s) URL. The caller decides whether to open it (same origin only).
47
+ */
48
+ export declare function trustedResultsUrl(raw: string | undefined): string | undefined;
49
+ /** Shared transport: CLI headers, no redirects, bounded responses, CLI-authored network errors. */
50
+ declare abstract class LocalHttpClient {
51
+ protected readonly options: LocalClientOptions;
52
+ constructor(options: LocalClientOptions);
53
+ protected sleep(ms: number): Promise<void>;
54
+ protected headers(extra?: Record<string, string>): Record<string, string>;
55
+ protected request(path: string, init: RequestInit, timeoutMs: number): Promise<{
56
+ response: Response;
57
+ text: string;
58
+ }>;
59
+ }
60
+ /**
61
+ * The requests before the session code, without a bearer: the availability
62
+ * check and the email verification. A code request is never retried: a
63
+ * repeated request would send a second email. A verify that failed on the
64
+ * network (or timed out) or with a 503 is sent once more, after 2 s, with the
65
+ * same token and code: One counts only wrong codes, and the right code
66
+ * verified again mints the next session code, as a renewal does. Only when
67
+ * the lost answer was to a wrong code does the retry cost a second try.
68
+ */
69
+ export declare class VerificationClient extends LocalHttpClient {
70
+ private postJson;
71
+ /**
72
+ * `GET local-session/availability`, before the email is asked for: what
73
+ * vigilbase.com accepts, or null when it gave no usable answer (a 404 from a
74
+ * server without the route, a network error, a timeout, a challenge or any
75
+ * other response), and the run goes on as before. Throws LOCAL_MODE_DISABLED
76
+ * when it answers that local checkups are switched off. Never retried.
77
+ */
78
+ availability(): Promise<LocalAvailability | null>;
79
+ /** `POST local-session/request`: vigilbase.com emails a 6-digit code and returns the token for it. */
80
+ requestCode(body: LocalRequestBody): Promise<PendingVerification>;
81
+ /** `POST local-session/verify`: trades the token and the emailed code for a session code (the next one on every call). */
82
+ verifyCode(body: LocalVerifyBody): Promise<VerifiedSession>;
83
+ }
84
+ export type UploadResult = {
85
+ replayed: boolean;
86
+ snapshotSha256: string;
87
+ provenance: Record<string, unknown>;
88
+ report: Record<string, unknown>;
89
+ /** The results page, when vigilbase.com returned one (see `trustedResultsUrl`). */
90
+ resultsUrl?: string;
91
+ };
92
+ export declare class SubmitClient extends LocalHttpClient {
93
+ private readonly code;
94
+ constructor(options: SubmitClientOptions);
95
+ private bearer;
96
+ /** `GET local-session`: the bound account (or null), masked requester, expiry and accepted limits. */
97
+ getSession(): Promise<LocalSession>;
98
+ /**
99
+ * `POST local-session/snapshot` with the exact file bytes. 503 and network
100
+ * errors are retried twice (after 2 s, then 5 s) with the same bytes, which
101
+ * the server treats idempotently. Challenges are never retried.
102
+ */
103
+ postSnapshot(bytes: Buffer, sha256: string): Promise<UploadResult>;
104
+ }
105
+ /** Refuse a server (its session, or its availability answer) that requires a newer CLI, or does not accept this schema version. */
106
+ export declare function assertServerAccepts(server: Pick<LocalSession, 'accepts' | 'minCliVersion'>, cliVersion: string, schemaVersion: string): void;
107
+ /**
108
+ * The masked address as the server masks it (first character, `***`, the
109
+ * domain); any other text from the server is replaced by `fallback`.
110
+ */
111
+ export declare function displayRequester(requester: string, fallback?: string): string;
112
+ export {};