@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,395 @@
1
+ import { NetworkError, readBoundedBody } from '../../lib/network.js';
2
+ import { CLI_VERSION_HEADER, LOCAL_AVAILABILITY_PATH, LOCAL_REQUEST_PATH, LOCAL_SESSION_PATH, LOCAL_SNAPSHOT_PATH, LOCAL_VERIFY_PATH, LocalAvailabilityResponseSchema, LocalErrorResponseSchema, LocalRequestBodySchema, LocalRequestResponseSchema, LocalSessionResponseSchema, LocalSnapshotResponseSchema, LocalVerifyBodySchema, LocalVerifyResponseSchema, PRODUCTION_ORIGIN, SESSION_CODE_REGEX, SNAPSHOT_SHA256_HEADER, compareSemver, } from './contract.js';
3
+ import { LocalCheckupError, browserCheckupHint } from './errors.js';
4
+ import { formatDuration } from './output.js';
5
+ export const GET_TIMEOUT_MS = 30_000;
6
+ /** The availability check is skipped (the run goes on) when vigilbase.com does not answer it within this. */
7
+ export const AVAILABILITY_TIMEOUT_MS = 10_000;
8
+ export const POST_TIMEOUT_MS = 120_000;
9
+ export const MAX_SUBMIT_RESPONSE_BYTES = 1024 * 1024;
10
+ export const POST_RETRY_DELAYS_MS = [2_000, 5_000];
11
+ /** A verify that failed on the network or with a 503 is sent once more, after this pause. */
12
+ export const VERIFY_RETRY_DELAYS_MS = [2_000];
13
+ function isLoopbackHost(hostname) {
14
+ return hostname === 'localhost' || hostname === '[::1]' || /^127\.(?:\d{1,3}\.){2}\d{1,3}$/.test(hostname);
15
+ }
16
+ /** True for http(s) loopback origins (local development), the only ones besides production that `--base-url` accepts. */
17
+ export function isLoopbackOrigin(origin) {
18
+ let url;
19
+ try {
20
+ url = new URL(origin);
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ return (url.protocol === 'http:' || url.protocol === 'https:') && isLoopbackHost(url.hostname);
26
+ }
27
+ /**
28
+ * The submit origin. Only `https://vigilbase.com` and loopback origins are
29
+ * accepted; there is no `*.vigilbase.com` wildcard (other subdomains are
30
+ * different services). Environment variables are never consulted.
31
+ */
32
+ export function resolveSubmitOrigin(explicit) {
33
+ if (explicit === undefined)
34
+ return PRODUCTION_ORIGIN;
35
+ let url;
36
+ try {
37
+ url = new URL(explicit.trim());
38
+ }
39
+ catch {
40
+ throw new LocalCheckupError('SUBMIT_ORIGIN_UNTRUSTED', '--base-url is not a valid origin.');
41
+ }
42
+ const originOnly = !url.username && !url.password && /^\/*$/.test(url.pathname) && !url.search && !url.hash;
43
+ if (originOnly && url.origin === PRODUCTION_ORIGIN)
44
+ return PRODUCTION_ORIGIN;
45
+ if (originOnly && isLoopbackOrigin(url.origin))
46
+ return url.origin;
47
+ // Do not echo the value: it may contain credentials.
48
+ throw new LocalCheckupError('SUBMIT_ORIGIN_UNTRUSTED', 'Local checkups submit only to https://vigilbase.com or a loopback origin.');
49
+ }
50
+ const REASON_CODES = {
51
+ session_invalid: 'SESSION_INVALID',
52
+ session_expired: 'SESSION_EXPIRED',
53
+ session_used: 'SESSION_USED',
54
+ session_busy: 'SESSION_BUSY',
55
+ session_not_ready: 'SESSION_INVALID',
56
+ run_closed: 'SESSION_CLOSED',
57
+ browser_origin: 'SUBMIT_FAILED',
58
+ malformed_json: 'SUBMIT_FAILED',
59
+ digest_mismatch: 'SUBMIT_FAILED',
60
+ unsupported_media_type: 'SUBMIT_FAILED',
61
+ length_required: 'SUBMIT_FAILED',
62
+ snapshot_too_large: 'SNAPSHOT_TOO_LARGE',
63
+ snapshot_invalid: 'SNAPSHOT_INVALID',
64
+ snapshot_incomplete: 'SNAPSHOT_INVALID',
65
+ account_mismatch: 'ACCOUNT_MISMATCH',
66
+ snapshot_stale: 'SNAPSHOT_STALE',
67
+ collector_unsupported: 'CLI_UPGRADE_REQUIRED',
68
+ secret_like_value: 'SNAPSHOT_SECRET_LIKE_VALUE',
69
+ evaluation_failed: 'SUBMIT_FAILED',
70
+ rate_limited: 'VIGILBASE_RATE_LIMITED',
71
+ local_mode_disabled: 'LOCAL_MODE_DISABLED',
72
+ storage_unavailable: 'SUBMIT_FAILED',
73
+ request_invalid: 'SUBMIT_FAILED',
74
+ email_invalid: 'EMAIL_INVALID',
75
+ code_invalid: 'EMAIL_CODE_INVALID',
76
+ verification_invalid: 'VERIFICATION_INVALID',
77
+ verification_expired: 'VERIFICATION_EXPIRED',
78
+ };
79
+ const REASON_MESSAGES = {
80
+ session_invalid: 'The checkup session is not valid (revoked or for another checkup).',
81
+ session_expired: 'The checkup session has expired.',
82
+ session_used: 'This checkup already has results from a different snapshot.',
83
+ session_busy: 'A different snapshot is already being processed for this checkup.',
84
+ session_not_ready: 'This checkup is not ready for an upload yet; its email is not verified.',
85
+ run_closed: 'This checkup was cancelled or has expired.',
86
+ browser_origin: 'vigilbase.com refused the request as a browser request.',
87
+ malformed_json: 'vigilbase.com could not read the uploaded file as JSON.',
88
+ digest_mismatch: 'The upload was corrupted in transit (SHA-256 mismatch).',
89
+ unsupported_media_type: 'vigilbase.com rejected the upload content type.',
90
+ length_required: 'vigilbase.com required a Content-Length for the upload.',
91
+ snapshot_too_large: 'The snapshot is larger than vigilbase.com accepts.',
92
+ snapshot_invalid: 'vigilbase.com rejected the snapshot as invalid.',
93
+ snapshot_incomplete: 'vigilbase.com rejected the snapshot as incomplete (missing or extra reads).',
94
+ account_mismatch: 'The snapshot is for a different Cloudflare account than this checkup.',
95
+ snapshot_stale: 'The snapshot is older than 24 hours (or dated in the future).',
96
+ collector_unsupported: 'vigilbase.com no longer accepts snapshots from this CLI or cf version.',
97
+ secret_like_value: 'vigilbase.com found a value that looks like a credential in the snapshot.',
98
+ evaluation_failed: 'vigilbase.com could not evaluate the snapshot; the checkup is unchanged.',
99
+ rate_limited: 'vigilbase.com rate-limited the upload.',
100
+ local_mode_disabled: 'Local checkups aren\'t available right now.',
101
+ storage_unavailable: 'vigilbase.com storage is temporarily unavailable.',
102
+ request_invalid: 'vigilbase.com could not read the verification request.',
103
+ email_invalid: 'vigilbase.com accepts work email addresses only (not personal or disposable ones).',
104
+ code_invalid: 'That code is not correct.',
105
+ verification_invalid: 'The email verification is not valid.',
106
+ verification_expired: 'The email verification has expired (the code timed out, was tried too often, or the checkup closed).',
107
+ };
108
+ const RATE_LIMITED_MESSAGES = {
109
+ verification: 'vigilbase.com rate-limited verification codes for now.',
110
+ session: REASON_MESSAGES.rate_limited,
111
+ };
112
+ function retryAfter(response, body) {
113
+ if (body?.retryAfterSec !== undefined)
114
+ return body.retryAfterSec;
115
+ const header = response.headers.get('retry-after');
116
+ if (header && /^\d{1,6}$/.test(header.trim()))
117
+ return Number(header.trim());
118
+ return undefined;
119
+ }
120
+ function isChallenge(response) {
121
+ if ((response.headers.get('cf-mitigated') ?? '').toLowerCase().includes('challenge'))
122
+ return true;
123
+ const type = (response.headers.get('content-type') ?? '').toLowerCase();
124
+ return (response.status === 403 || response.status === 503) && type.includes('text/html');
125
+ }
126
+ /** Map a non-2xx response from `origin` to a CLI error; never echoes server text. */
127
+ function responseError(response, text, endpoint, origin) {
128
+ if (isChallenge(response)) {
129
+ return new LocalCheckupError('VIGILBASE_CHALLENGED', 'vigilbase.com answered with a bot or WAF challenge instead of the API.', { status: response.status });
130
+ }
131
+ let body = null;
132
+ try {
133
+ const parsed = LocalErrorResponseSchema.safeParse(JSON.parse(text));
134
+ if (parsed.success)
135
+ body = parsed.data;
136
+ }
137
+ catch {
138
+ body = null;
139
+ }
140
+ const status = response.status;
141
+ const after = retryAfter(response, body);
142
+ const timing = after !== undefined ? { retryAfterSec: after } : {};
143
+ const reason = body?.reason;
144
+ if (status === 429 || reason === 'rate_limited') {
145
+ const wait = after !== undefined ? ` Try again in ${formatDuration(after)}.` : '';
146
+ return new LocalCheckupError('VIGILBASE_RATE_LIMITED', `${RATE_LIMITED_MESSAGES[endpoint]}${wait}`, { status, ...timing });
147
+ }
148
+ if (reason) {
149
+ // Local mode switched off: the browser checkup on the same origin (vigilbase.com, or a loopback dev server).
150
+ const hint = reason === 'local_mode_disabled' ? { hint: browserCheckupHint(origin) } : {};
151
+ return new LocalCheckupError(REASON_CODES[reason], REASON_MESSAGES[reason], { status, ...timing, ...hint });
152
+ }
153
+ const fallback = status === 401 ? (endpoint === 'verification' ? 'VERIFICATION_INVALID' : 'SESSION_INVALID')
154
+ : status === 410 ? (endpoint === 'verification' ? 'VERIFICATION_EXPIRED' : 'SESSION_CLOSED')
155
+ : status === 413 ? 'SNAPSHOT_TOO_LARGE' : 'SUBMIT_FAILED';
156
+ return new LocalCheckupError(fallback, `vigilbase.com rejected the request (HTTP ${status}).`, { status });
157
+ }
158
+ /** No answer (a network error or a timeout), or vigilbase.com storage briefly unavailable (503); never a challenge or the kill switch. */
159
+ function isTransientFailure(error) {
160
+ return error instanceof LocalCheckupError && error.code === 'SUBMIT_FAILED' && (error.status === 0 || error.status === 503);
161
+ }
162
+ /** A successful JSON body validated against `schema`, or SUBMIT_FAILED naming what was expected. */
163
+ function parseSuccess(schema, response, text, what) {
164
+ let parsed;
165
+ try {
166
+ parsed = schema.safeParse(JSON.parse(text));
167
+ }
168
+ catch {
169
+ parsed = null;
170
+ }
171
+ if (!parsed?.success)
172
+ throw new LocalCheckupError('SUBMIT_FAILED', `vigilbase.com returned an unexpected ${what} response.`, { status: response.status });
173
+ return parsed.data;
174
+ }
175
+ /**
176
+ * The results page from an upload response, as printable ASCII (`URL.href`
177
+ * percent-encodes everything else), or undefined when it is not a plain
178
+ * http(s) URL. The caller decides whether to open it (same origin only).
179
+ */
180
+ export function trustedResultsUrl(raw) {
181
+ if (raw === undefined)
182
+ return undefined;
183
+ let url;
184
+ try {
185
+ url = new URL(raw);
186
+ }
187
+ catch {
188
+ return undefined;
189
+ }
190
+ if ((url.protocol !== 'https:' && url.protocol !== 'http:') || url.username || url.password)
191
+ return undefined;
192
+ return /^[\x21-\x7e]{1,2048}$/.test(url.href) ? url.href : undefined;
193
+ }
194
+ /** Shared transport: CLI headers, no redirects, bounded responses, CLI-authored network errors. */
195
+ class LocalHttpClient {
196
+ options;
197
+ constructor(options) {
198
+ this.options = options;
199
+ }
200
+ sleep(ms) {
201
+ return this.options.sleep ? this.options.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms));
202
+ }
203
+ headers(extra = {}) {
204
+ return {
205
+ Accept: 'application/json',
206
+ 'User-Agent': this.options.userAgent,
207
+ [CLI_VERSION_HEADER]: this.options.cliVersion,
208
+ ...extra,
209
+ };
210
+ }
211
+ async request(path, init, timeoutMs) {
212
+ const controller = new AbortController();
213
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
214
+ const url = `${this.options.origin}${path}`;
215
+ const send = this.options.fetch ?? fetch;
216
+ try {
217
+ // Never follow redirects: credentials and bodies must reach only the checked origin.
218
+ // 'manual' surfaces the 3xx (Node returns its real status; browsers an
219
+ // opaque redirect) so it fails fast instead of looking like a network error.
220
+ const response = await send(url, { ...init, signal: controller.signal, redirect: 'manual' });
221
+ if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400)) {
222
+ await response.body?.cancel().catch(() => undefined);
223
+ throw new LocalCheckupError('SUBMIT_FAILED', `${this.options.origin} redirected the request (HTTP ${response.status || '3xx'}); the redirect was not followed and nothing was processed.`, { status: response.status || 302 });
224
+ }
225
+ return { response, text: await readBoundedBody(response, controller.signal, MAX_SUBMIT_RESPONSE_BYTES) };
226
+ }
227
+ catch (error) {
228
+ if (error instanceof LocalCheckupError)
229
+ throw error;
230
+ if (error instanceof NetworkError && error.code === 'RESPONSE_TOO_LARGE') {
231
+ throw new LocalCheckupError('SUBMIT_FAILED', 'vigilbase.com returned an unexpectedly large response.');
232
+ }
233
+ throw new LocalCheckupError('SUBMIT_FAILED', controller.signal.aborted
234
+ ? `The request to ${this.options.origin} timed out after ${timeoutMs}ms.`
235
+ : `Could not connect to ${this.options.origin}. Check your connection, VPN or proxy.`, { status: 0 });
236
+ }
237
+ finally {
238
+ clearTimeout(timer);
239
+ }
240
+ }
241
+ }
242
+ /**
243
+ * The requests before the session code, without a bearer: the availability
244
+ * check and the email verification. A code request is never retried: a
245
+ * repeated request would send a second email. A verify that failed on the
246
+ * network (or timed out) or with a 503 is sent once more, after 2 s, with the
247
+ * same token and code: One counts only wrong codes, and the right code
248
+ * verified again mints the next session code, as a renewal does. Only when
249
+ * the lost answer was to a wrong code does the retry cost a second try.
250
+ */
251
+ export class VerificationClient extends LocalHttpClient {
252
+ async postJson(path, body) {
253
+ const result = await this.request(path, {
254
+ method: 'POST',
255
+ headers: this.headers({ 'Content-Type': 'application/json' }),
256
+ body: JSON.stringify(body),
257
+ }, this.options.timeoutMs ?? GET_TIMEOUT_MS);
258
+ if (!result.response.ok || isChallenge(result.response))
259
+ throw responseError(result.response, result.text, 'verification', this.options.origin);
260
+ return result;
261
+ }
262
+ /**
263
+ * `GET local-session/availability`, before the email is asked for: what
264
+ * vigilbase.com accepts, or null when it gave no usable answer (a 404 from a
265
+ * server without the route, a network error, a timeout, a challenge or any
266
+ * other response), and the run goes on as before. Throws LOCAL_MODE_DISABLED
267
+ * when it answers that local checkups are switched off. Never retried.
268
+ */
269
+ async availability() {
270
+ let result;
271
+ try {
272
+ result = await this.request(LOCAL_AVAILABILITY_PATH, { method: 'GET', headers: this.headers() }, this.options.timeoutMs ?? AVAILABILITY_TIMEOUT_MS);
273
+ }
274
+ catch {
275
+ return null;
276
+ }
277
+ const { response, text } = result;
278
+ if (response.ok && !isChallenge(response)) {
279
+ try {
280
+ const parsed = LocalAvailabilityResponseSchema.safeParse(JSON.parse(text));
281
+ return parsed.success ? parsed.data : null;
282
+ }
283
+ catch {
284
+ return null;
285
+ }
286
+ }
287
+ const error = responseError(response, text, 'verification', this.options.origin);
288
+ if (response.status === 503 && error.code === 'LOCAL_MODE_DISABLED')
289
+ throw error;
290
+ return null;
291
+ }
292
+ /** `POST local-session/request`: vigilbase.com emails a 6-digit code and returns the token for it. */
293
+ async requestCode(body) {
294
+ // The address is never echoed, even when it does not fit the contract.
295
+ const checked = LocalRequestBodySchema.safeParse(body);
296
+ if (!checked.success)
297
+ throw new LocalCheckupError('EMAIL_INVALID', 'That is not an email address vigilbase.com can send a code to.');
298
+ const { response, text } = await this.postJson(LOCAL_REQUEST_PATH, checked.data);
299
+ return parseSuccess(LocalRequestResponseSchema, response, text, 'verification').verification;
300
+ }
301
+ /** `POST local-session/verify`: trades the token and the emailed code for a session code (the next one on every call). */
302
+ async verifyCode(body) {
303
+ const checked = LocalVerifyBodySchema.safeParse(body);
304
+ if (!checked.success)
305
+ throw new LocalCheckupError('EMAIL_CODE_INVALID', 'The code must be the 6 digits from the email.');
306
+ for (let attempt = 0;; attempt++) {
307
+ try {
308
+ const { response, text } = await this.postJson(LOCAL_VERIFY_PATH, checked.data);
309
+ return parseSuccess(LocalVerifyResponseSchema, response, text, 'verification').session;
310
+ }
311
+ catch (error) {
312
+ if (attempt >= VERIFY_RETRY_DELAYS_MS.length || !isTransientFailure(error))
313
+ throw error;
314
+ await this.sleep(VERIFY_RETRY_DELAYS_MS[attempt]);
315
+ }
316
+ }
317
+ }
318
+ }
319
+ export class SubmitClient extends LocalHttpClient {
320
+ code;
321
+ constructor(options) {
322
+ const { code, ...rest } = options;
323
+ super(rest);
324
+ if (!SESSION_CODE_REGEX.test(code))
325
+ throw new LocalCheckupError('SESSION_INVALID', 'vigilbase.com returned a malformed checkup session.');
326
+ this.code = code;
327
+ }
328
+ bearer(extra = {}) {
329
+ return this.headers({ Authorization: `Bearer ${this.code}`, ...extra });
330
+ }
331
+ /** `GET local-session`: the bound account (or null), masked requester, expiry and accepted limits. */
332
+ async getSession() {
333
+ const { response, text } = await this.request(LOCAL_SESSION_PATH, { method: 'GET', headers: this.bearer() }, this.options.timeoutMs ?? GET_TIMEOUT_MS);
334
+ if (!response.ok || isChallenge(response))
335
+ throw responseError(response, text, 'session', this.options.origin);
336
+ return parseSuccess(LocalSessionResponseSchema, response, text, 'session').session;
337
+ }
338
+ /**
339
+ * `POST local-session/snapshot` with the exact file bytes. 503 and network
340
+ * errors are retried twice (after 2 s, then 5 s) with the same bytes, which
341
+ * the server treats idempotently. Challenges are never retried.
342
+ */
343
+ async postSnapshot(bytes, sha256) {
344
+ for (let attempt = 0;; attempt++) {
345
+ let result;
346
+ try {
347
+ result = await this.request(LOCAL_SNAPSHOT_PATH, {
348
+ method: 'POST',
349
+ headers: this.bearer({ 'Content-Type': 'application/json', [SNAPSHOT_SHA256_HEADER]: sha256 }),
350
+ body: new Uint8Array(bytes),
351
+ }, this.options.timeoutMs ?? POST_TIMEOUT_MS);
352
+ }
353
+ catch (error) {
354
+ if (error instanceof LocalCheckupError && error.status === 0 && attempt < POST_RETRY_DELAYS_MS.length) {
355
+ await this.sleep(POST_RETRY_DELAYS_MS[attempt]);
356
+ continue;
357
+ }
358
+ throw error;
359
+ }
360
+ const { response, text } = result;
361
+ if (response.ok && !isChallenge(response)) {
362
+ const parsed = parseSuccess(LocalSnapshotResponseSchema, response, text, 'upload');
363
+ if (parsed.snapshotSha256 !== sha256) {
364
+ throw new LocalCheckupError('SUBMIT_FAILED', 'vigilbase.com acknowledged a different snapshot digest than the file uploaded.');
365
+ }
366
+ const report = { ...parsed.report };
367
+ // The report carries no run ID or share link; the results page arrives only as `resultsUrl`.
368
+ delete report.toolRunId;
369
+ delete report.shareableUrl;
370
+ delete report.shareUrl;
371
+ const resultsUrl = trustedResultsUrl(parsed.resultsUrl);
372
+ return { replayed: parsed.replayed, snapshotSha256: parsed.snapshotSha256, provenance: parsed.provenance, report, ...(resultsUrl ? { resultsUrl } : {}) };
373
+ }
374
+ const error = responseError(response, text, 'session', this.options.origin);
375
+ if (response.status === 503 && error.code !== 'VIGILBASE_CHALLENGED' && error.code !== 'LOCAL_MODE_DISABLED' && attempt < POST_RETRY_DELAYS_MS.length) {
376
+ await this.sleep(POST_RETRY_DELAYS_MS[attempt]);
377
+ continue;
378
+ }
379
+ throw error;
380
+ }
381
+ }
382
+ }
383
+ /** Refuse a server (its session, or its availability answer) that requires a newer CLI, or does not accept this schema version. */
384
+ export function assertServerAccepts(server, cliVersion, schemaVersion) {
385
+ if (!server.accepts.schemaVersions.includes(schemaVersion) || compareSemver(cliVersion, server.minCliVersion) < 0) {
386
+ throw new LocalCheckupError('CLI_UPGRADE_REQUIRED', `vigilbase.com requires @vigilbase/cli ${server.minCliVersion.slice(0, 40)} or newer for this snapshot.`);
387
+ }
388
+ }
389
+ /**
390
+ * The masked address as the server masks it (first character, `***`, the
391
+ * domain); any other text from the server is replaced by `fallback`.
392
+ */
393
+ export function displayRequester(requester, fallback = 'your email address') {
394
+ return /^[^\s@\p{Cc}\p{Cf}]{1,4}\*{3}@[A-Za-z0-9.-]{1,253}$/u.test(requester) ? requester : fallback;
395
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The interactive terminal UI of the one command and `submit`: a step
3
+ * timeline on stderr with inputs for the email and the code, an account
4
+ * picker, Yes/No questions, spinners and a live collection progress bar.
5
+ *
6
+ * It is used only on an interactive terminal (`terminalFeatures`: stdin and
7
+ * stderr are TTYs, TERM is not dumb, CI is not set, no `--json`); everywhere
8
+ * else the commands keep their plain lines and prompts. What it shows follows
9
+ * the same rules as the plain output: the masked address from vigilbase.com,
10
+ * never the address, the code, the token or the session code once entered;
11
+ * account names only through `displayText`; counts, never values, from the
12
+ * snapshot.
13
+ *
14
+ * Every label is built from the session's symbols (`ellipsis`, `join`), so an
15
+ * ASCII-only console (the Linux console, legacy Windows consoles) gets no
16
+ * character outside ASCII.
17
+ */
18
+ import { type Writer } from '../../lib/format.js';
19
+ import { Tui, type SignalSource, type TtyInput, type TtyOutput } from '../../lib/tui.js';
20
+ import type { CollectProgress } from './collector.js';
21
+ import type { EmailVerificationView } from './email-verification.js';
22
+ /** Sign-in attempts offered after the first one fails. */
23
+ export declare const MAX_SIGN_IN_RETRIES = 2;
24
+ export type CollectionPlan = {
25
+ zones: number;
26
+ plannedReads: number;
27
+ maxRequests: number;
28
+ };
29
+ export type CollectedSummary = {
30
+ zones: number;
31
+ total: number;
32
+ ok: number;
33
+ notCollectable: number;
34
+ denied: number;
35
+ /** Other HTTP errors and reads that could not be collected. */
36
+ failed: number;
37
+ path: string;
38
+ bytes: number;
39
+ /** `2 member emails`, `3 DNS names`, … (counts only). */
40
+ contents: string[];
41
+ inspect: string;
42
+ };
43
+ export type UploadSummary = {
44
+ replayed: boolean;
45
+ passed?: number;
46
+ warnings?: number;
47
+ failed?: number;
48
+ notCollected: number;
49
+ resultsUrl?: string;
50
+ opening: boolean;
51
+ requester: string;
52
+ };
53
+ export type CheckupTuiOptions = {
54
+ mode: 'text' | 'json';
55
+ stdin: TtyInput;
56
+ stderr: TtyOutput;
57
+ /** stdout; when it is not a terminal, the result lines are written there too (plain). */
58
+ stdout?: {
59
+ isTTY?: boolean;
60
+ };
61
+ env: NodeJS.ProcessEnv;
62
+ /** The command's writer, for those plain result lines. */
63
+ writer: Writer;
64
+ platform?: NodeJS.Platform;
65
+ signals?: SignalSource;
66
+ exit?: (code: number) => void;
67
+ frameMs?: number;
68
+ now?: () => number;
69
+ };
70
+ export declare class CheckupTui {
71
+ readonly tui: Tui;
72
+ private readonly writer;
73
+ private readonly now;
74
+ private readonly echoResults;
75
+ private constructor();
76
+ /** The terminal UI, or null for the plain output. */
77
+ static open(options: CheckupTuiOptions): CheckupTui | null;
78
+ /** What Ctrl-C prints at this stage. */
79
+ set cancelMessage(message: string);
80
+ /**
81
+ * A writer for the lines the commands print outside the steps below
82
+ * (warnings, notes): `Warning: …` becomes `▲`, anything else a `│` line,
83
+ * so nothing is written through the live frame.
84
+ */
85
+ get lines(): Writer;
86
+ intro(): void;
87
+ /** `…`, or `...` on an ASCII-only console: for masked IDs and cut text built outside this class. */
88
+ get ellipsis(): string;
89
+ /** `a · b · c`, or `a - b - c` on an ASCII-only console. */
90
+ join(parts: readonly string[]): string;
91
+ /** `label…` (`label...` in ASCII): an activity in progress, without its ellipsis. */
92
+ private ongoing;
93
+ /** `◒ label…` while `work` runs; `label` has no ellipsis. */
94
+ busy<T>(label: string, work: () => Promise<T>): Promise<T>;
95
+ /** `◒ label…` until stopped; `label` (also in `update`) has no ellipsis. */
96
+ spinner(label: string): {
97
+ update(label: string): void;
98
+ stop(): void;
99
+ };
100
+ /** `◇ title detail` */
101
+ step(title: string, detail?: string): void;
102
+ warning(text: string): void;
103
+ /** A Yes/No question; the answer stays on screen (`◇ question › Yes`). Ctrl-D answers no. */
104
+ confirm(question: string, options: {
105
+ initial: boolean;
106
+ detail?: string;
107
+ }): Promise<boolean>;
108
+ /** A question a flag answered: `◇ question › Yes (--yes)`. */
109
+ answeredByFlag(question: string, flag: string): void;
110
+ /**
111
+ * The verification's inputs: the address is shown while it is typed and
112
+ * sent, then replaced by the masked address vigilbase.com returns; the code
113
+ * is shown while it is typed, then masked (●●●●●●). A notice (a refused
114
+ * address, a wrong code) appears inside the next input, which is drawn in
115
+ * the same place.
116
+ */
117
+ emailView(): EmailVerificationView;
118
+ private retryClock;
119
+ /**
120
+ * cf's interactive sign-in with the terminal handed over (cf prints and
121
+ * reads directly); after a failed or cancelled sign-in, "Try signing in
122
+ * again?" up to MAX_SIGN_IN_RETRIES times. `check` is whoami afterwards.
123
+ */
124
+ signIn<T>(run: () => Promise<number>, check: () => Promise<T>): Promise<T>;
125
+ signedIn(summary: string): void;
126
+ /** The account picker; `label` is already safe (`displayText`). Null on Ctrl-D. */
127
+ chooseAccount(accounts: ReadonlyArray<{
128
+ id: string;
129
+ label: string;
130
+ detail: string;
131
+ }>): Promise<string | null>;
132
+ /** `◇ Account Acme Corp a1b2…8f90`, the last line before the collection. */
133
+ account(label: string, detail: string): void;
134
+ /**
135
+ * `◒ Collecting ████░░ zones 1/2 · 40/97 reads · ~20 s left`, redrawn on
136
+ * every progress event. The estimate is the larger of what the request
137
+ * budget allows for the reads left and the pace so far.
138
+ */
139
+ collecting(plan: CollectionPlan): {
140
+ update(progress: CollectProgress): void;
141
+ stop(): void;
142
+ };
143
+ /** `◇ 84 reads · 72 ok · 10 not collectable · 2 refused`, the saved file and what it contains. */
144
+ collected(summary: CollectedSummary): void;
145
+ /** `◆ Upload 13 KB to vigilbase.com? › Yes / No`; Enter on the initial state means No. */
146
+ confirmUpload(input: {
147
+ bytes: number;
148
+ where: string;
149
+ requester: string;
150
+ flag: string | null;
151
+ }): Promise<boolean>;
152
+ /** The upload and the scoring, which happen in one request. */
153
+ uploading<T>(bytes: number, work: () => Promise<T>): Promise<T>;
154
+ /**
155
+ * `└ ✓ Done. 10 passed · 2 failed`, the results link (underlined, an OSC 8
156
+ * link where supported) and the email note. `plainLines` are the plain
157
+ * result lines, also written to stdout when stdout is not a terminal.
158
+ */
159
+ uploaded(summary: UploadSummary, plainLines: readonly string[]): void;
160
+ /** Dim lines under the end of the timeline (the logout hint), then the command on a line of its own. */
161
+ footnote(text: string, command?: string): void;
162
+ /** Before an upload error is shown: the `submit` command for the saved snapshot, on a line of its own. */
163
+ uploadLater(command: string): void;
164
+ /**
165
+ * `■ message` with the hint under it, for an error that ends the command;
166
+ * `runCli` then does not print it again. After Ctrl-C nothing more is shown.
167
+ */
168
+ fail(error: unknown): void;
169
+ close(): void;
170
+ }