@vigilbase/cli 0.1.3 → 0.2.0

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 (142) hide show
  1. package/CLOUDFLARE.md +2 -0
  2. package/COMMANDS.md +43 -6
  3. package/LOCAL.md +731 -0
  4. package/README.md +58 -6
  5. package/dist/auth/device-auth.d.ts.map +1 -1
  6. package/dist/auth/device-auth.js +7 -3
  7. package/dist/auth/device-auth.js.map +1 -1
  8. package/dist/auth/index.d.ts +3 -0
  9. package/dist/auth/index.d.ts.map +1 -1
  10. package/dist/auth/index.js +2 -0
  11. package/dist/auth/index.js.map +1 -1
  12. package/dist/auth/provider.d.ts +18 -0
  13. package/dist/auth/provider.d.ts.map +1 -0
  14. package/dist/auth/provider.js +62 -0
  15. package/dist/auth/provider.js.map +1 -0
  16. package/dist/auth/refresh.d.ts +10 -0
  17. package/dist/auth/refresh.d.ts.map +1 -0
  18. package/dist/auth/refresh.js +143 -0
  19. package/dist/auth/refresh.js.map +1 -0
  20. package/dist/auth/store.d.ts +27 -2
  21. package/dist/auth/store.d.ts.map +1 -1
  22. package/dist/auth/store.js +23 -8
  23. package/dist/auth/store.js.map +1 -1
  24. package/dist/auth/token-metadata.d.ts +10 -0
  25. package/dist/auth/token-metadata.d.ts.map +1 -0
  26. package/dist/auth/token-metadata.js +22 -0
  27. package/dist/auth/token-metadata.js.map +1 -0
  28. package/dist/auth/types.d.ts +8 -1
  29. package/dist/auth/types.d.ts.map +1 -1
  30. package/dist/cli.d.ts.map +1 -1
  31. package/dist/cli.js +40 -9
  32. package/dist/cli.js.map +1 -1
  33. package/dist/commands/auth.d.ts.map +1 -1
  34. package/dist/commands/auth.js +20 -8
  35. package/dist/commands/auth.js.map +1 -1
  36. package/dist/commands/helpers.d.ts +15 -0
  37. package/dist/commands/helpers.d.ts.map +1 -1
  38. package/dist/commands/helpers.js +2 -2
  39. package/dist/commands/helpers.js.map +1 -1
  40. package/dist/commands/index.d.ts +3 -1
  41. package/dist/commands/index.d.ts.map +1 -1
  42. package/dist/commands/index.js +6 -1
  43. package/dist/commands/index.js.map +1 -1
  44. package/dist/commands/local.d.ts +30 -0
  45. package/dist/commands/local.d.ts.map +1 -0
  46. package/dist/commands/local.js +1520 -0
  47. package/dist/commands/local.js.map +1 -0
  48. package/dist/commands/public-tools.d.ts.map +1 -1
  49. package/dist/commands/public-tools.js +8 -4
  50. package/dist/commands/public-tools.js.map +1 -1
  51. package/dist/index.d.ts +2 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/lib/config.d.ts +16 -1
  56. package/dist/lib/config.d.ts.map +1 -1
  57. package/dist/lib/config.js +36 -4
  58. package/dist/lib/config.js.map +1 -1
  59. package/dist/lib/format.d.ts.map +1 -1
  60. package/dist/lib/format.js +9 -0
  61. package/dist/lib/format.js.map +1 -1
  62. package/dist/lib/help.d.ts +1 -1
  63. package/dist/lib/help.d.ts.map +1 -1
  64. package/dist/lib/help.js +9 -0
  65. package/dist/lib/help.js.map +1 -1
  66. package/dist/lib/network.d.ts.map +1 -1
  67. package/dist/lib/network.js +2 -1
  68. package/dist/lib/network.js.map +1 -1
  69. package/dist/lib/one-client.d.ts +3 -2
  70. package/dist/lib/one-client.d.ts.map +1 -1
  71. package/dist/lib/one-client.js +46 -2
  72. package/dist/lib/one-client.js.map +1 -1
  73. package/dist/lib/security-client.d.ts +1 -1
  74. package/dist/lib/tui.d.ts +309 -0
  75. package/dist/lib/tui.d.ts.map +1 -0
  76. package/dist/lib/tui.js +847 -0
  77. package/dist/lib/tui.js.map +1 -0
  78. package/dist/local/cloudflare-checkup/cf/allowlist.d.ts +95 -0
  79. package/dist/local/cloudflare-checkup/cf/allowlist.d.ts.map +1 -0
  80. package/dist/local/cloudflare-checkup/cf/allowlist.js +184 -0
  81. package/dist/local/cloudflare-checkup/cf/allowlist.js.map +1 -0
  82. package/dist/local/cloudflare-checkup/cf/auth-lifecycle.d.ts +113 -0
  83. package/dist/local/cloudflare-checkup/cf/auth-lifecycle.d.ts.map +1 -0
  84. package/dist/local/cloudflare-checkup/cf/auth-lifecycle.js +313 -0
  85. package/dist/local/cloudflare-checkup/cf/auth-lifecycle.js.map +1 -0
  86. package/dist/local/cloudflare-checkup/cf/child-env.d.ts +32 -0
  87. package/dist/local/cloudflare-checkup/cf/child-env.d.ts.map +1 -0
  88. package/dist/local/cloudflare-checkup/cf/child-env.js +81 -0
  89. package/dist/local/cloudflare-checkup/cf/child-env.js.map +1 -0
  90. package/dist/local/cloudflare-checkup/cf/install.d.ts +106 -0
  91. package/dist/local/cloudflare-checkup/cf/install.d.ts.map +1 -0
  92. package/dist/local/cloudflare-checkup/cf/install.js +309 -0
  93. package/dist/local/cloudflare-checkup/cf/install.js.map +1 -0
  94. package/dist/local/cloudflare-checkup/cf/locate.d.ts +44 -0
  95. package/dist/local/cloudflare-checkup/cf/locate.d.ts.map +1 -0
  96. package/dist/local/cloudflare-checkup/cf/locate.js +228 -0
  97. package/dist/local/cloudflare-checkup/cf/locate.js.map +1 -0
  98. package/dist/local/cloudflare-checkup/cf/run-cf.d.ts +89 -0
  99. package/dist/local/cloudflare-checkup/cf/run-cf.d.ts.map +1 -0
  100. package/dist/local/cloudflare-checkup/cf/run-cf.js +275 -0
  101. package/dist/local/cloudflare-checkup/cf/run-cf.js.map +1 -0
  102. package/dist/local/cloudflare-checkup/collector.d.ts +48 -0
  103. package/dist/local/cloudflare-checkup/collector.d.ts.map +1 -0
  104. package/dist/local/cloudflare-checkup/collector.js +296 -0
  105. package/dist/local/cloudflare-checkup/collector.js.map +1 -0
  106. package/dist/local/cloudflare-checkup/contract.d.ts +2793 -0
  107. package/dist/local/cloudflare-checkup/contract.d.ts.map +1 -0
  108. package/dist/local/cloudflare-checkup/contract.js +1614 -0
  109. package/dist/local/cloudflare-checkup/contract.js.map +1 -0
  110. package/dist/local/cloudflare-checkup/email-verification.d.ts +125 -0
  111. package/dist/local/cloudflare-checkup/email-verification.d.ts.map +1 -0
  112. package/dist/local/cloudflare-checkup/email-verification.js +212 -0
  113. package/dist/local/cloudflare-checkup/email-verification.js.map +1 -0
  114. package/dist/local/cloudflare-checkup/errors.d.ts +28 -0
  115. package/dist/local/cloudflare-checkup/errors.d.ts.map +1 -0
  116. package/dist/local/cloudflare-checkup/errors.js +99 -0
  117. package/dist/local/cloudflare-checkup/errors.js.map +1 -0
  118. package/dist/local/cloudflare-checkup/output.d.ts +24 -0
  119. package/dist/local/cloudflare-checkup/output.d.ts.map +1 -0
  120. package/dist/local/cloudflare-checkup/output.js +89 -0
  121. package/dist/local/cloudflare-checkup/output.js.map +1 -0
  122. package/dist/local/cloudflare-checkup/prompts.d.ts +41 -0
  123. package/dist/local/cloudflare-checkup/prompts.d.ts.map +1 -0
  124. package/dist/local/cloudflare-checkup/prompts.js +140 -0
  125. package/dist/local/cloudflare-checkup/prompts.js.map +1 -0
  126. package/dist/local/cloudflare-checkup/rate-budget.d.ts +42 -0
  127. package/dist/local/cloudflare-checkup/rate-budget.d.ts.map +1 -0
  128. package/dist/local/cloudflare-checkup/rate-budget.js +80 -0
  129. package/dist/local/cloudflare-checkup/rate-budget.js.map +1 -0
  130. package/dist/local/cloudflare-checkup/snapshot-file.d.ts +53 -0
  131. package/dist/local/cloudflare-checkup/snapshot-file.d.ts.map +1 -0
  132. package/dist/local/cloudflare-checkup/snapshot-file.js +186 -0
  133. package/dist/local/cloudflare-checkup/snapshot-file.js.map +1 -0
  134. package/dist/local/cloudflare-checkup/submit-client.d.ts +98 -0
  135. package/dist/local/cloudflare-checkup/submit-client.d.ts.map +1 -0
  136. package/dist/local/cloudflare-checkup/submit-client.js +349 -0
  137. package/dist/local/cloudflare-checkup/submit-client.js.map +1 -0
  138. package/dist/local/cloudflare-checkup/terminal-ui.d.ts +171 -0
  139. package/dist/local/cloudflare-checkup/terminal-ui.d.ts.map +1 -0
  140. package/dist/local/cloudflare-checkup/terminal-ui.js +404 -0
  141. package/dist/local/cloudflare-checkup/terminal-ui.js.map +1 -0
  142. package/package.json +8 -4
