leak-cli 2026.2.17-beta.0 → 2026.2.17
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/.env.example +2 -0
- package/README.md +164 -17
- package/examples/multi-host.example.json +50 -0
- package/package.json +9 -4
- package/scripts/buy.js +224 -189
- package/scripts/cli.js +81 -14
- package/scripts/config.js +128 -28
- package/scripts/config_store.js +23 -0
- package/scripts/host.js +1131 -0
- package/scripts/leak.js +1240 -173
- package/scripts/ui.js +106 -0
- package/src/access_mode.js +51 -0
- package/src/download_code.js +91 -0
- package/src/index.js +275 -100
package/scripts/ui.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const ANSI_PREFIX = "\x1b[";
|
|
2
|
+
const ANSI_RESET = `${ANSI_PREFIX}0m`;
|
|
3
|
+
|
|
4
|
+
const COLOR_CODES = {
|
|
5
|
+
bold: "1",
|
|
6
|
+
dim: "2",
|
|
7
|
+
red: "31",
|
|
8
|
+
green: "32",
|
|
9
|
+
yellow: "33",
|
|
10
|
+
blue: "34",
|
|
11
|
+
cyan: "36",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function supportsColor(stream = process.stdout) {
|
|
15
|
+
const noColorSet = Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
|
|
16
|
+
if (noColorSet) return false;
|
|
17
|
+
return Boolean(stream?.isTTY);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function supportsHyperlinks(stream = process.stdout) {
|
|
21
|
+
const noColorSet = Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
|
|
22
|
+
if (noColorSet) return false;
|
|
23
|
+
if (!stream?.isTTY) return false;
|
|
24
|
+
const term = String(process.env.TERM || "").toLowerCase();
|
|
25
|
+
if (!term || term === "dumb") return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function colorize(enabled, code, text) {
|
|
30
|
+
const value = String(text ?? "");
|
|
31
|
+
if (!enabled) return value;
|
|
32
|
+
return `${ANSI_PREFIX}${code}m${value}${ANSI_RESET}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeRows(rows) {
|
|
36
|
+
return rows
|
|
37
|
+
.map((row) => {
|
|
38
|
+
if (!row) return null;
|
|
39
|
+
if (Array.isArray(row)) return { key: row[0], value: row[1] };
|
|
40
|
+
return row;
|
|
41
|
+
})
|
|
42
|
+
.filter((row) => row && row.value !== undefined && row.value !== null && row.value !== "");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createUi(stream = process.stdout) {
|
|
46
|
+
const enabled = supportsColor(stream);
|
|
47
|
+
const hyperlinksEnabled = supportsHyperlinks(stream);
|
|
48
|
+
|
|
49
|
+
const heading = (text) => colorize(enabled, `${COLOR_CODES.bold};${COLOR_CODES.cyan}`, text);
|
|
50
|
+
const section = (text) => colorize(enabled, COLOR_CODES.bold, text);
|
|
51
|
+
const muted = (text) => colorize(enabled, COLOR_CODES.dim, text);
|
|
52
|
+
const key = (text) => colorize(enabled, COLOR_CODES.cyan, text);
|
|
53
|
+
const value = (text) => String(text ?? "");
|
|
54
|
+
|
|
55
|
+
const ok = (text) => colorize(enabled, COLOR_CODES.green, text);
|
|
56
|
+
const warn = (text) => colorize(enabled, COLOR_CODES.yellow, text);
|
|
57
|
+
const error = (text) => colorize(enabled, COLOR_CODES.red, text);
|
|
58
|
+
const info = (text) => colorize(enabled, COLOR_CODES.blue, text);
|
|
59
|
+
|
|
60
|
+
const statusLabel = (kind) => {
|
|
61
|
+
const normalized = String(kind || "info").toLowerCase();
|
|
62
|
+
if (normalized === "ok") return ok("[ok]");
|
|
63
|
+
if (normalized === "warn") return warn("[warn]");
|
|
64
|
+
if (normalized === "error") return error("[error]");
|
|
65
|
+
return info("[info]");
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const statusLine = (kind, text) => `${statusLabel(kind)} ${String(text ?? "")}`;
|
|
69
|
+
|
|
70
|
+
const formatRows = (rows, options = {}) => {
|
|
71
|
+
const { indent = " ", keySuffix = ":" } = options;
|
|
72
|
+
const normalized = normalizeRows(rows);
|
|
73
|
+
const width = normalized.reduce((max, row) => Math.max(max, String(row.key).length), 0);
|
|
74
|
+
return normalized.map((row) => {
|
|
75
|
+
const label = `${String(row.key).padEnd(width)}${keySuffix}`;
|
|
76
|
+
return `${indent}${key(label)} ${value(row.value)}`;
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const link = (url, label) => {
|
|
81
|
+
const href = String(url ?? "").trim();
|
|
82
|
+
const text = String(label ?? href);
|
|
83
|
+
if (!href) return text;
|
|
84
|
+
if (!hyperlinksEnabled) return text;
|
|
85
|
+
if (!/^https?:\/\//i.test(href)) return text;
|
|
86
|
+
return `\u001B]8;;${href}\u0007${text}\u001B]8;;\u0007`;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
enabled,
|
|
91
|
+
hyperlinksEnabled,
|
|
92
|
+
heading,
|
|
93
|
+
section,
|
|
94
|
+
muted,
|
|
95
|
+
key,
|
|
96
|
+
value,
|
|
97
|
+
ok,
|
|
98
|
+
warn,
|
|
99
|
+
error,
|
|
100
|
+
info,
|
|
101
|
+
statusLabel,
|
|
102
|
+
statusLine,
|
|
103
|
+
formatRows,
|
|
104
|
+
link,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const ACCESS_MODE_VALUES = Object.freeze([
|
|
2
|
+
"no-download-code-no-payment",
|
|
3
|
+
"download-code-only-no-payment",
|
|
4
|
+
"payment-only-no-download-code",
|
|
5
|
+
"download-code-and-payment",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_ACCESS_MODE = "payment-only-no-download-code";
|
|
9
|
+
|
|
10
|
+
const ACCESS_MODE_SET = new Set(ACCESS_MODE_VALUES);
|
|
11
|
+
|
|
12
|
+
export function normalizeAccessMode(value) {
|
|
13
|
+
return String(value || "").trim().toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isValidAccessMode(value) {
|
|
17
|
+
return ACCESS_MODE_SET.has(normalizeAccessMode(value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveAccessMode(value, fallback = DEFAULT_ACCESS_MODE) {
|
|
21
|
+
const normalized = normalizeAccessMode(value);
|
|
22
|
+
if (ACCESS_MODE_SET.has(normalized)) return normalized;
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function accessModeRequiresDownloadCode(mode) {
|
|
27
|
+
const normalized = normalizeAccessMode(mode);
|
|
28
|
+
return (
|
|
29
|
+
normalized === "download-code-only-no-payment" ||
|
|
30
|
+
normalized === "download-code-and-payment"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function accessModeRequiresPayment(mode) {
|
|
35
|
+
const normalized = normalizeAccessMode(mode);
|
|
36
|
+
return (
|
|
37
|
+
normalized === "payment-only-no-download-code" ||
|
|
38
|
+
normalized === "download-code-and-payment"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function accessModeSummary(mode) {
|
|
43
|
+
const normalized = normalizeAccessMode(mode);
|
|
44
|
+
if (normalized === "download-code-and-payment")
|
|
45
|
+
return "Download code and payment required";
|
|
46
|
+
if (normalized === "download-code-only-no-payment")
|
|
47
|
+
return "Download code required; no payment";
|
|
48
|
+
if (normalized === "payment-only-no-download-code")
|
|
49
|
+
return "Payment required; no download code";
|
|
50
|
+
return "No download code; no payment";
|
|
51
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { randomBytes, scrypt as scryptCallback, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const scrypt = promisify(scryptCallback);
|
|
5
|
+
|
|
6
|
+
export const DOWNLOAD_CODE_HEADER = "X-LEAK-DOWNLOAD-CODE";
|
|
7
|
+
|
|
8
|
+
const SCRYPT_PREFIX = "scrypt";
|
|
9
|
+
const SCRYPT_N = 16384;
|
|
10
|
+
const SCRYPT_R = 8;
|
|
11
|
+
const SCRYPT_P = 1;
|
|
12
|
+
const SCRYPT_KEYLEN = 32;
|
|
13
|
+
|
|
14
|
+
function assertHex(value, label) {
|
|
15
|
+
if (!/^[0-9a-f]+$/i.test(value) || value.length % 2 !== 0) {
|
|
16
|
+
throw new Error(`Invalid ${label} encoding`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeCode(value) {
|
|
21
|
+
return String(value || "").trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseDownloadCodeHash(encoded) {
|
|
25
|
+
const raw = String(encoded || "").trim();
|
|
26
|
+
const parts = raw.split("$");
|
|
27
|
+
if (parts.length !== 6) throw new Error("Invalid download code hash format");
|
|
28
|
+
if (parts[0] !== SCRYPT_PREFIX) throw new Error("Unsupported download code hash format");
|
|
29
|
+
|
|
30
|
+
const n = Number(parts[1]);
|
|
31
|
+
const r = Number(parts[2]);
|
|
32
|
+
const p = Number(parts[3]);
|
|
33
|
+
const saltHex = parts[4];
|
|
34
|
+
const digestHex = parts[5];
|
|
35
|
+
|
|
36
|
+
if (!Number.isFinite(n) || n <= 0) throw new Error("Invalid download code hash parameter: N");
|
|
37
|
+
if (!Number.isFinite(r) || r <= 0) throw new Error("Invalid download code hash parameter: r");
|
|
38
|
+
if (!Number.isFinite(p) || p <= 0) throw new Error("Invalid download code hash parameter: p");
|
|
39
|
+
assertHex(saltHex, "salt");
|
|
40
|
+
assertHex(digestHex, "digest");
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
n: Math.floor(n),
|
|
44
|
+
r: Math.floor(r),
|
|
45
|
+
p: Math.floor(p),
|
|
46
|
+
salt: Buffer.from(saltHex, "hex"),
|
|
47
|
+
digest: Buffer.from(digestHex, "hex"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isValidDownloadCodeHash(encoded) {
|
|
52
|
+
try {
|
|
53
|
+
parseDownloadCodeHash(encoded);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function hashDownloadCode(downloadCode) {
|
|
61
|
+
const normalized = normalizeCode(downloadCode);
|
|
62
|
+
if (!normalized) throw new Error("Download code cannot be empty");
|
|
63
|
+
|
|
64
|
+
const salt = randomBytes(16);
|
|
65
|
+
const digest = await scrypt(normalized, salt, SCRYPT_KEYLEN, {
|
|
66
|
+
N: SCRYPT_N,
|
|
67
|
+
r: SCRYPT_R,
|
|
68
|
+
p: SCRYPT_P,
|
|
69
|
+
maxmem: 64 * 1024 * 1024,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return `${SCRYPT_PREFIX}$${SCRYPT_N}$${SCRYPT_R}$${SCRYPT_P}$${salt.toString("hex")}$${Buffer.from(digest).toString("hex")}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function verifyDownloadCode(downloadCode, encodedHash) {
|
|
76
|
+
const normalized = normalizeCode(downloadCode);
|
|
77
|
+
if (!normalized) return false;
|
|
78
|
+
|
|
79
|
+
const parsed = parseDownloadCodeHash(encodedHash);
|
|
80
|
+
const actual = Buffer.from(
|
|
81
|
+
await scrypt(normalized, parsed.salt, parsed.digest.length, {
|
|
82
|
+
N: parsed.n,
|
|
83
|
+
r: parsed.r,
|
|
84
|
+
p: parsed.p,
|
|
85
|
+
maxmem: 64 * 1024 * 1024,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (actual.length !== parsed.digest.length) return false;
|
|
90
|
+
return timingSafeEqual(actual, parsed.digest);
|
|
91
|
+
}
|