@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.
- package/CLOUDFLARE.md +3 -4
- package/COMMANDS.md +158 -83
- package/LOCAL.md +721 -0
- package/ONE.md +2 -4
- package/README.md +341 -123
- package/SECURITY.md +2 -2
- package/dist/auth/device-auth.d.ts +0 -1
- package/dist/auth/device-auth.js +13 -8
- package/dist/auth/index.d.ts +4 -2
- package/dist/auth/index.js +3 -2
- package/dist/auth/provider.d.ts +18 -0
- package/dist/auth/provider.js +71 -0
- package/dist/auth/refresh.d.ts +29 -0
- package/dist/auth/refresh.js +199 -0
- package/dist/auth/store.d.ts +103 -4
- package/dist/auth/store.js +400 -74
- package/dist/auth/token-metadata.d.ts +9 -0
- package/dist/auth/token-metadata.js +21 -0
- package/dist/auth/types.d.ts +8 -2
- package/dist/auth/types.js +0 -1
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +0 -1
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +45 -15
- package/dist/commands/account.d.ts +0 -1
- package/dist/commands/account.js +0 -1
- package/dist/commands/auth.d.ts +0 -1
- package/dist/commands/auth.js +82 -22
- package/dist/commands/billing.d.ts +0 -1
- package/dist/commands/billing.js +0 -1
- package/dist/commands/cloudflare.d.ts +1 -2
- package/dist/commands/cloudflare.js +6 -6
- package/dist/commands/helpers.d.ts +15 -1
- package/dist/commands/helpers.js +2 -3
- package/dist/commands/index.d.ts +3 -2
- package/dist/commands/index.js +6 -2
- package/dist/commands/legacy-validation.d.ts +0 -1
- package/dist/commands/legacy-validation.js +0 -1
- package/dist/commands/local.d.ts +35 -0
- package/dist/commands/local.js +1543 -0
- package/dist/commands/one.d.ts +0 -1
- package/dist/commands/one.js +6 -7
- package/dist/commands/public-tools.d.ts +0 -1
- package/dist/commands/public-tools.js +55 -17
- package/dist/commands/runs.d.ts +0 -1
- package/dist/commands/runs.js +0 -1
- package/dist/commands/security.d.ts +1 -2
- package/dist/commands/security.js +1 -2
- package/dist/commands/watchdog.d.ts +0 -1
- package/dist/commands/watchdog.js +0 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -2
- package/dist/lib/banner.d.ts +0 -1
- package/dist/lib/banner.js +0 -1
- package/dist/lib/client.d.ts +4 -2
- package/dist/lib/client.js +42 -18
- package/dist/lib/cloudflare-client.d.ts +0 -1
- package/dist/lib/cloudflare-client.js +0 -1
- package/dist/lib/config.d.ts +31 -3
- package/dist/lib/config.js +72 -13
- package/dist/lib/format.d.ts +2 -1
- package/dist/lib/format.js +27 -7
- package/dist/lib/help.d.ts +1 -2
- package/dist/lib/help.js +37 -17
- package/dist/lib/network.d.ts +10 -1
- package/dist/lib/network.js +33 -5
- package/dist/lib/one-client.d.ts +5 -3
- package/dist/lib/one-client.js +49 -3
- package/dist/lib/one-operations.d.ts +0 -1
- package/dist/lib/one-operations.js +0 -1
- package/dist/lib/open-url.d.ts +0 -1
- package/dist/lib/open-url.js +0 -1
- package/dist/lib/platform.d.ts +2 -0
- package/dist/lib/platform.js +4 -0
- package/dist/lib/progress.d.ts +0 -1
- package/dist/lib/progress.js +0 -1
- package/dist/lib/security-client.d.ts +1 -2
- package/dist/lib/security-client.js +0 -1
- package/dist/lib/skill-fetch.d.ts +0 -1
- package/dist/lib/skill-fetch.js +0 -1
- package/dist/lib/system-trust.d.ts +0 -1
- package/dist/lib/system-trust.js +0 -1
- package/dist/lib/tui.d.ts +308 -0
- package/dist/lib/tui.js +846 -0
- package/dist/local/cloudflare-checkup/cf/allowlist.d.ts +94 -0
- package/dist/local/cloudflare-checkup/cf/allowlist.js +183 -0
- package/dist/local/cloudflare-checkup/cf/auth-lifecycle.d.ts +112 -0
- package/dist/local/cloudflare-checkup/cf/auth-lifecycle.js +312 -0
- package/dist/local/cloudflare-checkup/cf/child-env.d.ts +31 -0
- package/dist/local/cloudflare-checkup/cf/child-env.js +80 -0
- package/dist/local/cloudflare-checkup/cf/install.d.ts +105 -0
- package/dist/local/cloudflare-checkup/cf/install.js +308 -0
- package/dist/local/cloudflare-checkup/cf/locate.d.ts +43 -0
- package/dist/local/cloudflare-checkup/cf/locate.js +227 -0
- package/dist/local/cloudflare-checkup/cf/run-cf.d.ts +88 -0
- package/dist/local/cloudflare-checkup/cf/run-cf.js +274 -0
- package/dist/local/cloudflare-checkup/collector.d.ts +47 -0
- package/dist/local/cloudflare-checkup/collector.js +295 -0
- package/dist/local/cloudflare-checkup/contract.d.ts +2804 -0
- package/dist/local/cloudflare-checkup/contract.js +1620 -0
- package/dist/local/cloudflare-checkup/email-verification.d.ts +124 -0
- package/dist/local/cloudflare-checkup/email-verification.js +211 -0
- package/dist/local/cloudflare-checkup/errors.d.ts +29 -0
- package/dist/local/cloudflare-checkup/errors.js +103 -0
- package/dist/local/cloudflare-checkup/output.d.ts +23 -0
- package/dist/local/cloudflare-checkup/output.js +88 -0
- package/dist/local/cloudflare-checkup/prompts.d.ts +40 -0
- package/dist/local/cloudflare-checkup/prompts.js +139 -0
- package/dist/local/cloudflare-checkup/rate-budget.d.ts +41 -0
- package/dist/local/cloudflare-checkup/rate-budget.js +79 -0
- package/dist/local/cloudflare-checkup/snapshot-file.d.ts +52 -0
- package/dist/local/cloudflare-checkup/snapshot-file.js +185 -0
- package/dist/local/cloudflare-checkup/submit-client.d.ts +112 -0
- package/dist/local/cloudflare-checkup/submit-client.js +395 -0
- package/dist/local/cloudflare-checkup/terminal-ui.d.ts +170 -0
- package/dist/local/cloudflare-checkup/terminal-ui.js +403 -0
- package/dist/types.d.ts +25 -3
- package/dist/types.js +16 -1
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
- package/package.json +12 -12
- package/dist/auth/device-auth.d.ts.map +0 -1
- package/dist/auth/device-auth.js.map +0 -1
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/store.d.ts.map +0 -1
- package/dist/auth/store.js.map +0 -1
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/auth/types.js.map +0 -1
- package/dist/bin.d.ts.map +0 -1
- package/dist/bin.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/account.d.ts.map +0 -1
- package/dist/commands/account.js.map +0 -1
- package/dist/commands/auth.d.ts.map +0 -1
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/billing.d.ts.map +0 -1
- package/dist/commands/billing.js.map +0 -1
- package/dist/commands/cloudflare.d.ts.map +0 -1
- package/dist/commands/cloudflare.js.map +0 -1
- package/dist/commands/helpers.d.ts.map +0 -1
- package/dist/commands/helpers.js.map +0 -1
- package/dist/commands/index.d.ts.map +0 -1
- package/dist/commands/index.js.map +0 -1
- package/dist/commands/legacy-validation.d.ts.map +0 -1
- package/dist/commands/legacy-validation.js.map +0 -1
- package/dist/commands/one.d.ts.map +0 -1
- package/dist/commands/one.js.map +0 -1
- package/dist/commands/public-tools.d.ts.map +0 -1
- package/dist/commands/public-tools.js.map +0 -1
- package/dist/commands/runs.d.ts.map +0 -1
- package/dist/commands/runs.js.map +0 -1
- package/dist/commands/security.d.ts.map +0 -1
- package/dist/commands/security.js.map +0 -1
- package/dist/commands/watchdog.d.ts.map +0 -1
- package/dist/commands/watchdog.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/banner.d.ts.map +0 -1
- package/dist/lib/banner.js.map +0 -1
- package/dist/lib/client.d.ts.map +0 -1
- package/dist/lib/client.js.map +0 -1
- package/dist/lib/cloudflare-client.d.ts.map +0 -1
- package/dist/lib/cloudflare-client.js.map +0 -1
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/format.d.ts.map +0 -1
- package/dist/lib/format.js.map +0 -1
- package/dist/lib/help.d.ts.map +0 -1
- package/dist/lib/help.js.map +0 -1
- package/dist/lib/network.d.ts.map +0 -1
- package/dist/lib/network.js.map +0 -1
- package/dist/lib/one-client.d.ts.map +0 -1
- package/dist/lib/one-client.js.map +0 -1
- package/dist/lib/one-operations.d.ts.map +0 -1
- package/dist/lib/one-operations.js.map +0 -1
- package/dist/lib/open-url.d.ts.map +0 -1
- package/dist/lib/open-url.js.map +0 -1
- package/dist/lib/progress.d.ts.map +0 -1
- package/dist/lib/progress.js.map +0 -1
- package/dist/lib/security-client.d.ts.map +0 -1
- package/dist/lib/security-client.js.map +0 -1
- package/dist/lib/skill-fetch.d.ts.map +0 -1
- package/dist/lib/skill-fetch.js.map +0 -1
- package/dist/lib/system-trust.d.ts.map +0 -1
- package/dist/lib/system-trust.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email verification in the CLI: the one command and `submit` start with it.
|
|
3
|
+
*
|
|
4
|
+
* The CLI asks for a work email (or takes `--email`) and POSTs it to
|
|
5
|
+
* `local-session/request`; vigilbase.com creates the checkup and emails a
|
|
6
|
+
* 6-digit code. The CLI asks for the code and POSTs it with the returned
|
|
7
|
+
* verification token to `local-session/verify`, which answers with the
|
|
8
|
+
* session code for the upload. There is no bot check on this path; the server
|
|
9
|
+
* accepts business addresses only and rate-limits per mailbox (`+tag`
|
|
10
|
+
* variants included), per domain, per IP (an IPv6 /64 counts as one) and
|
|
11
|
+
* overall.
|
|
12
|
+
*
|
|
13
|
+
* The address, the emailed code, the token and the session code stay in this
|
|
14
|
+
* process's memory. They are never printed (only the masked address the server
|
|
15
|
+
* returns, `j***@acme.com`), logged, written to disk, put in a URL or JSON
|
|
16
|
+
* output, or passed to cf or npm. The token and the code are kept only so the
|
|
17
|
+
* session can be renewed once when it expires during a long collection.
|
|
18
|
+
*/
|
|
19
|
+
import type { LocalEmailLocale } from './contract.js';
|
|
20
|
+
import { LocalCheckupError } from './errors.js';
|
|
21
|
+
import { type PromptIO } from './prompts.js';
|
|
22
|
+
import { type VerificationClient } from './submit-client.js';
|
|
23
|
+
export declare const EMAIL_PROMPT = "Work email for your results: ";
|
|
24
|
+
export declare const CODE_PROMPT = "Code from the email: ";
|
|
25
|
+
/** Addresses asked for before giving up (a malformed one, or one vigilbase.com does not accept). */
|
|
26
|
+
export declare const MAX_EMAIL_ATTEMPTS = 3;
|
|
27
|
+
/** Codes asked for before giving up; One locks a checkup after 5 wrong codes. */
|
|
28
|
+
export declare const MAX_CODE_ATTEMPTS = 5;
|
|
29
|
+
/**
|
|
30
|
+
* On a terminal, how often the code is asked for again after vigilbase.com did
|
|
31
|
+
* not answer the verify (twice: the client already sent it once more). These
|
|
32
|
+
* are not tries: only a wrong code uses one.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MAX_UNANSWERED_VERIFIES = 2;
|
|
35
|
+
/** One `@` with text on both sides, no spaces or control characters, at most 254 characters. */
|
|
36
|
+
export declare function isEmailShaped(value: string): boolean;
|
|
37
|
+
/** `--email <address>`: checked before anything runs; the value is never echoed. */
|
|
38
|
+
export declare function parseEmailFlag(value: string): string;
|
|
39
|
+
/** The six digits of a typed code; `123 456` and `123-456` are accepted too. Null for anything else. */
|
|
40
|
+
export declare function normalizeEmailCode(input: string): string | null;
|
|
41
|
+
/** The email language from LC_ALL, LC_MESSAGES or LANG (the first one set): `pt*` → pt-PT, `ar*` → ar, else en. */
|
|
42
|
+
export declare function emailLocale(env: NodeJS.ProcessEnv): LocalEmailLocale;
|
|
43
|
+
/**
|
|
44
|
+
* A verified email and the session code it earned. Private fields keep the
|
|
45
|
+
* token, the emailed code and the session code out of `JSON.stringify` and
|
|
46
|
+
* `util.inspect`.
|
|
47
|
+
*/
|
|
48
|
+
export declare class VerifiedEmail {
|
|
49
|
+
#private;
|
|
50
|
+
/** The masked address from vigilbase.com, safe to print. */
|
|
51
|
+
readonly requester: string;
|
|
52
|
+
constructor(client: VerificationClient, token: string, emailCode: string, session: {
|
|
53
|
+
code: string;
|
|
54
|
+
requester: string;
|
|
55
|
+
expiresAt: string;
|
|
56
|
+
});
|
|
57
|
+
/** The current session code: in memory only, sent only as the upload's bearer. */
|
|
58
|
+
get sessionCode(): string;
|
|
59
|
+
get expiresAt(): string;
|
|
60
|
+
/** True until `renew` has been used. */
|
|
61
|
+
get renewable(): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Once per run: verify again with the same token and code, and vigilbase.com
|
|
64
|
+
* mints the next session code for the same checkup (the previous one stops
|
|
65
|
+
* working). It fails once the emailed code expired or the checkup closed.
|
|
66
|
+
*/
|
|
67
|
+
renew(): Promise<void>;
|
|
68
|
+
toJSON(): {
|
|
69
|
+
requester: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* How the verification asks and reports. The default is the plain prompts
|
|
74
|
+
* (`Work email for your results:` and `Code from the email:`, a visible line or
|
|
75
|
+
* one line of piped stdin) and status lines through `say` and `notice`; the
|
|
76
|
+
* terminal UI brings its own inputs and spinners.
|
|
77
|
+
*/
|
|
78
|
+
export type EmailVerificationView = {
|
|
79
|
+
/** The address as entered; null when the input ended. */
|
|
80
|
+
askEmail(): Promise<string | null>;
|
|
81
|
+
/** The emailed code as entered; null when the input ended. */
|
|
82
|
+
askCode(): Promise<string | null>;
|
|
83
|
+
/** The code went to `sentTo` (the masked address) and is valid until `expiresAt`. */
|
|
84
|
+
codeSent(sentTo: string, expiresAt: string): void;
|
|
85
|
+
/** Why the next question is asked again. */
|
|
86
|
+
notice(line: string): void;
|
|
87
|
+
/** The code request to vigilbase.com. */
|
|
88
|
+
requesting<T>(work: () => Promise<T>): Promise<T>;
|
|
89
|
+
/** The code check at vigilbase.com. */
|
|
90
|
+
verifying<T>(work: () => Promise<T>): Promise<T>;
|
|
91
|
+
/**
|
|
92
|
+
* A rate-limited code request (VIGILBASE_RATE_LIMITED): true to ask for
|
|
93
|
+
* another address. Asked only without `--email` and while addresses are
|
|
94
|
+
* left; without it the error stops the run (exit 2).
|
|
95
|
+
*/
|
|
96
|
+
rateLimited?(error: LocalCheckupError): Promise<boolean>;
|
|
97
|
+
};
|
|
98
|
+
export type EmailVerificationOptions = {
|
|
99
|
+
client: VerificationClient;
|
|
100
|
+
io: PromptIO;
|
|
101
|
+
locale: LocalEmailLocale;
|
|
102
|
+
/** `--email`, already checked with `parseEmailFlag`; it is never asked for again. */
|
|
103
|
+
email?: string;
|
|
104
|
+
/** A status line ("We emailed a 6-digit code to …"). */
|
|
105
|
+
say(line: string): void;
|
|
106
|
+
/** Why a prompt is asked again (stderr). */
|
|
107
|
+
notice(line: string): void;
|
|
108
|
+
/** Replaces the plain prompts and lines built from `io`, `say` and `notice`. */
|
|
109
|
+
view?: EmailVerificationView;
|
|
110
|
+
};
|
|
111
|
+
/** vigilbase.com starts one checkup per address every 8 minutes (One's creation budget). No final period. */
|
|
112
|
+
export declare const SAME_ADDRESS_WAIT = "The same address can get a new code 8 minutes after the last one";
|
|
113
|
+
/**
|
|
114
|
+
* Request a code, then ask for it until vigilbase.com accepts it (up to 5
|
|
115
|
+
* tries; One locks the checkup after 5 wrong codes). A wrong address is asked
|
|
116
|
+
* for again (up to 3), except with `--email`. An expired verification and a
|
|
117
|
+
* rate limit stop with their own codes (VERIFICATION_EXPIRED, and
|
|
118
|
+
* VIGILBASE_RATE_LIMITED with the retry time, exit 2), unless the view offers
|
|
119
|
+
* another address for a rate limit. A verify vigilbase.com did not answer is
|
|
120
|
+
* sent once more by the client; if that fails too, a terminal asks for the
|
|
121
|
+
* code again (twice at most, without using a try), since a new code for the
|
|
122
|
+
* same address would be refused for 8 minutes.
|
|
123
|
+
*/
|
|
124
|
+
export declare function verifyEmail(options: EmailVerificationOptions): Promise<VerifiedEmail>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { LocalCheckupError } from './errors.js';
|
|
2
|
+
import { formatClock } from './output.js';
|
|
3
|
+
import { hasInteractiveInput, promptAnswer } from './prompts.js';
|
|
4
|
+
import { displayRequester } from './submit-client.js';
|
|
5
|
+
export const EMAIL_PROMPT = 'Work email for your results: ';
|
|
6
|
+
export const CODE_PROMPT = 'Code from the email: ';
|
|
7
|
+
/** Addresses asked for before giving up (a malformed one, or one vigilbase.com does not accept). */
|
|
8
|
+
export const MAX_EMAIL_ATTEMPTS = 3;
|
|
9
|
+
/** Codes asked for before giving up; One locks a checkup after 5 wrong codes. */
|
|
10
|
+
export const MAX_CODE_ATTEMPTS = 5;
|
|
11
|
+
/**
|
|
12
|
+
* On a terminal, how often the code is asked for again after vigilbase.com did
|
|
13
|
+
* not answer the verify (twice: the client already sent it once more). These
|
|
14
|
+
* are not tries: only a wrong code uses one.
|
|
15
|
+
*/
|
|
16
|
+
export const MAX_UNANSWERED_VERIFIES = 2;
|
|
17
|
+
const MAX_EMAIL_LENGTH = 254;
|
|
18
|
+
const EMAIL_SHAPE = /^[^\s@\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]+@[^\s@\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]+$/u;
|
|
19
|
+
/** One `@` with text on both sides, no spaces or control characters, at most 254 characters. */
|
|
20
|
+
export function isEmailShaped(value) {
|
|
21
|
+
return value.length <= MAX_EMAIL_LENGTH && EMAIL_SHAPE.test(value);
|
|
22
|
+
}
|
|
23
|
+
/** `--email <address>`: checked before anything runs; the value is never echoed. */
|
|
24
|
+
export function parseEmailFlag(value) {
|
|
25
|
+
const email = value.trim();
|
|
26
|
+
if (!isEmailShaped(email))
|
|
27
|
+
throw new LocalCheckupError('EMAIL_INVALID', '--email is not an email address.');
|
|
28
|
+
return email;
|
|
29
|
+
}
|
|
30
|
+
/** The six digits of a typed code; `123 456` and `123-456` are accepted too. Null for anything else. */
|
|
31
|
+
export function normalizeEmailCode(input) {
|
|
32
|
+
const digits = input.replace(/[\s-]/g, '');
|
|
33
|
+
return /^\d{6}$/.test(digits) ? digits : null;
|
|
34
|
+
}
|
|
35
|
+
/** The email language from LC_ALL, LC_MESSAGES or LANG (the first one set): `pt*` → pt-PT, `ar*` → ar, else en. */
|
|
36
|
+
export function emailLocale(env) {
|
|
37
|
+
const value = [env.LC_ALL, env.LC_MESSAGES, env.LANG].find((candidate) => candidate !== undefined && candidate.trim() !== '') ?? '';
|
|
38
|
+
const language = value.trim().toLowerCase();
|
|
39
|
+
return language.startsWith('pt') ? 'pt-PT' : language.startsWith('ar') ? 'ar' : 'en';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A verified email and the session code it earned. Private fields keep the
|
|
43
|
+
* token, the emailed code and the session code out of `JSON.stringify` and
|
|
44
|
+
* `util.inspect`.
|
|
45
|
+
*/
|
|
46
|
+
export class VerifiedEmail {
|
|
47
|
+
#client;
|
|
48
|
+
#token;
|
|
49
|
+
#emailCode;
|
|
50
|
+
#sessionCode;
|
|
51
|
+
#expiresAt;
|
|
52
|
+
#renewed = false;
|
|
53
|
+
/** The masked address from vigilbase.com, safe to print. */
|
|
54
|
+
requester;
|
|
55
|
+
constructor(client, token, emailCode, session) {
|
|
56
|
+
this.#client = client;
|
|
57
|
+
this.#token = token;
|
|
58
|
+
this.#emailCode = emailCode;
|
|
59
|
+
this.#sessionCode = session.code;
|
|
60
|
+
this.#expiresAt = session.expiresAt;
|
|
61
|
+
this.requester = displayRequester(session.requester);
|
|
62
|
+
}
|
|
63
|
+
/** The current session code: in memory only, sent only as the upload's bearer. */
|
|
64
|
+
get sessionCode() {
|
|
65
|
+
return this.#sessionCode;
|
|
66
|
+
}
|
|
67
|
+
get expiresAt() {
|
|
68
|
+
return this.#expiresAt;
|
|
69
|
+
}
|
|
70
|
+
/** True until `renew` has been used. */
|
|
71
|
+
get renewable() {
|
|
72
|
+
return !this.#renewed;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Once per run: verify again with the same token and code, and vigilbase.com
|
|
76
|
+
* mints the next session code for the same checkup (the previous one stops
|
|
77
|
+
* working). It fails once the emailed code expired or the checkup closed.
|
|
78
|
+
*/
|
|
79
|
+
async renew() {
|
|
80
|
+
if (this.#renewed)
|
|
81
|
+
throw new LocalCheckupError('SESSION_EXPIRED', 'The checkup session expired again after it was renewed.');
|
|
82
|
+
this.#renewed = true;
|
|
83
|
+
const session = await this.#client.verifyCode({ verification: this.#token, code: this.#emailCode });
|
|
84
|
+
this.#sessionCode = session.code;
|
|
85
|
+
this.#expiresAt = session.expiresAt;
|
|
86
|
+
}
|
|
87
|
+
toJSON() {
|
|
88
|
+
return { requester: this.requester };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The plain prompts and status lines. */
|
|
92
|
+
function plainView(options) {
|
|
93
|
+
return {
|
|
94
|
+
askEmail: () => promptAnswer(options.io, EMAIL_PROMPT),
|
|
95
|
+
askCode: () => promptAnswer(options.io, CODE_PROMPT),
|
|
96
|
+
codeSent: (sentTo, expiresAt) => options.say(`We emailed a 6-digit code to ${sentTo} (valid until ${formatClock(expiresAt)}). Check spam if it doesn't arrive.`),
|
|
97
|
+
notice: (line) => options.notice(line),
|
|
98
|
+
requesting: (work) => work(),
|
|
99
|
+
verifying: (work) => work(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const PIPE_HINT = 'Run the command in a terminal, or pass --email <address> and pipe the code from the email to it once it asks (`Code from the email:` on stderr).';
|
|
103
|
+
/** vigilbase.com starts one checkup per address every 8 minutes (One's creation budget). No final period. */
|
|
104
|
+
export const SAME_ADDRESS_WAIT = 'The same address can get a new code 8 minutes after the last one';
|
|
105
|
+
/** The input ended (Ctrl-D on a terminal, or the end of piped stdin) before an answer. */
|
|
106
|
+
function noAnswer(options, what) {
|
|
107
|
+
const hint = !hasInteractiveInput(options.io.stdin) ? PIPE_HINT
|
|
108
|
+
: what === 'code' ? `Run the same command again. ${SAME_ADDRESS_WAIT}.` : 'Run the same command again and enter your work email.';
|
|
109
|
+
return new LocalCheckupError('CONFIRMATION_REQUIRED', `No ${what} was entered, and the input ended.`, { hint });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Ask for the address until vigilbase.com accepts one and emails a code (up to
|
|
113
|
+
* 3 addresses). A rate limit stops the run, unless the view offers another
|
|
114
|
+
* address and the user takes it.
|
|
115
|
+
*/
|
|
116
|
+
async function requestCode(options, view) {
|
|
117
|
+
for (let attempt = 1;; attempt++) {
|
|
118
|
+
let email = options.email;
|
|
119
|
+
if (email === undefined) {
|
|
120
|
+
const answer = (await view.askEmail())?.trim() ?? null;
|
|
121
|
+
if (answer === null)
|
|
122
|
+
throw noAnswer(options, 'email address');
|
|
123
|
+
if (!isEmailShaped(answer)) {
|
|
124
|
+
if (attempt >= MAX_EMAIL_ATTEMPTS)
|
|
125
|
+
throw new LocalCheckupError('EMAIL_INVALID', 'That is not an email address.');
|
|
126
|
+
view.notice('That is not an email address. Enter your work email.');
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
email = answer;
|
|
130
|
+
}
|
|
131
|
+
const address = email;
|
|
132
|
+
try {
|
|
133
|
+
return await view.requesting(() => options.client.requestCode({ email: address, locale: options.locale }));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (!(error instanceof LocalCheckupError))
|
|
137
|
+
throw error;
|
|
138
|
+
if (error.code === 'VIGILBASE_RATE_LIMITED') {
|
|
139
|
+
if (options.email === undefined && attempt < MAX_EMAIL_ATTEMPTS && view.rateLimited && await view.rateLimited(error))
|
|
140
|
+
continue;
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (error.code !== 'EMAIL_INVALID')
|
|
144
|
+
throw error;
|
|
145
|
+
if (options.email !== undefined) {
|
|
146
|
+
throw new LocalCheckupError('EMAIL_INVALID', 'vigilbase.com does not accept the --email address: use your work email (not a personal or disposable one).', { status: error.status });
|
|
147
|
+
}
|
|
148
|
+
if (attempt >= MAX_EMAIL_ATTEMPTS)
|
|
149
|
+
throw error;
|
|
150
|
+
view.notice('vigilbase.com accepts work email addresses only (not personal or disposable ones). Enter another address.');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** No answer to the verify (a network error or a timeout), or a 503: the code may be sent again. */
|
|
155
|
+
function isUnanswered(error) {
|
|
156
|
+
return error.code === 'SUBMIT_FAILED' && (error.status === 0 || error.status === 503);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Request a code, then ask for it until vigilbase.com accepts it (up to 5
|
|
160
|
+
* tries; One locks the checkup after 5 wrong codes). A wrong address is asked
|
|
161
|
+
* for again (up to 3), except with `--email`. An expired verification and a
|
|
162
|
+
* rate limit stop with their own codes (VERIFICATION_EXPIRED, and
|
|
163
|
+
* VIGILBASE_RATE_LIMITED with the retry time, exit 2), unless the view offers
|
|
164
|
+
* another address for a rate limit. A verify vigilbase.com did not answer is
|
|
165
|
+
* sent once more by the client; if that fails too, a terminal asks for the
|
|
166
|
+
* code again (twice at most, without using a try), since a new code for the
|
|
167
|
+
* same address would be refused for 8 minutes.
|
|
168
|
+
*/
|
|
169
|
+
export async function verifyEmail(options) {
|
|
170
|
+
const view = options.view ?? plainView(options);
|
|
171
|
+
const pending = await requestCode(options, view);
|
|
172
|
+
view.codeSent(displayRequester(pending.requester, 'the address you entered'), pending.expiresAt);
|
|
173
|
+
let unanswered = 0;
|
|
174
|
+
for (let attempt = 1;; attempt++) {
|
|
175
|
+
const left = MAX_CODE_ATTEMPTS - attempt;
|
|
176
|
+
const answer = await view.askCode();
|
|
177
|
+
if (answer === null)
|
|
178
|
+
throw noAnswer(options, 'code');
|
|
179
|
+
const code = normalizeEmailCode(answer);
|
|
180
|
+
if (code === null) {
|
|
181
|
+
if (left <= 0)
|
|
182
|
+
throw new LocalCheckupError('EMAIL_CODE_INVALID', 'No valid 6-digit code was entered.');
|
|
183
|
+
view.notice(`Enter the 6 digits from the email (${left} ${left === 1 ? 'try' : 'tries'} left).`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
// The client already sent a verify that got no answer, or a 503, once more.
|
|
188
|
+
const session = await view.verifying(() => options.client.verifyCode({ verification: pending.token, code }));
|
|
189
|
+
return new VerifiedEmail(options.client, pending.token, code, session);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error instanceof LocalCheckupError && isUnanswered(error) && unanswered < MAX_UNANSWERED_VERIFIES && hasInteractiveInput(options.io.stdin)) {
|
|
193
|
+
unanswered++;
|
|
194
|
+
// Not one of the 5 tries: the same code is still valid, and asking again costs nothing.
|
|
195
|
+
attempt--;
|
|
196
|
+
view.notice('vigilbase.com did not answer. Enter the same code again to retry.');
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (error instanceof LocalCheckupError && error.code === 'SUBMIT_FAILED') {
|
|
200
|
+
// A new code now would be refused for this address: say how long to wait.
|
|
201
|
+
throw new LocalCheckupError('SUBMIT_FAILED', error.message, {
|
|
202
|
+
hint: `Run the same command again in a few minutes. ${SAME_ADDRESS_WAIT}.`,
|
|
203
|
+
...(error.status !== undefined ? { status: error.status } : {}),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (!(error instanceof LocalCheckupError) || error.code !== 'EMAIL_CODE_INVALID' || left <= 0)
|
|
207
|
+
throw error;
|
|
208
|
+
view.notice(`That code is not correct (${left} ${left === 1 ? 'try' : 'tries'} left).`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const LOCAL_ERROR_CODES: readonly ["NODE_UNSUPPORTED", "CF_NOT_FOUND", "CF_WRONG_BINARY", "CF_UNSUPPORTED_VERSION", "CF_ALLOWLIST_MISMATCH", "CF_INSTALL_FAILED", "CF_AUTH_REQUIRED", "CF_PERMISSION_DENIED", "CF_RATE_LIMITED", "CF_ERROR", "ACCOUNT_SELECTION_REQUIRED", "ACCOUNT_NOT_VISIBLE", "ACCOUNT_TOO_LARGE", "ACCOUNT_MISMATCH", "OUTPUT_EXISTS", "SNAPSHOT_INVALID", "SNAPSHOT_TOO_LARGE", "SNAPSHOT_STALE", "SNAPSHOT_SECRET_LIKE_VALUE", "EMAIL_INVALID", "EMAIL_CODE_INVALID", "VERIFICATION_INVALID", "VERIFICATION_EXPIRED", "SESSION_INVALID", "SESSION_EXPIRED", "SESSION_USED", "SESSION_BUSY", "SESSION_CLOSED", "SUBMIT_ORIGIN_UNTRUSTED", "CONFIRMATION_REQUIRED", "CLI_UPGRADE_REQUIRED", "LOCAL_MODE_DISABLED", "VIGILBASE_RATE_LIMITED", "VIGILBASE_CHALLENGED", "SUBMIT_FAILED"];
|
|
2
|
+
export type LocalCheckupErrorCode = (typeof LOCAL_ERROR_CODES)[number];
|
|
3
|
+
/** LOCAL_MODE_DISABLED: the browser checkup on the origin the CLI talks to (vigilbase.com, or a loopback dev server). */
|
|
4
|
+
export declare function browserCheckupHint(origin: string): string;
|
|
5
|
+
/** One fix per code; shown after the error and in `--json` as `hint`. */
|
|
6
|
+
export declare const LOCAL_ERROR_HINTS: Record<LocalCheckupErrorCode, string>;
|
|
7
|
+
/**
|
|
8
|
+
* CF_AUTH_REQUIRED in env mode (`--cf-auth env`, or `auto` with the variable
|
|
9
|
+
* set): the browser sign-in is never offered while the token is used, so the
|
|
10
|
+
* hint names both ways out.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ENV_TOKEN_AUTH_HINT = "Set CLOUDFLARE_API_TOKEN in this shell to a valid read-only Cloudflare API token (read -rs CLOUDFLARE_API_TOKEN && export CLOUDFLARE_API_TOKEN) and run the same command again, or unset it (or use --cf-auth profile) to sign in with your browser instead.";
|
|
13
|
+
/** CF_PERMISSION_DENIED in env mode: the token lacks a read permission; the browser sign-in is the other way out. */
|
|
14
|
+
export declare const ENV_TOKEN_PERMISSION_HINT = "The API token in CLOUDFLARE_API_TOKEN cannot read everything the checkup needs. Add the read permissions to it, or unset it (or use --cf-auth profile) to sign in with your browser instead, then run the same command again.";
|
|
15
|
+
export declare class LocalCheckupError extends Error {
|
|
16
|
+
readonly code: LocalCheckupErrorCode;
|
|
17
|
+
readonly hint: string;
|
|
18
|
+
/** HTTP status of the Cloudflare or Vigilbase response, when one caused the error. */
|
|
19
|
+
readonly status?: number;
|
|
20
|
+
readonly retryAfterSec?: number;
|
|
21
|
+
constructor(code: LocalCheckupErrorCode, message: string, options?: {
|
|
22
|
+
hint?: string;
|
|
23
|
+
status?: number;
|
|
24
|
+
retryAfterSec?: number;
|
|
25
|
+
});
|
|
26
|
+
/** Mapped to exit code 2 by `runCli`. */
|
|
27
|
+
get isRateLimited(): boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function isLocalCheckupError(error: unknown): error is LocalCheckupError;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable error codes for `vigilbase cloudflare-checkup` (spec §F8).
|
|
3
|
+
*
|
|
4
|
+
* Every code matches `formatError`'s code pattern. Messages and hints are
|
|
5
|
+
* authored here; cf stderr and Vigilbase server text are never echoed. Hints
|
|
6
|
+
* point at the one command ("run the same command again", `--yes`,
|
|
7
|
+
* `--account-id`), not at the advanced subcommands. The browser
|
|
8
|
+
* troubleshooting accordion mirrors the code set.
|
|
9
|
+
*/
|
|
10
|
+
import { CF_TOKEN_ENV } from './cf/child-env.js';
|
|
11
|
+
import { PRODUCTION_ORIGIN } from './contract.js';
|
|
12
|
+
export const LOCAL_ERROR_CODES = [
|
|
13
|
+
// Environment and cf
|
|
14
|
+
'NODE_UNSUPPORTED', 'CF_NOT_FOUND', 'CF_WRONG_BINARY', 'CF_UNSUPPORTED_VERSION', 'CF_ALLOWLIST_MISMATCH',
|
|
15
|
+
'CF_INSTALL_FAILED', 'CF_AUTH_REQUIRED', 'CF_PERMISSION_DENIED', 'CF_RATE_LIMITED', 'CF_ERROR',
|
|
16
|
+
// Account
|
|
17
|
+
'ACCOUNT_SELECTION_REQUIRED', 'ACCOUNT_NOT_VISIBLE', 'ACCOUNT_TOO_LARGE', 'ACCOUNT_MISMATCH',
|
|
18
|
+
// Files and snapshots
|
|
19
|
+
'OUTPUT_EXISTS', 'SNAPSHOT_INVALID', 'SNAPSHOT_TOO_LARGE', 'SNAPSHOT_STALE', 'SNAPSHOT_SECRET_LIKE_VALUE',
|
|
20
|
+
// Email verification
|
|
21
|
+
'EMAIL_INVALID', 'EMAIL_CODE_INVALID', 'VERIFICATION_INVALID', 'VERIFICATION_EXPIRED',
|
|
22
|
+
// Session
|
|
23
|
+
'SESSION_INVALID', 'SESSION_EXPIRED', 'SESSION_USED', 'SESSION_BUSY', 'SESSION_CLOSED',
|
|
24
|
+
// Submission
|
|
25
|
+
'SUBMIT_ORIGIN_UNTRUSTED', 'CONFIRMATION_REQUIRED', 'CLI_UPGRADE_REQUIRED', 'LOCAL_MODE_DISABLED',
|
|
26
|
+
'VIGILBASE_RATE_LIMITED', 'VIGILBASE_CHALLENGED', 'SUBMIT_FAILED',
|
|
27
|
+
];
|
|
28
|
+
const INSTALL_CF = '`npm install --global cf@1.0.0-beta.13`';
|
|
29
|
+
/** LOCAL_MODE_DISABLED: the browser checkup on the origin the CLI talks to (vigilbase.com, or a loopback dev server). */
|
|
30
|
+
export function browserCheckupHint(origin) {
|
|
31
|
+
return `Run the Cloudflare Checkup in your browser instead: ${origin}/tools/cloudflare-checkup`;
|
|
32
|
+
}
|
|
33
|
+
/** One fix per code; shown after the error and in `--json` as `hint`. */
|
|
34
|
+
export const LOCAL_ERROR_HINTS = {
|
|
35
|
+
NODE_UNSUPPORTED: 'Install Node.js 22.18 or newer from https://nodejs.org (cf runs under the same Node), then run the same command again.',
|
|
36
|
+
CF_NOT_FOUND: `Run the same command again in a terminal and let it install cf (add --yes when no terminal is available), or install it yourself with ${INSTALL_CF}.`,
|
|
37
|
+
CF_WRONG_BINARY: `Another program named cf (for example the Cloud Foundry CLI, or a pnpm or Volta shim) was found. Run the same command again without --cf-bin and let it install cf, or pass --cf-bin "$(npm root -g)/cf".`,
|
|
38
|
+
CF_UNSUPPORTED_VERSION: `Run the same command again without --cf-bin and let it install the tested cf 1.0.0-beta.13, or install it yourself with ${INSTALL_CF}.`,
|
|
39
|
+
// Without --cf-bin the one command offers the managed install itself; the user's own cf is left alone.
|
|
40
|
+
CF_ALLOWLIST_MISMATCH: 'This cf release renamed commands the checkup needs. Run the same command again without --cf-bin and let it install the tested cf 1.0.0-beta.13 for Vigilbase; your own cf is left alone.',
|
|
41
|
+
CF_INSTALL_FAILED: `Install cf yourself with ${INSTALL_CF}, then run the same command again.`,
|
|
42
|
+
CF_AUTH_REQUIRED: `Run the same command again in a terminal to sign in to Cloudflare with a read-only login, or set ${CF_TOKEN_ENV} in this shell first.`,
|
|
43
|
+
CF_PERMISSION_DENIED: 'Your Cloudflare login cannot read this account\'s zones. Check its permissions, then run the same command again.',
|
|
44
|
+
CF_RATE_LIMITED: 'Cloudflare paused your API access. Wait 5 minutes, then run the same command again; add --max-requests-per-5min 600 if it repeats.',
|
|
45
|
+
CF_ERROR: 'Run the same command again in a few minutes. If it persists, contact help@vigilbase.com.',
|
|
46
|
+
ACCOUNT_SELECTION_REQUIRED: 'Run the same command again in a terminal to choose the account, or add --account-id <ACCOUNT_ID> with one of the IDs listed.',
|
|
47
|
+
ACCOUNT_NOT_VISIBLE: 'Check that your Cloudflare user can read the account and its zones, then run the same command again.',
|
|
48
|
+
ACCOUNT_TOO_LARGE: 'Local collection supports up to 100 zones. Use the browser token mode on vigilbase.com instead.',
|
|
49
|
+
ACCOUNT_MISMATCH: 'This checkup is for a different Cloudflare account. Run the same command again without --account-id to start a new checkup.',
|
|
50
|
+
OUTPUT_EXISTS: 'Choose another --out path, or pass --overwrite to replace the file.',
|
|
51
|
+
SNAPSHOT_INVALID: 'The file is not a valid snapshot. Run the same command again to collect a fresh one.',
|
|
52
|
+
SNAPSHOT_TOO_LARGE: 'The snapshot is over the upload limit. Use the browser token mode on vigilbase.com instead.',
|
|
53
|
+
SNAPSHOT_STALE: 'Snapshots are accepted for 24 hours. Run the same command again to collect a fresh one.',
|
|
54
|
+
SNAPSHOT_SECRET_LIKE_VALUE: 'The snapshot contains a value that looks like a credential. Remove it from the Cloudflare rule description or name, then run the same command again.',
|
|
55
|
+
EMAIL_INVALID: 'Use your work email address (personal and disposable addresses are not accepted), then run the same command again.',
|
|
56
|
+
EMAIL_CODE_INVALID: 'Run the same command again to get a new code, and enter the 6 digits from the newest email.',
|
|
57
|
+
VERIFICATION_INVALID: 'Run the same command again to get a new code by email.',
|
|
58
|
+
VERIFICATION_EXPIRED: 'Run the same command again to get a new code by email, and enter it before the time shown.',
|
|
59
|
+
SESSION_INVALID: 'Run the same command again to verify your email and start a new checkup.',
|
|
60
|
+
SESSION_EXPIRED: 'Run the same command again to verify your email and start a new checkup.',
|
|
61
|
+
SESSION_USED: 'This checkup already has results. Run the same command again to start a new checkup.',
|
|
62
|
+
SESSION_BUSY: 'A snapshot for this checkup is still being processed. Wait for the results email, or run the same command again to start a new checkup.',
|
|
63
|
+
SESSION_CLOSED: 'This checkup was cancelled or has expired. Run the same command again to start a new one.',
|
|
64
|
+
SUBMIT_ORIGIN_UNTRUSTED: 'Upload only to https://vigilbase.com (or a loopback origin for local testing).',
|
|
65
|
+
CONFIRMATION_REQUIRED: 'Run the command in a terminal to confirm the upload, or add --yes.',
|
|
66
|
+
CLI_UPGRADE_REQUIRED: 'Run `npx @vigilbase/cli@latest cloudflare-checkup`, or upgrade with `npm install --global @vigilbase/cli@latest`.',
|
|
67
|
+
LOCAL_MODE_DISABLED: browserCheckupHint(PRODUCTION_ORIGIN),
|
|
68
|
+
VIGILBASE_RATE_LIMITED: 'Too many requests to vigilbase.com. Wait as long as the error says (a few minutes if it says nothing), then run the same command again.',
|
|
69
|
+
VIGILBASE_CHALLENGED: 'vigilbase.com asked for a browser challenge. Try another network, or contact help@vigilbase.com.',
|
|
70
|
+
SUBMIT_FAILED: 'Run the same command again in a few minutes. If it persists, contact help@vigilbase.com.',
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* CF_AUTH_REQUIRED in env mode (`--cf-auth env`, or `auto` with the variable
|
|
74
|
+
* set): the browser sign-in is never offered while the token is used, so the
|
|
75
|
+
* hint names both ways out.
|
|
76
|
+
*/
|
|
77
|
+
export const ENV_TOKEN_AUTH_HINT = `Set ${CF_TOKEN_ENV} in this shell to a valid read-only Cloudflare API token (read -rs ${CF_TOKEN_ENV} && export ${CF_TOKEN_ENV}) and run the same command again, or unset it (or use --cf-auth profile) to sign in with your browser instead.`;
|
|
78
|
+
/** CF_PERMISSION_DENIED in env mode: the token lacks a read permission; the browser sign-in is the other way out. */
|
|
79
|
+
export const ENV_TOKEN_PERMISSION_HINT = `The API token in ${CF_TOKEN_ENV} cannot read everything the checkup needs. Add the read permissions to it, or unset it (or use --cf-auth profile) to sign in with your browser instead, then run the same command again.`;
|
|
80
|
+
export class LocalCheckupError extends Error {
|
|
81
|
+
code;
|
|
82
|
+
hint;
|
|
83
|
+
/** HTTP status of the Cloudflare or Vigilbase response, when one caused the error. */
|
|
84
|
+
status;
|
|
85
|
+
retryAfterSec;
|
|
86
|
+
constructor(code, message, options = {}) {
|
|
87
|
+
super(message);
|
|
88
|
+
this.name = 'LocalCheckupError';
|
|
89
|
+
this.code = code;
|
|
90
|
+
this.hint = options.hint ?? LOCAL_ERROR_HINTS[code];
|
|
91
|
+
if (options.status !== undefined)
|
|
92
|
+
this.status = options.status;
|
|
93
|
+
if (options.retryAfterSec !== undefined)
|
|
94
|
+
this.retryAfterSec = options.retryAfterSec;
|
|
95
|
+
}
|
|
96
|
+
/** Mapped to exit code 2 by `runCli`. */
|
|
97
|
+
get isRateLimited() {
|
|
98
|
+
return this.code === 'CF_RATE_LIMITED' || this.code === 'VIGILBASE_RATE_LIMITED';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function isLocalCheckupError(error) {
|
|
102
|
+
return error instanceof LocalCheckupError;
|
|
103
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EntryCounts } from './snapshot-file.js';
|
|
2
|
+
/**
|
|
3
|
+
* Untrusted single-line text (account names from cf): controls and line
|
|
4
|
+
* breaks collapsed, length bounded. The terminal UI passes its `ellipsis`
|
|
5
|
+
* (`...` on an ASCII-only console).
|
|
6
|
+
*/
|
|
7
|
+
export declare function displayText(value: string, max: number, ellipsis?: string): string;
|
|
8
|
+
/** `0123…cdef` (`0123...cdef` with the ASCII `ellipsis` of the terminal UI). */
|
|
9
|
+
export declare function maskId(id: string, ellipsis?: string): string;
|
|
10
|
+
/** `9f2c…a71e` */
|
|
11
|
+
export declare function shortSha(sha256: string): string;
|
|
12
|
+
export declare function formatBytes(bytes: number): string;
|
|
13
|
+
/** Local `HH:MM` for an ISO timestamp. */
|
|
14
|
+
export declare function formatClock(iso: string): string;
|
|
15
|
+
export declare function formatDuration(seconds: number): string;
|
|
16
|
+
/** `3 reads denied (HTTP 403) · 60 reads not collectable with cf` */
|
|
17
|
+
export declare function readsSummary(counts: EntryCounts, denied: number): string;
|
|
18
|
+
export type CountsProgress = {
|
|
19
|
+
update(text: string): void;
|
|
20
|
+
stop(): void;
|
|
21
|
+
};
|
|
22
|
+
/** A single updating stderr line, or nothing when progress is disabled. */
|
|
23
|
+
export declare function startCountsProgress(enabled: boolean, stream?: NodeJS.WritableStream): CountsProgress;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal formatting for local checkups. Progress shows counts only, never
|
|
3
|
+
* account or zone names, and is written to stderr. Every other line goes
|
|
4
|
+
* through the CLI writer, which strips terminal controls from untrusted text.
|
|
5
|
+
*/
|
|
6
|
+
import { sanitizeTerminal } from '../../lib/format.js';
|
|
7
|
+
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
8
|
+
/**
|
|
9
|
+
* Untrusted single-line text (account names from cf): controls and line
|
|
10
|
+
* breaks collapsed, length bounded. The terminal UI passes its `ellipsis`
|
|
11
|
+
* (`...` on an ASCII-only console).
|
|
12
|
+
*/
|
|
13
|
+
export function displayText(value, max, ellipsis = '…') {
|
|
14
|
+
const flat = sanitizeTerminal(value.replace(/[\s\u0085\u2028\u2029]+/g, ' ')).trim();
|
|
15
|
+
return flat.length > max ? `${flat.slice(0, Math.max(0, max - ellipsis.length))}${ellipsis}` : flat;
|
|
16
|
+
}
|
|
17
|
+
/** `0123…cdef` (`0123...cdef` with the ASCII `ellipsis` of the terminal UI). */
|
|
18
|
+
export function maskId(id, ellipsis = '…') {
|
|
19
|
+
return id.length > 8 ? `${id.slice(0, 4)}${ellipsis}${id.slice(-4)}` : id;
|
|
20
|
+
}
|
|
21
|
+
/** `9f2c…a71e` */
|
|
22
|
+
export function shortSha(sha256) {
|
|
23
|
+
return `${sha256.slice(0, 4)}…${sha256.slice(-4)}`;
|
|
24
|
+
}
|
|
25
|
+
export function formatBytes(bytes) {
|
|
26
|
+
if (bytes < 1024)
|
|
27
|
+
return `${bytes} B`;
|
|
28
|
+
if (bytes < 1024 * 1024)
|
|
29
|
+
return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
|
30
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
31
|
+
}
|
|
32
|
+
/** Local `HH:MM` for an ISO timestamp. */
|
|
33
|
+
export function formatClock(iso) {
|
|
34
|
+
const date = new Date(iso);
|
|
35
|
+
if (Number.isNaN(date.getTime()))
|
|
36
|
+
return iso;
|
|
37
|
+
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
|
38
|
+
}
|
|
39
|
+
export function formatDuration(seconds) {
|
|
40
|
+
if (seconds < 90)
|
|
41
|
+
return `~${Math.max(1, Math.round(seconds))} s`;
|
|
42
|
+
return `~${Math.round(seconds / 60)} min`;
|
|
43
|
+
}
|
|
44
|
+
/** `3 reads denied (HTTP 403) · 60 reads not collectable with cf` */
|
|
45
|
+
export function readsSummary(counts, denied) {
|
|
46
|
+
const parts = [];
|
|
47
|
+
if (denied > 0)
|
|
48
|
+
parts.push(`${denied} ${denied === 1 ? 'read' : 'reads'} denied (HTTP 403)`);
|
|
49
|
+
const otherHttp = counts.httpError - denied;
|
|
50
|
+
if (otherHttp > 0)
|
|
51
|
+
parts.push(`${otherHttp} ${otherHttp === 1 ? 'read' : 'reads'} failed with another HTTP error`);
|
|
52
|
+
if (counts.collectionFailed > 0)
|
|
53
|
+
parts.push(`${counts.collectionFailed} ${counts.collectionFailed === 1 ? 'read' : 'reads'} could not be collected`);
|
|
54
|
+
if (counts.notCollected > 0)
|
|
55
|
+
parts.push(`${counts.notCollected} reads not collectable with cf`);
|
|
56
|
+
return parts.join(' · ');
|
|
57
|
+
}
|
|
58
|
+
/** A single updating stderr line, or nothing when progress is disabled. */
|
|
59
|
+
export function startCountsProgress(enabled, stream = process.stderr) {
|
|
60
|
+
if (!enabled)
|
|
61
|
+
return { update: () => undefined, stop: () => undefined };
|
|
62
|
+
let text = '';
|
|
63
|
+
let frame = 0;
|
|
64
|
+
let closed = false;
|
|
65
|
+
let width = 0;
|
|
66
|
+
const draw = () => {
|
|
67
|
+
if (closed)
|
|
68
|
+
return;
|
|
69
|
+
const line = `${SPINNER[frame++ % SPINNER.length]} ${sanitizeTerminal(text).replace(/[\n\t]/g, ' ')}`;
|
|
70
|
+
width = Math.max(width, line.length);
|
|
71
|
+
stream.write(`\r${line.padEnd(width)}`);
|
|
72
|
+
};
|
|
73
|
+
const timer = setInterval(draw, 80);
|
|
74
|
+
timer.unref?.();
|
|
75
|
+
return {
|
|
76
|
+
update(next) {
|
|
77
|
+
text = next;
|
|
78
|
+
draw();
|
|
79
|
+
},
|
|
80
|
+
stop() {
|
|
81
|
+
if (closed)
|
|
82
|
+
return;
|
|
83
|
+
closed = true;
|
|
84
|
+
clearInterval(timer);
|
|
85
|
+
stream.write(`\r${' '.repeat(width)}\r`);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** One piped answer is at most this long (an email address is at most 254 bytes). */
|
|
2
|
+
export declare const MAX_ANSWER_BYTES = 256;
|
|
3
|
+
export type PromptIO = {
|
|
4
|
+
stdin?: NodeJS.ReadableStream;
|
|
5
|
+
stderr: NodeJS.WritableStream;
|
|
6
|
+
};
|
|
7
|
+
export declare function hasInteractiveInput(stdin: NodeJS.ReadableStream | undefined): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* A visible one-line prompt on a TTY (account picker, confirmations): the
|
|
10
|
+
* trimmed answer, or null when the input ended (Ctrl-D) before one.
|
|
11
|
+
*/
|
|
12
|
+
export declare function promptLine(io: PromptIO, label: string): Promise<string | null>;
|
|
13
|
+
/**
|
|
14
|
+
* One line of piped input, without consuming the next one: whatever follows
|
|
15
|
+
* the newline is pushed back onto the stream for the next prompt. Bytes past
|
|
16
|
+
* `MAX_ANSWER_BYTES` are dropped up to the newline, so an overlong line comes
|
|
17
|
+
* back too long and fails validation. Null at the end of the input.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readPipedLine(stdin: NodeJS.ReadableStream): Promise<string | null>;
|
|
20
|
+
/**
|
|
21
|
+
* A visible prompt for the email verification. On a terminal it is
|
|
22
|
+
* `promptLine`; without one the label still goes to stderr and the answer is
|
|
23
|
+
* one line of piped stdin. Null when the input ended without an answer.
|
|
24
|
+
*/
|
|
25
|
+
export declare function promptAnswer(io: PromptIO, label: string): Promise<string | null>;
|
|
26
|
+
/**
|
|
27
|
+
* `[y/N]` upload confirmation: an explicit `y` is required. `--yes` (or its
|
|
28
|
+
* alias `--confirm`) answers yes; without a terminal and without them it is
|
|
29
|
+
* CONFIRMATION_REQUIRED.
|
|
30
|
+
*/
|
|
31
|
+
export declare function confirmUpload(io: PromptIO, question: string, options: {
|
|
32
|
+
isConfirmed: boolean;
|
|
33
|
+
flag?: string;
|
|
34
|
+
}): Promise<boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* `[Y/n]` question where Enter means yes (installing cf). The caller handles
|
|
37
|
+
* `--yes` and the no-terminal case; an input that ends before an answer is
|
|
38
|
+
* CONFIRMATION_REQUIRED.
|
|
39
|
+
*/
|
|
40
|
+
export declare function confirmDefaultYes(io: PromptIO, question: string): Promise<boolean>;
|