@@ -0,0 +1,186 @@
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
+ }
186
+ //# sourceMappingURL=snapshot-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-file.js","sourceRoot":"","sources":["../../../src/local/cloudflare-checkup/snapshot-file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ import { 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
+ /** `POST local-session/request`: the token naming the run that awaits its emailed code. */
9
+ export type PendingVerification = LocalRequestResponse['verification'];
10
+ /** `POST local-session/verify`: the session code for the upload (kept in memory only). */
11
+ export type VerifiedSession = LocalVerifyResponse['session'];
12
+ export declare const GET_TIMEOUT_MS = 30000;
13
+ export declare const POST_TIMEOUT_MS = 120000;
14
+ export declare const MAX_SUBMIT_RESPONSE_BYTES: number;
15
+ export declare const POST_RETRY_DELAYS_MS: readonly [2000, 5000];
16
+ /** A verify that failed on the network or with a 503 is sent once more, after this pause. */
17
+ export declare const VERIFY_RETRY_DELAYS_MS: readonly [2000];
18
+ /**
19
+ * The submit origin. Only `https://vigilbase.com` and loopback origins are
20
+ * accepted; there is no `*.vigilbase.com` wildcard (other subdomains are
21
+ * different services). Environment variables are never consulted.
22
+ */
23
+ export declare function resolveSubmitOrigin(explicit: string | undefined): string;
24
+ export type LocalClientOptions = {
25
+ origin: string;
26
+ cliVersion: string;
27
+ userAgent: string;
28
+ /** Overrides every request timeout (`--timeout`). */
29
+ timeoutMs?: number;
30
+ sleep?: (ms: number) => Promise<void>;
31
+ /** Injectable for tests; the global `fetch` (looked up per request) otherwise. */
32
+ fetch?: typeof fetch;
33
+ };
34
+ export type SubmitClientOptions = LocalClientOptions & {
35
+ code: string;
36
+ };
37
+ /**
38
+ * The results page from an upload response, as printable ASCII (`URL.href`
39
+ * percent-encodes everything else), or undefined when it is not a plain
40
+ * http(s) URL. The caller decides whether to open it (same origin only).
41
+ */
42
+ export declare function trustedResultsUrl(raw: string | undefined): string | undefined;
43
+ /** Shared transport: CLI headers, no redirects, bounded responses, CLI-authored network errors. */
44
+ declare abstract class LocalHttpClient {
45
+ protected readonly options: LocalClientOptions;
46
+ constructor(options: LocalClientOptions);
47
+ protected sleep(ms: number): Promise<void>;
48
+ protected headers(extra?: Record<string, string>): Record<string, string>;
49
+ protected request(path: string, init: RequestInit, timeoutMs: number): Promise<{
50
+ response: Response;
51
+ text: string;
52
+ }>;
53
+ }
54
+ /**
55
+ * Email verification without a bearer. A code request is never retried: a
56
+ * repeated request would send a second email. A verify that failed on the
57
+ * network (or timed out) or with a 503 is sent once more, after 2 s, with the
58
+ * same token and code: One counts only wrong codes, and the right code
59
+ * verified again mints the next session code, as a renewal does. Only when
60
+ * the lost answer was to a wrong code does the retry cost a second try.
61
+ */
62
+ export declare class VerificationClient extends LocalHttpClient {
63
+ private postJson;
64
+ /** `POST local-session/request`: vigilbase.com emails a 6-digit code and returns the token for it. */
65
+ requestCode(body: LocalRequestBody): Promise<PendingVerification>;
66
+ /** `POST local-session/verify`: trades the token and the emailed code for a session code (the next one on every call). */
67
+ verifyCode(body: LocalVerifyBody): Promise<VerifiedSession>;
68
+ }
69
+ export type UploadResult = {
70
+ replayed: boolean;
71
+ snapshotSha256: string;
72
+ provenance: Record<string, unknown>;
73
+ report: Record<string, unknown>;
74
+ /** The results page, when vigilbase.com returned one (see `trustedResultsUrl`). */
75
+ resultsUrl?: string;
76
+ };
77
+ export declare class SubmitClient extends LocalHttpClient {
78
+ private readonly code;
79
+ constructor(options: SubmitClientOptions);
80
+ private bearer;
81
+ /** `GET local-session`: the bound account (or null), masked requester, expiry and accepted limits. */
82
+ getSession(): Promise<LocalSession>;
83
+ /**
84
+ * `POST local-session/snapshot` with the exact file bytes. 503 and network
85
+ * errors are retried twice (after 2 s, then 5 s) with the same bytes, which
86
+ * the server treats idempotently. Challenges are never retried.
87
+ */
88
+ postSnapshot(bytes: Buffer, sha256: string): Promise<UploadResult>;
89
+ }
90
+ /** Refuse a server that requires a newer CLI, or does not accept this schema version. */
91
+ export declare function assertSessionAccepts(session: LocalSession, cliVersion: string, schemaVersion: string): void;
92
+ /**
93
+ * The masked address as the server masks it (first character, `***`, the
94
+ * domain); any other text from the server is replaced by `fallback`.
95
+ */
96
+ export declare function displayRequester(requester: string, fallback?: string): string;
97
+ export {};
98
+ //# sourceMappingURL=submit-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submit-client.d.ts","sourceRoot":"","sources":["../../../src/local/cloudflare-checkup/submit-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,349 @@
1
+ import { NetworkError, readBoundedBody } from '../../lib/network.js';
2
+ import { CLI_VERSION_HEADER, LOCAL_REQUEST_PATH, LOCAL_SESSION_PATH, LOCAL_SNAPSHOT_PATH, LOCAL_VERIFY_PATH, LocalErrorResponseSchema, LocalRequestBodySchema, LocalRequestResponseSchema, LocalSessionResponseSchema, LocalSnapshotResponseSchema, LocalVerifyBodySchema, LocalVerifyResponseSchema, PRODUCTION_ORIGIN, SESSION_CODE_REGEX, SNAPSHOT_SHA256_HEADER, compareSemver, } from './contract.js';
3
+ import { LocalCheckupError } from './errors.js';
4
+ import { formatDuration } from './output.js';
5
+ export const GET_TIMEOUT_MS = 30_000;
6
+ export const POST_TIMEOUT_MS = 120_000;
7
+ export const MAX_SUBMIT_RESPONSE_BYTES = 1024 * 1024;
8
+ export const POST_RETRY_DELAYS_MS = [2_000, 5_000];
9
+ /** A verify that failed on the network or with a 503 is sent once more, after this pause. */
10
+ export const VERIFY_RETRY_DELAYS_MS = [2_000];
11
+ function isLoopbackHost(hostname) {
12
+ return hostname === 'localhost' || hostname === '[::1]' || /^127\.(?:\d{1,3}\.){2}\d{1,3}$/.test(hostname);
13
+ }
14
+ /**
15
+ * The submit origin. Only `https://vigilbase.com` and loopback origins are
16
+ * accepted; there is no `*.vigilbase.com` wildcard (other subdomains are
17
+ * different services). Environment variables are never consulted.
18
+ */
19
+ export function resolveSubmitOrigin(explicit) {
20
+ if (explicit === undefined)
21
+ return PRODUCTION_ORIGIN;
22
+ let url;
23
+ try {
24
+ url = new URL(explicit.trim());
25
+ }
26
+ catch {
27
+ throw new LocalCheckupError('SUBMIT_ORIGIN_UNTRUSTED', '--base-url is not a valid origin.');
28
+ }
29
+ const originOnly = !url.username && !url.password && /^\/*$/.test(url.pathname) && !url.search && !url.hash;
30
+ if (originOnly && url.origin === PRODUCTION_ORIGIN)
31
+ return PRODUCTION_ORIGIN;
32
+ if (originOnly && (url.protocol === 'http:' || url.protocol === 'https:') && isLoopbackHost(url.hostname))
33
+ return url.origin;
34
+ // Do not echo the value: it may contain credentials.
35
+ throw new LocalCheckupError('SUBMIT_ORIGIN_UNTRUSTED', 'Local checkups submit only to https://vigilbase.com or a loopback origin.');
36
+ }
37
+ const REASON_CODES = {
38
+ session_invalid: 'SESSION_INVALID',
39
+ session_expired: 'SESSION_EXPIRED',
40
+ session_used: 'SESSION_USED',
41
+ session_busy: 'SESSION_BUSY',
42
+ session_not_ready: 'SESSION_INVALID',
43
+ run_closed: 'SESSION_CLOSED',
44
+ browser_origin: 'SUBMIT_FAILED',
45
+ malformed_json: 'SUBMIT_FAILED',
46
+ digest_mismatch: 'SUBMIT_FAILED',
47
+ unsupported_media_type: 'SUBMIT_FAILED',
48
+ length_required: 'SUBMIT_FAILED',
49
+ snapshot_too_large: 'SNAPSHOT_TOO_LARGE',
50
+ snapshot_invalid: 'SNAPSHOT_INVALID',
51
+ snapshot_incomplete: 'SNAPSHOT_INVALID',
52
+ account_mismatch: 'ACCOUNT_MISMATCH',
53
+ snapshot_stale: 'SNAPSHOT_STALE',
54
+ collector_unsupported: 'CLI_UPGRADE_REQUIRED',
55
+ secret_like_value: 'SNAPSHOT_SECRET_LIKE_VALUE',
56
+ evaluation_failed: 'SUBMIT_FAILED',
57
+ rate_limited: 'VIGILBASE_RATE_LIMITED',
58
+ local_mode_disabled: 'LOCAL_MODE_DISABLED',
59
+ storage_unavailable: 'SUBMIT_FAILED',
60
+ request_invalid: 'SUBMIT_FAILED',
61
+ email_invalid: 'EMAIL_INVALID',
62
+ code_invalid: 'EMAIL_CODE_INVALID',
63
+ verification_invalid: 'VERIFICATION_INVALID',
64
+ verification_expired: 'VERIFICATION_EXPIRED',
65
+ };
66
+ const REASON_MESSAGES = {
67
+ session_invalid: 'The checkup session is not valid (revoked or for another checkup).',
68
+ session_expired: 'The checkup session has expired.',
69
+ session_used: 'This checkup already has results from a different snapshot.',
70
+ session_busy: 'A different snapshot is already being processed for this checkup.',
71
+ session_not_ready: 'This checkup is not ready for an upload yet; its email is not verified.',
72
+ run_closed: 'This checkup was cancelled or has expired.',
73
+ browser_origin: 'vigilbase.com refused the request as a browser request.',
74
+ malformed_json: 'vigilbase.com could not read the uploaded file as JSON.',
75
+ digest_mismatch: 'The upload was corrupted in transit (SHA-256 mismatch).',
76
+ unsupported_media_type: 'vigilbase.com rejected the upload content type.',
77
+ length_required: 'vigilbase.com required a Content-Length for the upload.',
78
+ snapshot_too_large: 'The snapshot is larger than vigilbase.com accepts.',
79
+ snapshot_invalid: 'vigilbase.com rejected the snapshot as invalid.',
80
+ snapshot_incomplete: 'vigilbase.com rejected the snapshot as incomplete (missing or extra reads).',
81
+ account_mismatch: 'The snapshot is for a different Cloudflare account than this checkup.',
82
+ snapshot_stale: 'The snapshot is older than 24 hours (or dated in the future).',
83
+ collector_unsupported: 'vigilbase.com no longer accepts snapshots from this CLI or cf version.',
84
+ secret_like_value: 'vigilbase.com found a value that looks like a credential in the snapshot.',
85
+ evaluation_failed: 'vigilbase.com could not evaluate the snapshot; the checkup is unchanged.',
86
+ rate_limited: 'vigilbase.com rate-limited the upload.',
87
+ local_mode_disabled: 'Local checkups are currently disabled on vigilbase.com.',
88
+ storage_unavailable: 'vigilbase.com storage is temporarily unavailable.',
89
+ request_invalid: 'vigilbase.com could not read the verification request.',
90
+ email_invalid: 'vigilbase.com accepts work email addresses only (not personal or disposable ones).',
91
+ code_invalid: 'That code is not correct.',
92
+ verification_invalid: 'The email verification is not valid.',
93
+ verification_expired: 'The email verification has expired (the code timed out, was tried too often, or the checkup closed).',
94
+ };
95
+ const RATE_LIMITED_MESSAGES = {
96
+ verification: 'vigilbase.com rate-limited verification codes for now.',
97
+ session: REASON_MESSAGES.rate_limited,
98
+ };
99
+ function retryAfter(response, body) {
100
+ if (body?.retryAfterSec !== undefined)
101
+ return body.retryAfterSec;
102
+ const header = response.headers.get('retry-after');
103
+ if (header && /^\d{1,6}$/.test(header.trim()))
104
+ return Number(header.trim());
105
+ return undefined;
106
+ }
107
+ function isChallenge(response) {
108
+ if ((response.headers.get('cf-mitigated') ?? '').toLowerCase().includes('challenge'))
109
+ return true;
110
+ const type = (response.headers.get('content-type') ?? '').toLowerCase();
111
+ return (response.status === 403 || response.status === 503) && type.includes('text/html');
112
+ }
113
+ /** Map a non-2xx response to a CLI error; never echoes server text. */
114
+ function responseError(response, text, endpoint) {
115
+ if (isChallenge(response)) {
116
+ return new LocalCheckupError('VIGILBASE_CHALLENGED', 'vigilbase.com answered with a bot or WAF challenge instead of the API.', { status: response.status });
117
+ }
118
+ let body = null;
119
+ try {
120
+ const parsed = LocalErrorResponseSchema.safeParse(JSON.parse(text));
121
+ if (parsed.success)
122
+ body = parsed.data;
123
+ }
124
+ catch {
125
+ body = null;
126
+ }
127
+ const status = response.status;
128
+ const after = retryAfter(response, body);
129
+ const timing = after !== undefined ? { retryAfterSec: after } : {};
130
+ const reason = body?.reason;
131
+ if (status === 429 || reason === 'rate_limited') {
132
+ const wait = after !== undefined ? ` Try again in ${formatDuration(after)}.` : '';
133
+ return new LocalCheckupError('VIGILBASE_RATE_LIMITED', `${RATE_LIMITED_MESSAGES[endpoint]}${wait}`, { status, ...timing });
134
+ }
135
+ if (reason)
136
+ return new LocalCheckupError(REASON_CODES[reason], REASON_MESSAGES[reason], { status, ...timing });
137
+ const fallback = status === 401 ? (endpoint === 'verification' ? 'VERIFICATION_INVALID' : 'SESSION_INVALID')
138
+ : status === 410 ? (endpoint === 'verification' ? 'VERIFICATION_EXPIRED' : 'SESSION_CLOSED')
139
+ : status === 413 ? 'SNAPSHOT_TOO_LARGE' : 'SUBMIT_FAILED';
140
+ return new LocalCheckupError(fallback, `vigilbase.com rejected the request (HTTP ${status}).`, { status });
141
+ }
142
+ /** No answer (a network error or a timeout), or vigilbase.com storage briefly unavailable (503); never a challenge or the kill switch. */
143
+ function isTransientFailure(error) {
144
+ return error instanceof LocalCheckupError && error.code === 'SUBMIT_FAILED' && (error.status === 0 || error.status === 503);
145
+ }
146
+ /** A successful JSON body validated against `schema`, or SUBMIT_FAILED naming what was expected. */
147
+ function parseSuccess(schema, response, text, what) {
148
+ let parsed;
149
+ try {
150
+ parsed = schema.safeParse(JSON.parse(text));
151
+ }
152
+ catch {
153
+ parsed = null;
154
+ }
155
+ if (!parsed?.success)
156
+ throw new LocalCheckupError('SUBMIT_FAILED', `vigilbase.com returned an unexpected ${what} response.`, { status: response.status });
157
+ return parsed.data;
158
+ }
159
+ /**
160
+ * The results page from an upload response, as printable ASCII (`URL.href`
161
+ * percent-encodes everything else), or undefined when it is not a plain
162
+ * http(s) URL. The caller decides whether to open it (same origin only).
163
+ */
164
+ export function trustedResultsUrl(raw) {
165
+ if (raw === undefined)
166
+ return undefined;
167
+ let url;
168
+ try {
169
+ url = new URL(raw);
170
+ }
171
+ catch {
172
+ return undefined;
173
+ }
174
+ if ((url.protocol !== 'https:' && url.protocol !== 'http:') || url.username || url.password)
175
+ return undefined;
176
+ return /^[\x21-\x7e]{1,2048}$/.test(url.href) ? url.href : undefined;
177
+ }
178
+ /** Shared transport: CLI headers, no redirects, bounded responses, CLI-authored network errors. */
179
+ class LocalHttpClient {
180
+ options;
181
+ constructor(options) {
182
+ this.options = options;
183
+ }
184
+ sleep(ms) {
185
+ return this.options.sleep ? this.options.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms));
186
+ }
187
+ headers(extra = {}) {
188
+ return {
189
+ Accept: 'application/json',
190
+ 'User-Agent': this.options.userAgent,
191
+ [CLI_VERSION_HEADER]: this.options.cliVersion,
192
+ ...extra,
193
+ };
194
+ }
195
+ async request(path, init, timeoutMs) {
196
+ const controller = new AbortController();
197
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
198
+ const url = `${this.options.origin}${path}`;
199
+ const send = this.options.fetch ?? fetch;
200
+ try {
201
+ // Never follow redirects: credentials and bodies must reach only the checked origin.
202
+ // 'manual' surfaces the 3xx (Node returns its real status; browsers an
203
+ // opaque redirect) so it fails fast instead of looking like a network error.
204
+ const response = await send(url, { ...init, signal: controller.signal, redirect: 'manual' });
205
+ if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400)) {
206
+ await response.body?.cancel().catch(() => undefined);
207
+ 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 });
208
+ }
209
+ return { response, text: await readBoundedBody(response, controller.signal, MAX_SUBMIT_RESPONSE_BYTES) };
210
+ }
211
+ catch (error) {
212
+ if (error instanceof LocalCheckupError)
213
+ throw error;
214
+ if (error instanceof NetworkError && error.code === 'RESPONSE_TOO_LARGE') {
215
+ throw new LocalCheckupError('SUBMIT_FAILED', 'vigilbase.com returned an unexpectedly large response.');
216
+ }
217
+ throw new LocalCheckupError('SUBMIT_FAILED', controller.signal.aborted
218
+ ? `The request to ${this.options.origin} timed out after ${timeoutMs}ms.`
219
+ : `Could not connect to ${this.options.origin}. Check your connection, VPN or proxy.`, { status: 0 });
220
+ }
221
+ finally {
222
+ clearTimeout(timer);
223
+ }
224
+ }
225
+ }
226
+ /**
227
+ * Email verification without a bearer. A code request is never retried: a
228
+ * repeated request would send a second email. A verify that failed on the
229
+ * network (or timed out) or with a 503 is sent once more, after 2 s, with the
230
+ * same token and code: One counts only wrong codes, and the right code
231
+ * verified again mints the next session code, as a renewal does. Only when
232
+ * the lost answer was to a wrong code does the retry cost a second try.
233
+ */
234
+ export class VerificationClient extends LocalHttpClient {
235
+ async postJson(path, body) {
236
+ const result = await this.request(path, {
237
+ method: 'POST',
238
+ headers: this.headers({ 'Content-Type': 'application/json' }),
239
+ body: JSON.stringify(body),
240
+ }, this.options.timeoutMs ?? GET_TIMEOUT_MS);
241
+ if (!result.response.ok || isChallenge(result.response))
242
+ throw responseError(result.response, result.text, 'verification');
243
+ return result;
244
+ }
245
+ /** `POST local-session/request`: vigilbase.com emails a 6-digit code and returns the token for it. */
246
+ async requestCode(body) {
247
+ // The address is never echoed, even when it does not fit the contract.
248
+ const checked = LocalRequestBodySchema.safeParse(body);
249
+ if (!checked.success)
250
+ throw new LocalCheckupError('EMAIL_INVALID', 'That is not an email address vigilbase.com can send a code to.');
251
+ const { response, text } = await this.postJson(LOCAL_REQUEST_PATH, checked.data);
252
+ return parseSuccess(LocalRequestResponseSchema, response, text, 'verification').verification;
253
+ }
254
+ /** `POST local-session/verify`: trades the token and the emailed code for a session code (the next one on every call). */
255
+ async verifyCode(body) {
256
+ const checked = LocalVerifyBodySchema.safeParse(body);
257
+ if (!checked.success)
258
+ throw new LocalCheckupError('EMAIL_CODE_INVALID', 'The code must be the 6 digits from the email.');
259
+ for (let attempt = 0;; attempt++) {
260
+ try {
261
+ const { response, text } = await this.postJson(LOCAL_VERIFY_PATH, checked.data);
262
+ return parseSuccess(LocalVerifyResponseSchema, response, text, 'verification').session;
263
+ }
264
+ catch (error) {
265
+ if (attempt >= VERIFY_RETRY_DELAYS_MS.length || !isTransientFailure(error))
266
+ throw error;
267
+ await this.sleep(VERIFY_RETRY_DELAYS_MS[attempt]);
268
+ }
269
+ }
270
+ }
271
+ }
272
+ export class SubmitClient extends LocalHttpClient {
273
+ code;
274
+ constructor(options) {
275
+ const { code, ...rest } = options;
276
+ super(rest);
277
+ if (!SESSION_CODE_REGEX.test(code))
278
+ throw new LocalCheckupError('SESSION_INVALID', 'vigilbase.com returned a malformed checkup session.');
279
+ this.code = code;
280
+ }
281
+ bearer(extra = {}) {
282
+ return this.headers({ Authorization: `Bearer ${this.code}`, ...extra });
283
+ }
284
+ /** `GET local-session`: the bound account (or null), masked requester, expiry and accepted limits. */
285
+ async getSession() {
286
+ const { response, text } = await this.request(LOCAL_SESSION_PATH, { method: 'GET', headers: this.bearer() }, this.options.timeoutMs ?? GET_TIMEOUT_MS);
287
+ if (!response.ok || isChallenge(response))
288
+ throw responseError(response, text, 'session');
289
+ return parseSuccess(LocalSessionResponseSchema, response, text, 'session').session;
290
+ }
291
+ /**
292
+ * `POST local-session/snapshot` with the exact file bytes. 503 and network
293
+ * errors are retried twice (after 2 s, then 5 s) with the same bytes, which
294
+ * the server treats idempotently. Challenges are never retried.
295
+ */
296
+ async postSnapshot(bytes, sha256) {
297
+ for (let attempt = 0;; attempt++) {
298
+ let result;
299
+ try {
300
+ result = await this.request(LOCAL_SNAPSHOT_PATH, {
301
+ method: 'POST',
302
+ headers: this.bearer({ 'Content-Type': 'application/json', [SNAPSHOT_SHA256_HEADER]: sha256 }),
303
+ body: new Uint8Array(bytes),
304
+ }, this.options.timeoutMs ?? POST_TIMEOUT_MS);
305
+ }
306
+ catch (error) {
307
+ if (error instanceof LocalCheckupError && error.status === 0 && attempt < POST_RETRY_DELAYS_MS.length) {
308
+ await this.sleep(POST_RETRY_DELAYS_MS[attempt]);
309
+ continue;
310
+ }
311
+ throw error;
312
+ }
313
+ const { response, text } = result;
314
+ if (response.ok && !isChallenge(response)) {
315
+ const parsed = parseSuccess(LocalSnapshotResponseSchema, response, text, 'upload');
316
+ if (parsed.snapshotSha256 !== sha256) {
317
+ throw new LocalCheckupError('SUBMIT_FAILED', 'vigilbase.com acknowledged a different snapshot digest than the file uploaded.');
318
+ }
319
+ const report = { ...parsed.report };
320
+ // The report carries no run ID or share link; the results page arrives only as `resultsUrl`.
321
+ delete report.toolRunId;
322
+ delete report.shareableUrl;
323
+ delete report.shareUrl;
324
+ const resultsUrl = trustedResultsUrl(parsed.resultsUrl);
325
+ return { replayed: parsed.replayed, snapshotSha256: parsed.snapshotSha256, provenance: parsed.provenance, report, ...(resultsUrl ? { resultsUrl } : {}) };
326
+ }
327
+ const error = responseError(response, text, 'session');
328
+ if (response.status === 503 && error.code !== 'VIGILBASE_CHALLENGED' && error.code !== 'LOCAL_MODE_DISABLED' && attempt < POST_RETRY_DELAYS_MS.length) {
329
+ await this.sleep(POST_RETRY_DELAYS_MS[attempt]);
330
+ continue;
331
+ }
332
+ throw error;
333
+ }
334
+ }
335
+ }
336
+ /** Refuse a server that requires a newer CLI, or does not accept this schema version. */
337
+ export function assertSessionAccepts(session, cliVersion, schemaVersion) {
338
+ if (!session.accepts.schemaVersions.includes(schemaVersion) || compareSemver(cliVersion, session.minCliVersion) < 0) {
339
+ throw new LocalCheckupError('CLI_UPGRADE_REQUIRED', `vigilbase.com requires @vigilbase/cli ${session.minCliVersion.slice(0, 40)} or newer for this snapshot.`);
340
+ }
341
+ }
342
+ /**
343
+ * The masked address as the server masks it (first character, `***`, the
344
+ * domain); any other text from the server is replaced by `fallback`.
345
+ */
346
+ export function displayRequester(requester, fallback = 'your email address') {
347
+ return /^[^\s@\p{Cc}\p{Cf}]{1,4}\*{3}@[A-Za-z0-9.-]{1,253}$/u.test(requester) ? requester : fallback;
348
+ }
349
+ //# sourceMappingURL=submit-client.js.map