anonli 1.0.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.
- package/README.md +165 -0
- package/bin/anonli.js +2 -0
- package/dist/index.js +3442 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3442 @@
|
|
|
1
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/types/config.ts
|
|
16
|
+
var DEFAULT_CONFIG;
|
|
17
|
+
var init_config = __esm({
|
|
18
|
+
"src/types/config.ts"() {
|
|
19
|
+
"use strict";
|
|
20
|
+
DEFAULT_CONFIG = {
|
|
21
|
+
baseUrl: "https://anon.li"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// src/lib/config.ts
|
|
27
|
+
import fs from "fs";
|
|
28
|
+
import os from "os";
|
|
29
|
+
import path from "path";
|
|
30
|
+
function getConfigPath() {
|
|
31
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
32
|
+
const base = xdg || path.join(os.homedir(), ".config");
|
|
33
|
+
return path.join(base, "anonli.json");
|
|
34
|
+
}
|
|
35
|
+
function loadConfig() {
|
|
36
|
+
const configPath = getConfigPath();
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
const config = { ...DEFAULT_CONFIG, ...parsed };
|
|
41
|
+
if (!config.baseUrl) {
|
|
42
|
+
config.baseUrl = DEFAULT_CONFIG.baseUrl;
|
|
43
|
+
}
|
|
44
|
+
return config;
|
|
45
|
+
} catch {
|
|
46
|
+
return { ...DEFAULT_CONFIG };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function saveConfig(config) {
|
|
50
|
+
const configPath = getConfigPath();
|
|
51
|
+
const dir = path.dirname(configPath);
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
53
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", {
|
|
54
|
+
mode: 384
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function getApiKey() {
|
|
58
|
+
return process.env.ANONLI_API_KEY || loadConfig().apiKey;
|
|
59
|
+
}
|
|
60
|
+
function getBaseUrl() {
|
|
61
|
+
return process.env.ANONLI_BASE_URL || loadConfig().baseUrl;
|
|
62
|
+
}
|
|
63
|
+
function setApiKey(key) {
|
|
64
|
+
const config = loadConfig();
|
|
65
|
+
config.apiKey = key;
|
|
66
|
+
saveConfig(config);
|
|
67
|
+
}
|
|
68
|
+
function removeApiKey() {
|
|
69
|
+
const config = loadConfig();
|
|
70
|
+
delete config.apiKey;
|
|
71
|
+
delete config.userEmail;
|
|
72
|
+
delete config.userName;
|
|
73
|
+
saveConfig(config);
|
|
74
|
+
}
|
|
75
|
+
function setUserInfo(email, name) {
|
|
76
|
+
const config = loadConfig();
|
|
77
|
+
config.userEmail = email;
|
|
78
|
+
config.userName = name;
|
|
79
|
+
saveConfig(config);
|
|
80
|
+
}
|
|
81
|
+
function getUserInfo() {
|
|
82
|
+
const config = loadConfig();
|
|
83
|
+
if (!config.userEmail) return null;
|
|
84
|
+
return { email: config.userEmail, name: config.userName ?? null };
|
|
85
|
+
}
|
|
86
|
+
function maskApiKey(key) {
|
|
87
|
+
if (key.length <= 8) return "****";
|
|
88
|
+
return key.slice(0, 6) + "..." + key.slice(-4);
|
|
89
|
+
}
|
|
90
|
+
var init_config2 = __esm({
|
|
91
|
+
"src/lib/config.ts"() {
|
|
92
|
+
"use strict";
|
|
93
|
+
init_config();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// src/lib/errors.ts
|
|
98
|
+
var EXIT_ERROR, EXIT_AUTH, EXIT_RATE_LIMIT, EXIT_PLAN_LIMIT, EXIT_NOT_FOUND, CliError, AuthError, ApiError, RateLimitError, PlanLimitError;
|
|
99
|
+
var init_errors = __esm({
|
|
100
|
+
"src/lib/errors.ts"() {
|
|
101
|
+
"use strict";
|
|
102
|
+
EXIT_ERROR = 1;
|
|
103
|
+
EXIT_AUTH = 2;
|
|
104
|
+
EXIT_RATE_LIMIT = 3;
|
|
105
|
+
EXIT_PLAN_LIMIT = 4;
|
|
106
|
+
EXIT_NOT_FOUND = 5;
|
|
107
|
+
CliError = class extends Error {
|
|
108
|
+
constructor(message, exitCode = EXIT_ERROR) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.exitCode = exitCode;
|
|
111
|
+
this.name = "CliError";
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
AuthError = class extends CliError {
|
|
115
|
+
constructor(message = "Not authenticated. Run `anonli login` first.") {
|
|
116
|
+
super(message, EXIT_AUTH);
|
|
117
|
+
this.name = "AuthError";
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
ApiError = class extends CliError {
|
|
121
|
+
statusCode;
|
|
122
|
+
code;
|
|
123
|
+
requestId;
|
|
124
|
+
constructor(message, statusCode, code, requestId) {
|
|
125
|
+
const exitCode = statusCode === 404 ? EXIT_NOT_FOUND : statusCode === 429 ? EXIT_RATE_LIMIT : statusCode === 402 || statusCode === 403 ? EXIT_PLAN_LIMIT : EXIT_ERROR;
|
|
126
|
+
super(message, exitCode);
|
|
127
|
+
this.name = "ApiError";
|
|
128
|
+
this.statusCode = statusCode;
|
|
129
|
+
this.code = code;
|
|
130
|
+
this.requestId = requestId;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
RateLimitError = class extends ApiError {
|
|
134
|
+
constructor(message, resetAt) {
|
|
135
|
+
super(message, 429, "RATE_LIMITED");
|
|
136
|
+
this.resetAt = resetAt;
|
|
137
|
+
this.name = "RateLimitError";
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
PlanLimitError = class extends CliError {
|
|
141
|
+
constructor(message, suggestion) {
|
|
142
|
+
super(message, EXIT_PLAN_LIMIT);
|
|
143
|
+
this.suggestion = suggestion;
|
|
144
|
+
this.name = "PlanLimitError";
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// package.json
|
|
151
|
+
var require_package = __commonJS({
|
|
152
|
+
"package.json"(exports, module) {
|
|
153
|
+
module.exports = {
|
|
154
|
+
name: "anonli",
|
|
155
|
+
version: "1.0.0",
|
|
156
|
+
description: "anon.li CLI - encrypted file drops and anonymous email aliases",
|
|
157
|
+
type: "module",
|
|
158
|
+
license: "AGPL-3.0",
|
|
159
|
+
author: "anon.li",
|
|
160
|
+
homepage: "https://anon.li",
|
|
161
|
+
repository: {
|
|
162
|
+
type: "git",
|
|
163
|
+
url: "https://github.com/anondotli/cli"
|
|
164
|
+
},
|
|
165
|
+
bugs: {
|
|
166
|
+
url: "https://github.com/anondotli/cli/issues"
|
|
167
|
+
},
|
|
168
|
+
keywords: [
|
|
169
|
+
"cli",
|
|
170
|
+
"encrypted",
|
|
171
|
+
"file-sharing",
|
|
172
|
+
"email",
|
|
173
|
+
"alias",
|
|
174
|
+
"privacy",
|
|
175
|
+
"private",
|
|
176
|
+
"anonymous",
|
|
177
|
+
"e2ee"
|
|
178
|
+
],
|
|
179
|
+
bin: {
|
|
180
|
+
anonli: "./bin/anonli.js"
|
|
181
|
+
},
|
|
182
|
+
files: [
|
|
183
|
+
"bin",
|
|
184
|
+
"dist"
|
|
185
|
+
],
|
|
186
|
+
scripts: {
|
|
187
|
+
build: "tsup",
|
|
188
|
+
dev: "tsup --watch",
|
|
189
|
+
typecheck: "tsc --noEmit",
|
|
190
|
+
prepublishOnly: "npm run build"
|
|
191
|
+
},
|
|
192
|
+
dependencies: {
|
|
193
|
+
boxen: "^8.0.1",
|
|
194
|
+
chalk: "^5.6.2",
|
|
195
|
+
"cli-progress": "^3.12.0",
|
|
196
|
+
"cli-table3": "^0.6.5",
|
|
197
|
+
commander: "^13.1.0",
|
|
198
|
+
figures: "^6.1.0",
|
|
199
|
+
"hash-wasm": "^4.12.0",
|
|
200
|
+
"mime-types": "^2.1.35",
|
|
201
|
+
ora: "^8.2.0",
|
|
202
|
+
semver: "^7.7.4",
|
|
203
|
+
"terminal-link": "^3.0.0"
|
|
204
|
+
},
|
|
205
|
+
devDependencies: {
|
|
206
|
+
"@types/cli-progress": "^3.11.6",
|
|
207
|
+
"@types/mime-types": "^2.1.4",
|
|
208
|
+
"@types/node": "^22.19.15",
|
|
209
|
+
"@types/semver": "^7.7.1",
|
|
210
|
+
tsup: "^8.5.1",
|
|
211
|
+
typescript: "^5.9.3"
|
|
212
|
+
},
|
|
213
|
+
engines: {
|
|
214
|
+
node: ">=18"
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// src/lib/constants.ts
|
|
221
|
+
var MB, GB, MAX_CHUNKS_PER_FILE, MIN_CHUNK_SIZE, FILE_SIZE_THRESHOLD_1GB, ARGON2_MEMORY, ARGON2_TIME, ARGON2_PARALLELISM, ARGON2_HASH_LENGTH, AUTH_TAG_SIZE, IV_LENGTH, SALT_LENGTH, CLI_VERSION, MAX_RETRIES, RETRY_BASE_DELAY;
|
|
222
|
+
var init_constants = __esm({
|
|
223
|
+
"src/lib/constants.ts"() {
|
|
224
|
+
"use strict";
|
|
225
|
+
MB = 1024 * 1024;
|
|
226
|
+
GB = 1024 * MB;
|
|
227
|
+
MAX_CHUNKS_PER_FILE = 100;
|
|
228
|
+
MIN_CHUNK_SIZE = 50 * MB;
|
|
229
|
+
FILE_SIZE_THRESHOLD_1GB = 1 * GB;
|
|
230
|
+
ARGON2_MEMORY = 65536;
|
|
231
|
+
ARGON2_TIME = 3;
|
|
232
|
+
ARGON2_PARALLELISM = 1;
|
|
233
|
+
ARGON2_HASH_LENGTH = 32;
|
|
234
|
+
AUTH_TAG_SIZE = 16;
|
|
235
|
+
IV_LENGTH = 12;
|
|
236
|
+
SALT_LENGTH = 32;
|
|
237
|
+
CLI_VERSION = require_package().version;
|
|
238
|
+
MAX_RETRIES = 3;
|
|
239
|
+
RETRY_BASE_DELAY = 1e3;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// src/lib/api.ts
|
|
244
|
+
var api_exports = {};
|
|
245
|
+
__export(api_exports, {
|
|
246
|
+
apiDelete: () => apiDelete,
|
|
247
|
+
apiFetch: () => apiFetch,
|
|
248
|
+
apiGet: () => apiGet,
|
|
249
|
+
apiGetList: () => apiGetList,
|
|
250
|
+
apiPatch: () => apiPatch,
|
|
251
|
+
apiPost: () => apiPost,
|
|
252
|
+
apiRawFetch: () => apiRawFetch,
|
|
253
|
+
extractRateLimit: () => extractRateLimit,
|
|
254
|
+
requireAuth: () => requireAuth
|
|
255
|
+
});
|
|
256
|
+
function getHeaders() {
|
|
257
|
+
const apiKey = getApiKey();
|
|
258
|
+
const headers = {
|
|
259
|
+
"Content-Type": "application/json",
|
|
260
|
+
"User-Agent": `anonli-cli/${CLI_VERSION}`
|
|
261
|
+
};
|
|
262
|
+
if (apiKey) {
|
|
263
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
264
|
+
}
|
|
265
|
+
return headers;
|
|
266
|
+
}
|
|
267
|
+
function extractRateLimit(res) {
|
|
268
|
+
const limit = res.headers.get("X-RateLimit-Limit");
|
|
269
|
+
const remaining = res.headers.get("X-RateLimit-Remaining");
|
|
270
|
+
const reset = res.headers.get("X-RateLimit-Reset");
|
|
271
|
+
if (limit && remaining && reset) {
|
|
272
|
+
return {
|
|
273
|
+
limit: parseInt(limit, 10),
|
|
274
|
+
remaining: parseInt(remaining, 10),
|
|
275
|
+
reset: parseInt(reset, 10)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return void 0;
|
|
279
|
+
}
|
|
280
|
+
async function handleError(res) {
|
|
281
|
+
let body;
|
|
282
|
+
try {
|
|
283
|
+
body = await res.json();
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
if (res.status === 401) {
|
|
287
|
+
const rawMsg = (body && "error" in body && typeof body.error === "string" ? body.error : body?.error?.message) || "Unauthorized";
|
|
288
|
+
throw new AuthError(CODE_MESSAGES["UNAUTHORIZED"] ?? rawMsg);
|
|
289
|
+
}
|
|
290
|
+
if (res.status === 429) {
|
|
291
|
+
const resetHeader = res.headers.get("X-RateLimit-Reset");
|
|
292
|
+
const resetDate = resetHeader ? new Date(parseInt(resetHeader, 10)) : new Date(Date.now() + 6e4);
|
|
293
|
+
throw new RateLimitError(
|
|
294
|
+
body?.error?.message || "Rate limit exceeded",
|
|
295
|
+
resetDate
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const apiCode = body?.error?.code;
|
|
299
|
+
const rawMessage = (body && "error" in body && typeof body.error === "object" ? body.error.message : body?.error) || `Request failed with status ${res.status}`;
|
|
300
|
+
const message = apiCode && CODE_MESSAGES[apiCode] || rawMessage;
|
|
301
|
+
throw new ApiError(
|
|
302
|
+
message,
|
|
303
|
+
res.status,
|
|
304
|
+
apiCode,
|
|
305
|
+
body?.meta?.request_id
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
async function fetchWithRetry(url, options, retries = MAX_RETRIES) {
|
|
309
|
+
let lastError = null;
|
|
310
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
311
|
+
try {
|
|
312
|
+
const res = await fetch(url, options);
|
|
313
|
+
if (res.status >= 400 && res.status < 500) {
|
|
314
|
+
return res;
|
|
315
|
+
}
|
|
316
|
+
if (res.status >= 500 && attempt < retries) {
|
|
317
|
+
await delay(RETRY_BASE_DELAY * Math.pow(2, attempt));
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
return res;
|
|
321
|
+
} catch (err) {
|
|
322
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
323
|
+
if (lastError.name === "AbortError") {
|
|
324
|
+
throw lastError;
|
|
325
|
+
}
|
|
326
|
+
if (attempt < retries) {
|
|
327
|
+
await delay(RETRY_BASE_DELAY * Math.pow(2, attempt));
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
throw lastError || new Error("Request failed after retries");
|
|
333
|
+
}
|
|
334
|
+
function delay(ms) {
|
|
335
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
336
|
+
}
|
|
337
|
+
async function apiGet(path4, retry = true) {
|
|
338
|
+
const url = `${getBaseUrl()}${path4}`;
|
|
339
|
+
const res = retry ? await fetchWithRetry(url, { method: "GET", headers: getHeaders() }) : await fetch(url, { method: "GET", headers: getHeaders() });
|
|
340
|
+
if (!res.ok) await handleError(res);
|
|
341
|
+
const rateLimit = extractRateLimit(res);
|
|
342
|
+
const json = await res.json();
|
|
343
|
+
return { data: json.data, rateLimit };
|
|
344
|
+
}
|
|
345
|
+
async function apiGetList(path4) {
|
|
346
|
+
const url = `${getBaseUrl()}${path4}`;
|
|
347
|
+
const res = await fetchWithRetry(url, {
|
|
348
|
+
method: "GET",
|
|
349
|
+
headers: getHeaders()
|
|
350
|
+
});
|
|
351
|
+
if (!res.ok) await handleError(res);
|
|
352
|
+
const rateLimit = extractRateLimit(res);
|
|
353
|
+
const json = await res.json();
|
|
354
|
+
return {
|
|
355
|
+
data: json.data,
|
|
356
|
+
total: json.meta.total,
|
|
357
|
+
rateLimit,
|
|
358
|
+
meta: json.meta
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async function apiPost(path4, body) {
|
|
362
|
+
const url = `${getBaseUrl()}${path4}`;
|
|
363
|
+
const res = await fetch(url, {
|
|
364
|
+
method: "POST",
|
|
365
|
+
headers: getHeaders(),
|
|
366
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
367
|
+
});
|
|
368
|
+
if (!res.ok) await handleError(res);
|
|
369
|
+
const rateLimit = extractRateLimit(res);
|
|
370
|
+
const json = await res.json();
|
|
371
|
+
return { data: json.data, rateLimit };
|
|
372
|
+
}
|
|
373
|
+
async function apiPatch(path4, body) {
|
|
374
|
+
const url = `${getBaseUrl()}${path4}`;
|
|
375
|
+
const res = await fetch(url, {
|
|
376
|
+
method: "PATCH",
|
|
377
|
+
headers: getHeaders(),
|
|
378
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
379
|
+
});
|
|
380
|
+
if (!res.ok) await handleError(res);
|
|
381
|
+
const rateLimit = extractRateLimit(res);
|
|
382
|
+
const json = await res.json();
|
|
383
|
+
return { data: json.data, rateLimit };
|
|
384
|
+
}
|
|
385
|
+
async function apiDelete(path4) {
|
|
386
|
+
const url = `${getBaseUrl()}${path4}`;
|
|
387
|
+
const res = await fetch(url, {
|
|
388
|
+
method: "DELETE",
|
|
389
|
+
headers: getHeaders()
|
|
390
|
+
});
|
|
391
|
+
if (!res.ok) await handleError(res);
|
|
392
|
+
return { rateLimit: extractRateLimit(res) };
|
|
393
|
+
}
|
|
394
|
+
async function apiRawFetch(url, options) {
|
|
395
|
+
return fetch(url, options);
|
|
396
|
+
}
|
|
397
|
+
async function apiFetch(path4, options = {}) {
|
|
398
|
+
const url = `${getBaseUrl()}${path4}`;
|
|
399
|
+
return fetch(url, {
|
|
400
|
+
...options,
|
|
401
|
+
headers: {
|
|
402
|
+
...getHeaders(),
|
|
403
|
+
...options.headers
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function requireAuth() {
|
|
408
|
+
if (!getApiKey()) {
|
|
409
|
+
throw new AuthError();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
var CODE_MESSAGES;
|
|
413
|
+
var init_api = __esm({
|
|
414
|
+
"src/lib/api.ts"() {
|
|
415
|
+
"use strict";
|
|
416
|
+
init_config2();
|
|
417
|
+
init_errors();
|
|
418
|
+
init_constants();
|
|
419
|
+
CODE_MESSAGES = {
|
|
420
|
+
NOT_FOUND: "Resource not found \u2014 it may have expired or been deleted",
|
|
421
|
+
QUOTA_EXCEEDED: "Storage limit reached \u2014 upgrade at anon.li/dashboard",
|
|
422
|
+
FORBIDDEN: "This feature requires a paid plan \u2014 see `anonli subscribe`",
|
|
423
|
+
UNAUTHORIZED: "Invalid or expired API key \u2014 run `anonli login` to re-authenticate"
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// src/index.ts
|
|
429
|
+
import { Command as Command26 } from "commander";
|
|
430
|
+
|
|
431
|
+
// src/commands/login.ts
|
|
432
|
+
import { Command } from "commander";
|
|
433
|
+
|
|
434
|
+
// src/lib/auth.ts
|
|
435
|
+
init_config2();
|
|
436
|
+
init_api();
|
|
437
|
+
|
|
438
|
+
// src/lib/ui.ts
|
|
439
|
+
import chalk2 from "chalk";
|
|
440
|
+
import ora from "ora";
|
|
441
|
+
import cliProgress from "cli-progress";
|
|
442
|
+
import figures from "figures";
|
|
443
|
+
import boxen from "boxen";
|
|
444
|
+
import Table from "cli-table3";
|
|
445
|
+
import terminalLink from "terminal-link";
|
|
446
|
+
import readline from "readline";
|
|
447
|
+
|
|
448
|
+
// src/lib/theme.ts
|
|
449
|
+
import chalk from "chalk";
|
|
450
|
+
if (process.env.NO_COLOR !== void 0 || process.env.TERM === "dumb") {
|
|
451
|
+
chalk.level = 0;
|
|
452
|
+
}
|
|
453
|
+
var blue = chalk.hex("#6366F1");
|
|
454
|
+
var brandColor = Object.assign(
|
|
455
|
+
(str) => blue(str),
|
|
456
|
+
{ multiline: (str) => str.split("\n").map((line) => blue(line)).join("\n") }
|
|
457
|
+
);
|
|
458
|
+
var c = {
|
|
459
|
+
// Status
|
|
460
|
+
success: chalk.hex("#10B981"),
|
|
461
|
+
error: chalk.hex("#EF4444"),
|
|
462
|
+
warning: chalk.hex("#F59E0B"),
|
|
463
|
+
info: chalk.hex("#8B5CF6"),
|
|
464
|
+
// Text hierarchy
|
|
465
|
+
primary: chalk.hex("#F8FAFC"),
|
|
466
|
+
secondary: chalk.hex("#94A3B8"),
|
|
467
|
+
muted: chalk.hex("#64748B"),
|
|
468
|
+
subtle: chalk.hex("#475569"),
|
|
469
|
+
// Accents
|
|
470
|
+
accent: chalk.hex("#A78BFA"),
|
|
471
|
+
link: chalk.hex("#22D3EE"),
|
|
472
|
+
gold: chalk.hex("#FBBF24"),
|
|
473
|
+
// Borders
|
|
474
|
+
border: chalk.hex("#334155")
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// src/lib/brand.ts
|
|
478
|
+
init_config2();
|
|
479
|
+
var LOGO = `
|
|
480
|
+
__ _ _ __ ___ _ __ | (_)
|
|
481
|
+
/ _\` | '_ \\ / _ \\| '_ \\ | | |
|
|
482
|
+
| (_| | | | | (_) | | | |_| | |
|
|
483
|
+
\\__,_|_| |_|\\___/|_| |_(_)_|_|`;
|
|
484
|
+
function printBanner(version) {
|
|
485
|
+
const logo = brandColor.multiline(LOGO.slice(1));
|
|
486
|
+
const tagline = c.secondary(" Encrypted drops & anonymous aliases") + c.muted(` v${version}`);
|
|
487
|
+
let greeting = "";
|
|
488
|
+
if (getApiKey()) {
|
|
489
|
+
const info2 = getUserInfo();
|
|
490
|
+
if (info2) {
|
|
491
|
+
const displayName = info2.name ? info2.name.split(" ")[0] : info2.email.split("@")[0];
|
|
492
|
+
greeting = "\n " + c.secondary("Welcome back, ") + c.primary(displayName) + c.secondary("!");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return `
|
|
496
|
+
${logo}
|
|
497
|
+
${tagline}${greeting}
|
|
498
|
+
`;
|
|
499
|
+
}
|
|
500
|
+
function printHeader(commandName) {
|
|
501
|
+
const name = brandColor("anon.li");
|
|
502
|
+
const arrow = c.muted(" > ");
|
|
503
|
+
const cmd = c.primary(commandName);
|
|
504
|
+
const divider = c.subtle("\u2500".repeat(48));
|
|
505
|
+
return `${name}${arrow}${cmd}
|
|
506
|
+
${divider}`;
|
|
507
|
+
}
|
|
508
|
+
function getCommandPath(cmd) {
|
|
509
|
+
const parts = [];
|
|
510
|
+
let current = cmd;
|
|
511
|
+
while (current) {
|
|
512
|
+
if (current.parent) {
|
|
513
|
+
parts.unshift(current.name());
|
|
514
|
+
}
|
|
515
|
+
current = current.parent;
|
|
516
|
+
}
|
|
517
|
+
return parts.join(" ");
|
|
518
|
+
}
|
|
519
|
+
function createHelpConfig(version) {
|
|
520
|
+
return {
|
|
521
|
+
formatHelp(cmd, helper) {
|
|
522
|
+
const isRoot = !cmd.parent;
|
|
523
|
+
let output = "";
|
|
524
|
+
if (isRoot) {
|
|
525
|
+
output += printBanner(version) + "\n";
|
|
526
|
+
} else {
|
|
527
|
+
output += printHeader(getCommandPath(cmd)) + "\n\n";
|
|
528
|
+
}
|
|
529
|
+
const desc = helper.commandDescription(cmd);
|
|
530
|
+
if (desc && !isRoot) {
|
|
531
|
+
output += ` ${c.secondary(desc)}
|
|
532
|
+
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
const usage = helper.commandUsage(cmd);
|
|
536
|
+
output += ` ${c.muted("Usage")}
|
|
537
|
+
`;
|
|
538
|
+
output += ` ${c.subtle("$")} ${c.primary(usage)}
|
|
539
|
+
`;
|
|
540
|
+
const args = helper.visibleArguments(cmd);
|
|
541
|
+
if (args.length > 0) {
|
|
542
|
+
const termLen = helper.longestArgumentTermLength(cmd, helper);
|
|
543
|
+
output += `
|
|
544
|
+
${c.muted("Arguments")}
|
|
545
|
+
`;
|
|
546
|
+
for (const arg of args) {
|
|
547
|
+
const term = helper.argumentTerm(arg);
|
|
548
|
+
const desc2 = helper.argumentDescription(arg);
|
|
549
|
+
output += ` ${c.accent(term.padEnd(termLen + 2))} ${c.secondary(desc2)}
|
|
550
|
+
`;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const cmds = helper.visibleCommands(cmd).filter(
|
|
554
|
+
(sub) => sub.name() !== "help"
|
|
555
|
+
);
|
|
556
|
+
if (cmds.length > 0) {
|
|
557
|
+
let maxLen = 0;
|
|
558
|
+
const items = cmds.map((sub) => {
|
|
559
|
+
const term = helper.subcommandTerm(sub);
|
|
560
|
+
if (term.length > maxLen) maxLen = term.length;
|
|
561
|
+
return { term, desc: helper.subcommandDescription(sub) };
|
|
562
|
+
});
|
|
563
|
+
output += `
|
|
564
|
+
${c.muted("Commands")}
|
|
565
|
+
`;
|
|
566
|
+
for (const { term, desc: desc2 } of items) {
|
|
567
|
+
output += ` ${c.accent(term.padEnd(maxLen + 2))} ${c.secondary(desc2)}
|
|
568
|
+
`;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const opts = helper.visibleOptions(cmd);
|
|
572
|
+
if (opts.length > 0) {
|
|
573
|
+
const termLen = helper.longestOptionTermLength(cmd, helper);
|
|
574
|
+
output += `
|
|
575
|
+
${c.muted("Options")}
|
|
576
|
+
`;
|
|
577
|
+
for (const opt of opts) {
|
|
578
|
+
const term = helper.optionTerm(opt);
|
|
579
|
+
const desc2 = helper.optionDescription(opt);
|
|
580
|
+
output += ` ${c.accent(term.padEnd(termLen + 2))} ${c.secondary(desc2)}
|
|
581
|
+
`;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
output += "\n";
|
|
585
|
+
return output;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/lib/ui.ts
|
|
591
|
+
var _quiet = false;
|
|
592
|
+
function setQuiet(val) {
|
|
593
|
+
_quiet = val;
|
|
594
|
+
}
|
|
595
|
+
function outputJson(data) {
|
|
596
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
597
|
+
}
|
|
598
|
+
function spinner(text) {
|
|
599
|
+
return ora({
|
|
600
|
+
text: c.secondary(text),
|
|
601
|
+
color: "magenta",
|
|
602
|
+
isSilent: _quiet
|
|
603
|
+
}).start();
|
|
604
|
+
}
|
|
605
|
+
function success(message) {
|
|
606
|
+
if (_quiet) return;
|
|
607
|
+
console.log(c.success(figures.tick) + " " + c.primary(message));
|
|
608
|
+
}
|
|
609
|
+
function error(message) {
|
|
610
|
+
console.error(c.error(figures.cross) + " " + c.primary(message));
|
|
611
|
+
}
|
|
612
|
+
function warn(message) {
|
|
613
|
+
if (_quiet) return;
|
|
614
|
+
console.log(c.warning(figures.warning) + " " + c.secondary(message));
|
|
615
|
+
}
|
|
616
|
+
function info(message) {
|
|
617
|
+
if (_quiet) return;
|
|
618
|
+
console.log(c.info(figures.info) + " " + c.secondary(message));
|
|
619
|
+
}
|
|
620
|
+
function dim(text) {
|
|
621
|
+
return c.muted(text);
|
|
622
|
+
}
|
|
623
|
+
function bold(text) {
|
|
624
|
+
return c.primary(chalk2.bold(text));
|
|
625
|
+
}
|
|
626
|
+
function link(url) {
|
|
627
|
+
return terminalLink(c.link(url), url, {
|
|
628
|
+
fallback: () => c.link(url)
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function header(title) {
|
|
632
|
+
if (_quiet) return;
|
|
633
|
+
console.log(printHeader(title));
|
|
634
|
+
}
|
|
635
|
+
function spacer() {
|
|
636
|
+
if (_quiet) return;
|
|
637
|
+
console.log();
|
|
638
|
+
}
|
|
639
|
+
function keyValue(label, value, indent = 2) {
|
|
640
|
+
if (_quiet) return;
|
|
641
|
+
const pad = " ".repeat(indent);
|
|
642
|
+
console.log(`${pad}${c.secondary(label + ":")} ${c.primary(value)}`);
|
|
643
|
+
}
|
|
644
|
+
function sectionTitle(title) {
|
|
645
|
+
if (_quiet) return;
|
|
646
|
+
console.log(c.secondary(chalk2.bold(title)));
|
|
647
|
+
}
|
|
648
|
+
function successBox(title, content) {
|
|
649
|
+
if (_quiet) return;
|
|
650
|
+
console.log(
|
|
651
|
+
boxen(content, {
|
|
652
|
+
title: c.success(figures.tick + " " + title),
|
|
653
|
+
titleAlignment: "left",
|
|
654
|
+
borderStyle: "round",
|
|
655
|
+
borderColor: "#10B981",
|
|
656
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
657
|
+
margin: { top: 1, bottom: 0, left: 0, right: 0 }
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
function errorBox(title, message, suggestion) {
|
|
662
|
+
let content = c.primary(message);
|
|
663
|
+
if (suggestion) {
|
|
664
|
+
content += "\n" + c.muted(suggestion);
|
|
665
|
+
}
|
|
666
|
+
console.log(
|
|
667
|
+
boxen(content, {
|
|
668
|
+
title: c.error(figures.cross + " " + title),
|
|
669
|
+
titleAlignment: "left",
|
|
670
|
+
borderStyle: "round",
|
|
671
|
+
borderColor: "#EF4444",
|
|
672
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
673
|
+
margin: { top: 1, bottom: 0, left: 0, right: 0 }
|
|
674
|
+
})
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
function box(content, opts) {
|
|
678
|
+
if (_quiet) return;
|
|
679
|
+
console.log(
|
|
680
|
+
boxen(content, {
|
|
681
|
+
title: opts?.title,
|
|
682
|
+
titleAlignment: "left",
|
|
683
|
+
borderStyle: "round",
|
|
684
|
+
borderColor: opts?.borderColor || "#334155",
|
|
685
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
686
|
+
margin: { top: 1, bottom: 0, left: 0, right: 0 }
|
|
687
|
+
})
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
function confirm(prompt2) {
|
|
691
|
+
return new Promise((resolve) => {
|
|
692
|
+
const rl = readline.createInterface({
|
|
693
|
+
input: process.stdin,
|
|
694
|
+
output: process.stdout
|
|
695
|
+
});
|
|
696
|
+
const styled = `${c.warning(figures.warning)} ${c.primary(prompt2)} ${c.muted("[y/N]")} `;
|
|
697
|
+
rl.question(styled, (answer) => {
|
|
698
|
+
rl.close();
|
|
699
|
+
resolve(answer.toLowerCase() === "y");
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function prompt(question, opts) {
|
|
704
|
+
const styled = `${c.info(figures.pointer)} ${c.primary(question)} `;
|
|
705
|
+
if (!opts?.mask) {
|
|
706
|
+
return new Promise((resolve) => {
|
|
707
|
+
const rl = readline.createInterface({
|
|
708
|
+
input: process.stdin,
|
|
709
|
+
output: process.stdout
|
|
710
|
+
});
|
|
711
|
+
rl.question(styled, (answer) => {
|
|
712
|
+
rl.close();
|
|
713
|
+
resolve(answer.trim());
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
return new Promise((resolve) => {
|
|
718
|
+
process.stdout.write(styled);
|
|
719
|
+
const stdin = process.stdin;
|
|
720
|
+
if (!stdin.isTTY) {
|
|
721
|
+
const rl = readline.createInterface({ input: stdin });
|
|
722
|
+
rl.once("line", (line) => {
|
|
723
|
+
rl.close();
|
|
724
|
+
console.log();
|
|
725
|
+
resolve(line.trim());
|
|
726
|
+
});
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const wasRaw = stdin.isRaw;
|
|
730
|
+
stdin.setRawMode(true);
|
|
731
|
+
stdin.resume();
|
|
732
|
+
stdin.setEncoding("utf8");
|
|
733
|
+
let input = "";
|
|
734
|
+
const onData = (data) => {
|
|
735
|
+
for (const ch of data) {
|
|
736
|
+
if (ch === "\r" || ch === "\n") {
|
|
737
|
+
stdin.setRawMode(wasRaw);
|
|
738
|
+
stdin.removeListener("data", onData);
|
|
739
|
+
stdin.pause();
|
|
740
|
+
console.log();
|
|
741
|
+
resolve(input.trim());
|
|
742
|
+
return;
|
|
743
|
+
} else if (ch === "") {
|
|
744
|
+
stdin.setRawMode(wasRaw);
|
|
745
|
+
stdin.removeListener("data", onData);
|
|
746
|
+
stdin.pause();
|
|
747
|
+
console.log();
|
|
748
|
+
process.exit(0);
|
|
749
|
+
return;
|
|
750
|
+
} else if (ch === "\x7F" || ch === "\b") {
|
|
751
|
+
if (input.length > 0) {
|
|
752
|
+
input = input.slice(0, -1);
|
|
753
|
+
process.stdout.write("\b \b");
|
|
754
|
+
}
|
|
755
|
+
} else if (ch.charCodeAt(0) >= 32) {
|
|
756
|
+
input += ch;
|
|
757
|
+
process.stdout.write(c.muted("*"));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
stdin.on("data", onData);
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
function usageBar(used, limit, width = 12) {
|
|
765
|
+
if (limit === 0) {
|
|
766
|
+
return used > 0 ? c.error("\u2593".repeat(width)) : c.subtle("\u2591".repeat(width));
|
|
767
|
+
}
|
|
768
|
+
const ratio = Math.min(used / limit, 1);
|
|
769
|
+
const filled = Math.round(ratio * width);
|
|
770
|
+
const empty = width - filled;
|
|
771
|
+
let colorFn = c.success;
|
|
772
|
+
if (used > limit) colorFn = c.error;
|
|
773
|
+
else if (ratio >= 0.9) colorFn = c.warning;
|
|
774
|
+
else if (ratio >= 0.7) colorFn = c.warning;
|
|
775
|
+
return colorFn("\u2593".repeat(filled)) + c.subtle("\u2591".repeat(empty));
|
|
776
|
+
}
|
|
777
|
+
function usageRow(label, used, limit, opts) {
|
|
778
|
+
if (_quiet) return;
|
|
779
|
+
const labelWidth = opts?.labelWidth ?? 14;
|
|
780
|
+
const barWidth = opts?.barWidth ?? 12;
|
|
781
|
+
const pad = " ";
|
|
782
|
+
const paddedLabel = c.secondary((label + ":").padEnd(labelWidth));
|
|
783
|
+
if (limit === void 0) {
|
|
784
|
+
console.log(`${pad}${paddedLabel} ${c.primary(String(used))}`);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const bar = usageBar(used, limit, barWidth);
|
|
788
|
+
const count = `${used}/${limit}`;
|
|
789
|
+
const overLimit = used > limit;
|
|
790
|
+
const countStr = overLimit ? c.error(count) : c.primary(count);
|
|
791
|
+
const warnStr = overLimit ? " " + c.warning("\u25B2") : "";
|
|
792
|
+
console.log(`${pad}${paddedLabel} ${bar} ${countStr}${warnStr}`);
|
|
793
|
+
}
|
|
794
|
+
function storageRow(label, used, limit, opts) {
|
|
795
|
+
if (_quiet) return;
|
|
796
|
+
const labelWidth = opts?.labelWidth ?? 14;
|
|
797
|
+
const barWidth = opts?.barWidth ?? 12;
|
|
798
|
+
const pad = " ";
|
|
799
|
+
const paddedLabel = c.secondary((label + ":").padEnd(labelWidth));
|
|
800
|
+
const bar = usageBar(used, limit, barWidth);
|
|
801
|
+
console.log(
|
|
802
|
+
`${pad}${paddedLabel} ${bar} ${c.primary(formatBytes(used))}${c.muted("/")}${c.primary(formatBytes(limit))}`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
function statusBadge(label, variant) {
|
|
806
|
+
if (variant === "active") {
|
|
807
|
+
return c.success("\u25CF") + " " + c.success(label);
|
|
808
|
+
}
|
|
809
|
+
return c.error("\u25CB") + " " + c.error(label);
|
|
810
|
+
}
|
|
811
|
+
function tierBadge(tier, product) {
|
|
812
|
+
const t = tier.toLowerCase();
|
|
813
|
+
const productLabel = product ? ` (${product.charAt(0).toUpperCase() + product.slice(1)})` : "";
|
|
814
|
+
if (t === "pro") return c.gold(chalk2.bold("PRO")) + c.muted(productLabel);
|
|
815
|
+
if (t === "plus") return c.accent(chalk2.bold("PLUS")) + c.muted(productLabel);
|
|
816
|
+
return c.muted("Free");
|
|
817
|
+
}
|
|
818
|
+
function progressBar(total, label) {
|
|
819
|
+
const bar = new cliProgress.SingleBar(
|
|
820
|
+
{
|
|
821
|
+
format: ` ${c.secondary(label)} ${chalk2.hex("#8B5CF6")("{bar}")} ${c.primary("{percentage}%")} ${c.subtle("\u2503")} ${c.muted("{value}/{total}")}`,
|
|
822
|
+
barCompleteChar: "\u2501",
|
|
823
|
+
barIncompleteChar: chalk2.hex("#334155")("\u2501"),
|
|
824
|
+
hideCursor: true,
|
|
825
|
+
noTTYOutput: _quiet
|
|
826
|
+
},
|
|
827
|
+
cliProgress.Presets.shades_classic
|
|
828
|
+
);
|
|
829
|
+
bar.start(total, 0);
|
|
830
|
+
return bar;
|
|
831
|
+
}
|
|
832
|
+
function showRateLimit(rateLimit) {
|
|
833
|
+
if (!rateLimit || _quiet) return;
|
|
834
|
+
console.log(
|
|
835
|
+
c.subtle(
|
|
836
|
+
` ${figures.bullet} API: ${rateLimit.remaining}/${rateLimit.limit} requests remaining`
|
|
837
|
+
)
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
function table(headers, rows) {
|
|
841
|
+
if (_quiet) return;
|
|
842
|
+
const t = new Table({
|
|
843
|
+
head: headers.map((h) => c.secondary(chalk2.bold(h))),
|
|
844
|
+
chars: {
|
|
845
|
+
top: c.subtle("\u2500"),
|
|
846
|
+
"top-mid": c.subtle("\u252C"),
|
|
847
|
+
"top-left": c.subtle("\u256D"),
|
|
848
|
+
"top-right": c.subtle("\u256E"),
|
|
849
|
+
bottom: c.subtle("\u2500"),
|
|
850
|
+
"bottom-mid": c.subtle("\u2534"),
|
|
851
|
+
"bottom-left": c.subtle("\u2570"),
|
|
852
|
+
"bottom-right": c.subtle("\u256F"),
|
|
853
|
+
left: c.subtle("\u2502"),
|
|
854
|
+
"left-mid": c.subtle("\u251C"),
|
|
855
|
+
mid: c.subtle("\u2500"),
|
|
856
|
+
"mid-mid": c.subtle("\u253C"),
|
|
857
|
+
right: c.subtle("\u2502"),
|
|
858
|
+
"right-mid": c.subtle("\u2524"),
|
|
859
|
+
middle: c.subtle("\u2502")
|
|
860
|
+
},
|
|
861
|
+
style: {
|
|
862
|
+
head: [],
|
|
863
|
+
border: [],
|
|
864
|
+
"padding-left": 1,
|
|
865
|
+
"padding-right": 1
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
for (const row of rows) {
|
|
869
|
+
t.push(row);
|
|
870
|
+
}
|
|
871
|
+
console.log(t.toString());
|
|
872
|
+
}
|
|
873
|
+
function updateNotice(current, latest) {
|
|
874
|
+
if (_quiet) return;
|
|
875
|
+
const content = [
|
|
876
|
+
`${c.muted("Current:")} ${c.primary(current)} ${c.muted("\u2192")} ${c.muted("Latest:")} ${c.success(latest)}`,
|
|
877
|
+
"",
|
|
878
|
+
`${c.secondary("Run")} ${c.accent("anonli update")} ${c.secondary("to upgrade")}`
|
|
879
|
+
].join("\n");
|
|
880
|
+
console.log(
|
|
881
|
+
boxen(content, {
|
|
882
|
+
title: c.warning(figures.warning + " Update Available"),
|
|
883
|
+
titleAlignment: "left",
|
|
884
|
+
borderStyle: "round",
|
|
885
|
+
borderColor: "#F59E0B",
|
|
886
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
887
|
+
margin: { top: 1, bottom: 0, left: 0, right: 0 }
|
|
888
|
+
})
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
function formatBytes(bytes) {
|
|
892
|
+
if (bytes === 0) return "0 B";
|
|
893
|
+
const k = 1024;
|
|
894
|
+
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
895
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
896
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
897
|
+
}
|
|
898
|
+
function formatDate(iso) {
|
|
899
|
+
return new Date(iso).toLocaleDateString("en-US", {
|
|
900
|
+
year: "numeric",
|
|
901
|
+
month: "short",
|
|
902
|
+
day: "numeric"
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
function formatDays(days) {
|
|
906
|
+
if (days === 1) return "24 hours";
|
|
907
|
+
return `${days} days`;
|
|
908
|
+
}
|
|
909
|
+
function alignedKeyValue(label, value, labelWidth = 18, indent = 2) {
|
|
910
|
+
if (_quiet) return;
|
|
911
|
+
const pad = " ".repeat(indent);
|
|
912
|
+
const paddedLabel = (label + ":").padEnd(labelWidth);
|
|
913
|
+
console.log(`${pad}${c.secondary(paddedLabel)} ${c.primary(value)}`);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/lib/auth.ts
|
|
917
|
+
async function runAuthFlow(token) {
|
|
918
|
+
let apiKey = token;
|
|
919
|
+
if (!apiKey) {
|
|
920
|
+
info(
|
|
921
|
+
`Get your API key from ${link("https://anon.li/dashboard/settings/api")}`
|
|
922
|
+
);
|
|
923
|
+
spacer();
|
|
924
|
+
apiKey = await prompt("API key:", { mask: true });
|
|
925
|
+
}
|
|
926
|
+
if (!apiKey) {
|
|
927
|
+
error("No API key provided.");
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
if (!apiKey.startsWith("ak_")) {
|
|
931
|
+
errorBox(
|
|
932
|
+
"Invalid Key",
|
|
933
|
+
'API keys start with "ak_".',
|
|
934
|
+
"Get your key at https://anon.li/dashboard/settings/api"
|
|
935
|
+
);
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
const spin = spinner("Validating API key...");
|
|
939
|
+
try {
|
|
940
|
+
setApiKey(apiKey);
|
|
941
|
+
const result = await apiGet("/api/v1/me");
|
|
942
|
+
spin.stop();
|
|
943
|
+
setUserInfo(result.data.email, result.data.name);
|
|
944
|
+
const badge = tierBadge(result.data.tier, result.data.product);
|
|
945
|
+
successBox(
|
|
946
|
+
"Authenticated",
|
|
947
|
+
`Logged in as ${c.accent(result.data.email)} ${badge}`
|
|
948
|
+
);
|
|
949
|
+
showRateLimit(result.rateLimit);
|
|
950
|
+
return true;
|
|
951
|
+
} catch (err) {
|
|
952
|
+
removeApiKey();
|
|
953
|
+
spin.stop();
|
|
954
|
+
errorBox(
|
|
955
|
+
"Authentication Failed",
|
|
956
|
+
err instanceof Error ? err.message : "Invalid API key.",
|
|
957
|
+
"Get your key at https://anon.li/dashboard/settings/api"
|
|
958
|
+
);
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/commands/login.ts
|
|
964
|
+
init_config2();
|
|
965
|
+
var loginCommand = new Command("login").description("Authenticate with your API key").option("--token <key>", "API key (or enter interactively)").action(async (options) => {
|
|
966
|
+
if (getApiKey()) {
|
|
967
|
+
errorBox(
|
|
968
|
+
"Already Logged In",
|
|
969
|
+
"You are already authenticated.",
|
|
970
|
+
"Run `anonli logout` first, then try again."
|
|
971
|
+
);
|
|
972
|
+
process.exit(1);
|
|
973
|
+
}
|
|
974
|
+
const success2 = await runAuthFlow(options.token);
|
|
975
|
+
if (!success2) {
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// src/commands/logout.ts
|
|
981
|
+
init_config2();
|
|
982
|
+
import { Command as Command2 } from "commander";
|
|
983
|
+
var logoutCommand = new Command2("logout").description("Remove stored API key").action(() => {
|
|
984
|
+
if (!getApiKey()) {
|
|
985
|
+
info("Not currently authenticated.");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
removeApiKey();
|
|
989
|
+
success("Logged out. API key removed.");
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
// src/commands/whoami.ts
|
|
993
|
+
init_api();
|
|
994
|
+
init_config2();
|
|
995
|
+
import { Command as Command3 } from "commander";
|
|
996
|
+
var whoamiCommand = new Command3("whoami").description("Show current user info").option("--json", "Output raw JSON").action(async (options) => {
|
|
997
|
+
requireAuth();
|
|
998
|
+
const spin = spinner("Fetching account info...");
|
|
999
|
+
try {
|
|
1000
|
+
const result = await apiGet("/api/v1/me");
|
|
1001
|
+
spin.stop();
|
|
1002
|
+
const d = result.data;
|
|
1003
|
+
setUserInfo(d.email, d.name);
|
|
1004
|
+
if (options.json) {
|
|
1005
|
+
outputJson(d);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
header(d.email);
|
|
1009
|
+
const badge = tierBadge(d.tier, d.product);
|
|
1010
|
+
console.log(
|
|
1011
|
+
` ${badge} ${c.muted("\xB7")} ${c.secondary("Joined")} ${c.primary(formatDate(d.created_at))}`
|
|
1012
|
+
);
|
|
1013
|
+
spacer();
|
|
1014
|
+
sectionTitle("Alias");
|
|
1015
|
+
usageRow("Random", d.aliases.random.used, d.aliases.random.limit);
|
|
1016
|
+
usageRow("Custom", d.aliases.custom.used, d.aliases.custom.limit);
|
|
1017
|
+
usageRow("Recipients", d.recipients.used, d.recipients.limit);
|
|
1018
|
+
usageRow("Domains", d.domains.used, d.domains.limit);
|
|
1019
|
+
spacer();
|
|
1020
|
+
sectionTitle("Drop");
|
|
1021
|
+
usageRow("Active", d.drops.count);
|
|
1022
|
+
storageRow("Storage", parseInt(d.storage.used), parseInt(d.storage.limit));
|
|
1023
|
+
alignedKeyValue("Max File", formatBytes(d.limits.max_file_size), 14);
|
|
1024
|
+
alignedKeyValue("Expiry", formatDays(d.limits.max_expiry_days), 14);
|
|
1025
|
+
spacer();
|
|
1026
|
+
showRateLimit(result.rateLimit);
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
spin.fail("Failed to fetch account info.");
|
|
1029
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
// src/commands/drop.ts
|
|
1035
|
+
import { Command as Command11 } from "commander";
|
|
1036
|
+
|
|
1037
|
+
// src/commands/drop-upload.ts
|
|
1038
|
+
init_api();
|
|
1039
|
+
import { Command as Command4 } from "commander";
|
|
1040
|
+
import fs2 from "fs";
|
|
1041
|
+
import os2 from "os";
|
|
1042
|
+
import path2 from "path";
|
|
1043
|
+
|
|
1044
|
+
// src/lib/limits.ts
|
|
1045
|
+
init_api();
|
|
1046
|
+
init_errors();
|
|
1047
|
+
var FEATURE_MAP = {
|
|
1048
|
+
noBranding: { label: "Hide branding", tier: "Pro" },
|
|
1049
|
+
downloadNotifications: { label: "Download notifications", tier: "Pro" },
|
|
1050
|
+
customKey: { label: "Password protection", tier: "Plus" }
|
|
1051
|
+
};
|
|
1052
|
+
async function fetchPlanInfo() {
|
|
1053
|
+
const { data } = await apiGet("/api/v1/me");
|
|
1054
|
+
return data;
|
|
1055
|
+
}
|
|
1056
|
+
function assertFeature(features, featureKey, flagName) {
|
|
1057
|
+
if (features[featureKey]) return;
|
|
1058
|
+
const info2 = FEATURE_MAP[featureKey];
|
|
1059
|
+
const label = info2?.label ?? featureKey;
|
|
1060
|
+
const tier = info2?.tier ?? "a paid";
|
|
1061
|
+
throw new PlanLimitError(
|
|
1062
|
+
`${label} (${flagName}) requires a ${tier} plan.`,
|
|
1063
|
+
`Upgrade with: anonli subscribe`
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
function assertCountLimit(resourceName, used, limit) {
|
|
1067
|
+
if (limit === 0) {
|
|
1068
|
+
throw new PlanLimitError(
|
|
1069
|
+
`${capitalize(resourceName)} is not available on your plan.`,
|
|
1070
|
+
`Upgrade with: anonli subscribe`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
if (used >= limit) {
|
|
1074
|
+
throw new PlanLimitError(
|
|
1075
|
+
`${capitalize(resourceName)} limit reached (${used}/${limit}).`,
|
|
1076
|
+
`Upgrade with: anonli subscribe`
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function assertStorageLimit(totalUploadSize, storageUsed, storageLimit) {
|
|
1081
|
+
const used = BigInt(storageUsed);
|
|
1082
|
+
const limit = BigInt(storageLimit);
|
|
1083
|
+
const upload = BigInt(totalUploadSize);
|
|
1084
|
+
if (used + upload > limit) {
|
|
1085
|
+
const available = limit > used ? Number(limit - used) : 0;
|
|
1086
|
+
throw new PlanLimitError(
|
|
1087
|
+
`Upload size (${formatBytes(totalUploadSize)}) exceeds available storage (${formatBytes(available)} of ${formatBytes(Number(limit))} remaining).`,
|
|
1088
|
+
`Upgrade with: anonli subscribe`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function warnExpiryCap(requestedDays, maxDays) {
|
|
1093
|
+
if (requestedDays > maxDays) {
|
|
1094
|
+
warn(
|
|
1095
|
+
`Expiry capped to ${maxDays} days on your plan (requested ${requestedDays}).`
|
|
1096
|
+
);
|
|
1097
|
+
return maxDays;
|
|
1098
|
+
}
|
|
1099
|
+
return requestedDays;
|
|
1100
|
+
}
|
|
1101
|
+
function capitalize(s) {
|
|
1102
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/commands/drop-upload.ts
|
|
1106
|
+
init_config2();
|
|
1107
|
+
|
|
1108
|
+
// src/lib/crypto.ts
|
|
1109
|
+
init_constants();
|
|
1110
|
+
import { webcrypto } from "crypto";
|
|
1111
|
+
import { argon2id } from "hash-wasm";
|
|
1112
|
+
var subtle = webcrypto.subtle;
|
|
1113
|
+
var getRandomValues = webcrypto.getRandomValues.bind(webcrypto);
|
|
1114
|
+
var ALGORITHM = { name: "AES-GCM", length: 256 };
|
|
1115
|
+
async function generateKey() {
|
|
1116
|
+
const key = await subtle.generateKey(ALGORITHM, true, [
|
|
1117
|
+
"encrypt",
|
|
1118
|
+
"decrypt"
|
|
1119
|
+
]);
|
|
1120
|
+
const exported = await subtle.exportKey("raw", key);
|
|
1121
|
+
return arrayBufferToBase64Url(exported);
|
|
1122
|
+
}
|
|
1123
|
+
async function importKey(keyString) {
|
|
1124
|
+
const keyData = base64UrlToArrayBuffer(keyString);
|
|
1125
|
+
return subtle.importKey("raw", keyData, ALGORITHM, true, [
|
|
1126
|
+
"encrypt",
|
|
1127
|
+
"decrypt"
|
|
1128
|
+
]);
|
|
1129
|
+
}
|
|
1130
|
+
async function createEncryptionContext() {
|
|
1131
|
+
const keyString = await generateKey();
|
|
1132
|
+
const key = await importKey(keyString);
|
|
1133
|
+
const ivString = generateBaseIv();
|
|
1134
|
+
const baseIv = new Uint8Array(base64UrlToArrayBuffer(ivString));
|
|
1135
|
+
return { key, keyString, baseIv, ivString };
|
|
1136
|
+
}
|
|
1137
|
+
async function encryptChunk(chunk, key, baseIv, chunkIndex) {
|
|
1138
|
+
const iv = generateChunkIv(baseIv, chunkIndex);
|
|
1139
|
+
return subtle.encrypt({ name: "AES-GCM", iv: getView(iv) }, key, chunk);
|
|
1140
|
+
}
|
|
1141
|
+
async function decryptChunk(encryptedChunk, key, baseIv, chunkIndex) {
|
|
1142
|
+
const iv = generateChunkIv(baseIv, chunkIndex);
|
|
1143
|
+
return subtle.decrypt(
|
|
1144
|
+
{ name: "AES-GCM", iv: getView(iv) },
|
|
1145
|
+
key,
|
|
1146
|
+
encryptedChunk
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
async function encryptFilename(filename, key, iv) {
|
|
1150
|
+
const encoder = new TextEncoder();
|
|
1151
|
+
const filenameBuffer = encoder.encode(filename);
|
|
1152
|
+
const filenameIv = generateChunkIv(iv, 4294967295);
|
|
1153
|
+
const encrypted = await subtle.encrypt(
|
|
1154
|
+
{ name: "AES-GCM", iv: getView(filenameIv) },
|
|
1155
|
+
key,
|
|
1156
|
+
filenameBuffer
|
|
1157
|
+
);
|
|
1158
|
+
return arrayBufferToBase64Url(encrypted);
|
|
1159
|
+
}
|
|
1160
|
+
async function decryptFilename(encryptedFilename, key, iv) {
|
|
1161
|
+
const encrypted = base64UrlToArrayBuffer(encryptedFilename);
|
|
1162
|
+
const filenameIv = generateChunkIv(iv, 4294967295);
|
|
1163
|
+
const decrypted = await subtle.decrypt(
|
|
1164
|
+
{ name: "AES-GCM", iv: getView(filenameIv) },
|
|
1165
|
+
key,
|
|
1166
|
+
encrypted
|
|
1167
|
+
);
|
|
1168
|
+
return new TextDecoder().decode(decrypted);
|
|
1169
|
+
}
|
|
1170
|
+
async function encryptKeyWithPassword(keyString, password) {
|
|
1171
|
+
const salt = generateSalt();
|
|
1172
|
+
const wrappingKey = await deriveKeyFromPassword(password, salt);
|
|
1173
|
+
const iv = generateBaseIv();
|
|
1174
|
+
const ivBuffer = new Uint8Array(base64UrlToArrayBuffer(iv));
|
|
1175
|
+
const encoder = new TextEncoder();
|
|
1176
|
+
const keyData = encoder.encode(keyString);
|
|
1177
|
+
const encrypted = await subtle.encrypt(
|
|
1178
|
+
{ name: "AES-GCM", iv: ivBuffer },
|
|
1179
|
+
wrappingKey,
|
|
1180
|
+
keyData
|
|
1181
|
+
);
|
|
1182
|
+
return {
|
|
1183
|
+
encryptedKey: arrayBufferToBase64Url(encrypted),
|
|
1184
|
+
iv,
|
|
1185
|
+
salt
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
async function decryptKeyWithPassword(encryptedKey, password, salt, iv) {
|
|
1189
|
+
const wrappingKey = await deriveKeyFromPassword(password, salt);
|
|
1190
|
+
const ivBuffer = new Uint8Array(base64UrlToArrayBuffer(iv));
|
|
1191
|
+
const encryptedData = base64UrlToArrayBuffer(encryptedKey);
|
|
1192
|
+
const decrypted = await subtle.decrypt(
|
|
1193
|
+
{ name: "AES-GCM", iv: ivBuffer },
|
|
1194
|
+
wrappingKey,
|
|
1195
|
+
encryptedData
|
|
1196
|
+
);
|
|
1197
|
+
return new TextDecoder().decode(decrypted);
|
|
1198
|
+
}
|
|
1199
|
+
function getChunkParams(fileSize) {
|
|
1200
|
+
if (fileSize <= MIN_CHUNK_SIZE) {
|
|
1201
|
+
return { chunkSize: fileSize || 1, chunkCount: 1 };
|
|
1202
|
+
}
|
|
1203
|
+
const chunksNeeded = Math.ceil(fileSize / MIN_CHUNK_SIZE);
|
|
1204
|
+
if (chunksNeeded <= MAX_CHUNKS_PER_FILE) {
|
|
1205
|
+
return {
|
|
1206
|
+
chunkSize: Math.ceil(fileSize / chunksNeeded),
|
|
1207
|
+
chunkCount: chunksNeeded
|
|
1208
|
+
};
|
|
1209
|
+
} else {
|
|
1210
|
+
return {
|
|
1211
|
+
chunkSize: Math.ceil(fileSize / MAX_CHUNKS_PER_FILE),
|
|
1212
|
+
chunkCount: MAX_CHUNKS_PER_FILE
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function calculateEncryptedSize(originalSize, chunkSize) {
|
|
1217
|
+
const chunkCount = Math.ceil(originalSize / chunkSize);
|
|
1218
|
+
return originalSize + chunkCount * AUTH_TAG_SIZE;
|
|
1219
|
+
}
|
|
1220
|
+
function getConcurrency(fileSize) {
|
|
1221
|
+
return fileSize < FILE_SIZE_THRESHOLD_1GB ? 3 : 5;
|
|
1222
|
+
}
|
|
1223
|
+
function generateChunkIv(baseIv, chunkIndex) {
|
|
1224
|
+
const iv = new Uint8Array(IV_LENGTH);
|
|
1225
|
+
iv.set(baseIv.slice(0, 8));
|
|
1226
|
+
const view = new DataView(iv.buffer);
|
|
1227
|
+
view.setUint32(8, chunkIndex, false);
|
|
1228
|
+
return iv;
|
|
1229
|
+
}
|
|
1230
|
+
function generateBaseIv() {
|
|
1231
|
+
const iv = getRandomValues(new Uint8Array(IV_LENGTH));
|
|
1232
|
+
return arrayBufferToBase64Url(iv);
|
|
1233
|
+
}
|
|
1234
|
+
function generateSalt() {
|
|
1235
|
+
const salt = getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
1236
|
+
return arrayBufferToBase64Url(salt);
|
|
1237
|
+
}
|
|
1238
|
+
async function deriveKeyFromPassword(password, salt) {
|
|
1239
|
+
const saltBytes = new Uint8Array(base64UrlToArrayBuffer(salt));
|
|
1240
|
+
const hash = await argon2id({
|
|
1241
|
+
password,
|
|
1242
|
+
salt: saltBytes,
|
|
1243
|
+
memorySize: ARGON2_MEMORY,
|
|
1244
|
+
iterations: ARGON2_TIME,
|
|
1245
|
+
parallelism: ARGON2_PARALLELISM,
|
|
1246
|
+
hashLength: ARGON2_HASH_LENGTH,
|
|
1247
|
+
outputType: "binary"
|
|
1248
|
+
});
|
|
1249
|
+
return subtle.importKey(
|
|
1250
|
+
"raw",
|
|
1251
|
+
hash,
|
|
1252
|
+
ALGORITHM,
|
|
1253
|
+
true,
|
|
1254
|
+
["encrypt", "decrypt"]
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
function getView(arr) {
|
|
1258
|
+
if (arr instanceof ArrayBuffer) return arr;
|
|
1259
|
+
return arr.buffer.slice(
|
|
1260
|
+
arr.byteOffset,
|
|
1261
|
+
arr.byteOffset + arr.byteLength
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
function arrayBufferToBase64Url(buffer) {
|
|
1265
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
1266
|
+
return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1267
|
+
}
|
|
1268
|
+
function base64UrlToArrayBuffer(base64url) {
|
|
1269
|
+
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
1270
|
+
while (base64.length % 4) {
|
|
1271
|
+
base64 += "=";
|
|
1272
|
+
}
|
|
1273
|
+
const buf = Buffer.from(base64, "base64");
|
|
1274
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/commands/drop-upload.ts
|
|
1278
|
+
import { lookup } from "mime-types";
|
|
1279
|
+
function collectFiles(inputPath) {
|
|
1280
|
+
const stat = fs2.statSync(inputPath);
|
|
1281
|
+
if (stat.isFile()) {
|
|
1282
|
+
return [
|
|
1283
|
+
{
|
|
1284
|
+
absolutePath: inputPath,
|
|
1285
|
+
relativeName: path2.basename(inputPath),
|
|
1286
|
+
size: stat.size
|
|
1287
|
+
}
|
|
1288
|
+
];
|
|
1289
|
+
}
|
|
1290
|
+
if (stat.isDirectory()) {
|
|
1291
|
+
const entries = [];
|
|
1292
|
+
const walk = (dir, prefix) => {
|
|
1293
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
1294
|
+
const fullPath = path2.join(dir, entry.name);
|
|
1295
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1296
|
+
if (entry.isFile()) {
|
|
1297
|
+
entries.push({
|
|
1298
|
+
absolutePath: fullPath,
|
|
1299
|
+
relativeName: relPath,
|
|
1300
|
+
size: fs2.statSync(fullPath).size
|
|
1301
|
+
});
|
|
1302
|
+
} else if (entry.isDirectory()) {
|
|
1303
|
+
walk(fullPath, relPath);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
walk(inputPath, "");
|
|
1308
|
+
return entries;
|
|
1309
|
+
}
|
|
1310
|
+
throw new Error(`${inputPath} is not a file or directory`);
|
|
1311
|
+
}
|
|
1312
|
+
function checkPasswordStrength(password) {
|
|
1313
|
+
if (password.length < 12) {
|
|
1314
|
+
warn(`Password is short (${password.length} chars). At least 12 characters recommended.`);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const uniqueChars = new Set(password).size;
|
|
1318
|
+
const entropy = password.length * Math.log2(Math.max(uniqueChars, 2));
|
|
1319
|
+
if (entropy < 40) {
|
|
1320
|
+
warn("Password may be weak (low entropy). Consider a longer, more varied passphrase.");
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
var dropUploadCommand = new Command4("upload").argument("[path]", "File or folder to upload (omit to read from stdin)").description("Create an encrypted drop").option("-t, --title <title>", "Drop title").option("-m, --message <message>", "Drop message").option("-e, --expiry <days>", "Expiry in days", parseInt).option("-n, --max-downloads <n>", "Max download count", parseInt).option("-p, --password <password>", "Password-protect the drop").option("--name <filename>", "Filename when reading from stdin").option("--hide-branding", "Hide anon.li branding on download page").option("--notify", "Send email notification when files are downloaded").action(async (inputPath, options) => {
|
|
1324
|
+
requireAuth();
|
|
1325
|
+
let files;
|
|
1326
|
+
let stdinTmpFile;
|
|
1327
|
+
if (!inputPath) {
|
|
1328
|
+
if (process.stdin.isTTY) {
|
|
1329
|
+
error("No path provided. Provide a file/folder path, or pipe data via stdin.");
|
|
1330
|
+
process.exit(1);
|
|
1331
|
+
}
|
|
1332
|
+
if (!options.name) {
|
|
1333
|
+
error("--name <filename> is required when uploading from stdin.");
|
|
1334
|
+
process.exit(1);
|
|
1335
|
+
}
|
|
1336
|
+
const spin = spinner("Reading from stdin...");
|
|
1337
|
+
const chunks = [];
|
|
1338
|
+
for await (const chunk of process.stdin) {
|
|
1339
|
+
chunks.push(chunk);
|
|
1340
|
+
}
|
|
1341
|
+
const data = Buffer.concat(chunks);
|
|
1342
|
+
spin.stop();
|
|
1343
|
+
stdinTmpFile = path2.join(os2.tmpdir(), `anonli-stdin-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
1344
|
+
fs2.writeFileSync(stdinTmpFile, data, { mode: 384 });
|
|
1345
|
+
files = [{
|
|
1346
|
+
absolutePath: stdinTmpFile,
|
|
1347
|
+
relativeName: options.name,
|
|
1348
|
+
size: data.length,
|
|
1349
|
+
tmpFile: true
|
|
1350
|
+
}];
|
|
1351
|
+
} else {
|
|
1352
|
+
const resolved = path2.resolve(inputPath);
|
|
1353
|
+
if (!fs2.existsSync(resolved)) {
|
|
1354
|
+
error(`Path not found: ${resolved}`);
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1357
|
+
files = collectFiles(resolved);
|
|
1358
|
+
}
|
|
1359
|
+
if (files.length === 0) {
|
|
1360
|
+
error("No files found.");
|
|
1361
|
+
process.exit(1);
|
|
1362
|
+
}
|
|
1363
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
|
|
1364
|
+
info(
|
|
1365
|
+
`${files.length} file(s), ${formatBytes(totalSize)} total`
|
|
1366
|
+
);
|
|
1367
|
+
if (options.password) {
|
|
1368
|
+
checkPasswordStrength(options.password);
|
|
1369
|
+
}
|
|
1370
|
+
const limitSpin = spinner("Checking plan limits...");
|
|
1371
|
+
const plan = await fetchPlanInfo();
|
|
1372
|
+
limitSpin.stop();
|
|
1373
|
+
if (options.hideBranding) {
|
|
1374
|
+
assertFeature(plan.features, "noBranding", "--hide-branding");
|
|
1375
|
+
}
|
|
1376
|
+
if (options.notify) {
|
|
1377
|
+
assertFeature(plan.features, "downloadNotifications", "--notify");
|
|
1378
|
+
}
|
|
1379
|
+
if (options.password) {
|
|
1380
|
+
assertFeature(plan.features, "customKey", "--password");
|
|
1381
|
+
}
|
|
1382
|
+
assertStorageLimit(totalSize, plan.storage.used, plan.storage.limit);
|
|
1383
|
+
if (options.expiry) {
|
|
1384
|
+
options.expiry = warnExpiryCap(options.expiry, plan.limits.max_expiry_days);
|
|
1385
|
+
}
|
|
1386
|
+
try {
|
|
1387
|
+
const ctx = await createEncryptionContext();
|
|
1388
|
+
const { key, keyString, baseIv, ivString } = ctx;
|
|
1389
|
+
let customKey = false;
|
|
1390
|
+
let salt;
|
|
1391
|
+
let customKeyData;
|
|
1392
|
+
let customKeyIv;
|
|
1393
|
+
if (options.password) {
|
|
1394
|
+
const protection = await encryptKeyWithPassword(
|
|
1395
|
+
keyString,
|
|
1396
|
+
options.password
|
|
1397
|
+
);
|
|
1398
|
+
customKey = true;
|
|
1399
|
+
salt = protection.salt;
|
|
1400
|
+
customKeyData = protection.encryptedKey;
|
|
1401
|
+
customKeyIv = protection.iv;
|
|
1402
|
+
}
|
|
1403
|
+
let encryptedTitle;
|
|
1404
|
+
let encryptedMessage;
|
|
1405
|
+
if (options.title) {
|
|
1406
|
+
encryptedTitle = await encryptFilename(
|
|
1407
|
+
options.title,
|
|
1408
|
+
key,
|
|
1409
|
+
baseIv
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
if (options.message) {
|
|
1413
|
+
encryptedMessage = await encryptFilename(
|
|
1414
|
+
options.message,
|
|
1415
|
+
key,
|
|
1416
|
+
baseIv
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
const createSpin = spinner("Creating drop...");
|
|
1420
|
+
const createRes = await apiFetch("/api/v1/drop", {
|
|
1421
|
+
method: "POST",
|
|
1422
|
+
body: JSON.stringify({
|
|
1423
|
+
iv: ivString,
|
|
1424
|
+
fileCount: files.length,
|
|
1425
|
+
...encryptedTitle && { encryptedTitle },
|
|
1426
|
+
...encryptedMessage && { encryptedMessage },
|
|
1427
|
+
...options.expiry && { expiry: options.expiry },
|
|
1428
|
+
...options.maxDownloads && { maxDownloads: options.maxDownloads },
|
|
1429
|
+
...options.hideBranding && { hideBranding: true },
|
|
1430
|
+
...options.notify && { notifyOnDownload: true },
|
|
1431
|
+
...customKey && {
|
|
1432
|
+
customKey: true,
|
|
1433
|
+
salt,
|
|
1434
|
+
customKeyData,
|
|
1435
|
+
customKeyIv
|
|
1436
|
+
}
|
|
1437
|
+
})
|
|
1438
|
+
});
|
|
1439
|
+
if (!createRes.ok) {
|
|
1440
|
+
const err = await createRes.json().catch(() => ({}));
|
|
1441
|
+
createSpin.fail("Failed to create drop");
|
|
1442
|
+
error(
|
|
1443
|
+
err?.error?.message || err?.error || "Unknown error"
|
|
1444
|
+
);
|
|
1445
|
+
process.exit(1);
|
|
1446
|
+
}
|
|
1447
|
+
const createData = await createRes.json();
|
|
1448
|
+
const { drop_id: dropId, session_token: sessionToken } = createData.data;
|
|
1449
|
+
createSpin.succeed(`Drop created: ${dropId}`);
|
|
1450
|
+
const fileChunkRecords = [];
|
|
1451
|
+
for (let i = 0; i < files.length; i++) {
|
|
1452
|
+
const file = files[i];
|
|
1453
|
+
const { chunkSize, chunkCount } = getChunkParams(file.size);
|
|
1454
|
+
const encryptedSize = calculateEncryptedSize(
|
|
1455
|
+
file.size,
|
|
1456
|
+
chunkSize
|
|
1457
|
+
);
|
|
1458
|
+
const encryptedName = await encryptFilename(
|
|
1459
|
+
file.relativeName,
|
|
1460
|
+
key,
|
|
1461
|
+
baseIv
|
|
1462
|
+
);
|
|
1463
|
+
const addRes = await apiFetch(`/api/v1/drop/${dropId}/file`, {
|
|
1464
|
+
method: "POST",
|
|
1465
|
+
body: JSON.stringify({
|
|
1466
|
+
size: encryptedSize,
|
|
1467
|
+
encryptedName,
|
|
1468
|
+
iv: ivString,
|
|
1469
|
+
mimeType: lookup(file.absolutePath) || "application/octet-stream",
|
|
1470
|
+
chunkCount,
|
|
1471
|
+
chunkSize,
|
|
1472
|
+
...sessionToken && { sessionToken }
|
|
1473
|
+
})
|
|
1474
|
+
});
|
|
1475
|
+
if (!addRes.ok) {
|
|
1476
|
+
const err = await addRes.json().catch(() => ({}));
|
|
1477
|
+
error(
|
|
1478
|
+
`Failed to add file ${file.relativeName}: ${err?.error || "Unknown error"}`
|
|
1479
|
+
);
|
|
1480
|
+
process.exit(1);
|
|
1481
|
+
}
|
|
1482
|
+
const addData = await addRes.json();
|
|
1483
|
+
const { fileId, uploadUrls } = addData;
|
|
1484
|
+
const bar = progressBar(
|
|
1485
|
+
chunkCount,
|
|
1486
|
+
`${files.length > 1 ? `[${i + 1}/${files.length}] ` : ""}${file.relativeName}`
|
|
1487
|
+
);
|
|
1488
|
+
const chunks = [];
|
|
1489
|
+
const concurrency = getConcurrency(file.size);
|
|
1490
|
+
const fd = fs2.openSync(file.absolutePath, "r");
|
|
1491
|
+
try {
|
|
1492
|
+
let nextChunk = 0;
|
|
1493
|
+
async function processChunk(chunkIndex) {
|
|
1494
|
+
const start = chunkIndex * chunkSize;
|
|
1495
|
+
const end = Math.min(start + chunkSize, file.size);
|
|
1496
|
+
const length = end - start;
|
|
1497
|
+
const buffer = Buffer.alloc(length);
|
|
1498
|
+
fs2.readSync(fd, buffer, 0, length, start);
|
|
1499
|
+
const encrypted = await encryptChunk(
|
|
1500
|
+
buffer.buffer.slice(
|
|
1501
|
+
buffer.byteOffset,
|
|
1502
|
+
buffer.byteOffset + buffer.byteLength
|
|
1503
|
+
),
|
|
1504
|
+
key,
|
|
1505
|
+
baseIv,
|
|
1506
|
+
chunkIndex
|
|
1507
|
+
);
|
|
1508
|
+
const presignedUrl = uploadUrls[String(chunkIndex + 1)];
|
|
1509
|
+
let url = presignedUrl;
|
|
1510
|
+
const headers = {};
|
|
1511
|
+
if (url.includes("/relay/") && url.includes("?")) {
|
|
1512
|
+
const splitIndex = url.indexOf("?");
|
|
1513
|
+
const baseUrl2 = url.slice(0, splitIndex);
|
|
1514
|
+
const query = url.slice(splitIndex + 1);
|
|
1515
|
+
url = baseUrl2;
|
|
1516
|
+
headers["X-Relay-Query"] = query;
|
|
1517
|
+
}
|
|
1518
|
+
const uploadRes = await apiRawFetch(url, {
|
|
1519
|
+
method: "PUT",
|
|
1520
|
+
headers,
|
|
1521
|
+
body: new Uint8Array(encrypted)
|
|
1522
|
+
});
|
|
1523
|
+
if (!uploadRes.ok) {
|
|
1524
|
+
throw new Error(
|
|
1525
|
+
`Failed to upload chunk ${chunkIndex} of ${file.relativeName}`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
const etag = uploadRes.headers.get("ETag") || "";
|
|
1529
|
+
chunks.push({ chunkIndex, etag });
|
|
1530
|
+
bar.increment();
|
|
1531
|
+
}
|
|
1532
|
+
let firstError = null;
|
|
1533
|
+
const running = [];
|
|
1534
|
+
while (nextChunk < chunkCount && !firstError) {
|
|
1535
|
+
while (running.length < concurrency && nextChunk < chunkCount && !firstError) {
|
|
1536
|
+
const idx = nextChunk++;
|
|
1537
|
+
const p = processChunk(idx).then(() => {
|
|
1538
|
+
running.splice(running.indexOf(p), 1);
|
|
1539
|
+
}).catch((err) => {
|
|
1540
|
+
firstError = err instanceof Error ? err : new Error(String(err));
|
|
1541
|
+
running.splice(running.indexOf(p), 1);
|
|
1542
|
+
});
|
|
1543
|
+
running.push(p);
|
|
1544
|
+
}
|
|
1545
|
+
if (running.length > 0) {
|
|
1546
|
+
await Promise.race(running);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (running.length > 0) {
|
|
1550
|
+
await Promise.allSettled(running);
|
|
1551
|
+
}
|
|
1552
|
+
if (firstError) {
|
|
1553
|
+
throw firstError;
|
|
1554
|
+
}
|
|
1555
|
+
} finally {
|
|
1556
|
+
fs2.closeSync(fd);
|
|
1557
|
+
}
|
|
1558
|
+
bar.stop();
|
|
1559
|
+
chunks.sort((a, b) => a.chunkIndex - b.chunkIndex);
|
|
1560
|
+
fileChunkRecords.push({ fileId, chunks });
|
|
1561
|
+
}
|
|
1562
|
+
const finishSpin = spinner("Finalizing drop...");
|
|
1563
|
+
const finishRes = await apiFetch(
|
|
1564
|
+
`/api/v1/drop/${dropId}?action=finish`,
|
|
1565
|
+
{
|
|
1566
|
+
method: "PATCH",
|
|
1567
|
+
body: JSON.stringify({
|
|
1568
|
+
files: fileChunkRecords,
|
|
1569
|
+
...sessionToken && { sessionToken }
|
|
1570
|
+
})
|
|
1571
|
+
}
|
|
1572
|
+
);
|
|
1573
|
+
if (!finishRes.ok) {
|
|
1574
|
+
const err = await finishRes.json().catch(() => ({}));
|
|
1575
|
+
finishSpin.fail("Failed to finalize drop");
|
|
1576
|
+
error(
|
|
1577
|
+
err?.error || "Unknown error"
|
|
1578
|
+
);
|
|
1579
|
+
process.exit(1);
|
|
1580
|
+
}
|
|
1581
|
+
finishSpin.stop();
|
|
1582
|
+
const baseUrl = getBaseUrl();
|
|
1583
|
+
const shareUrl = customKey ? `${baseUrl}/d/${dropId}` : `${baseUrl}/d/${dropId}#${keyString}`;
|
|
1584
|
+
const boxLines = [
|
|
1585
|
+
`${c.secondary("URL:")} ${link(shareUrl)}`,
|
|
1586
|
+
`${c.secondary("Files:")} ${c.primary(String(files.length))}`,
|
|
1587
|
+
`${c.secondary("Size:")} ${c.primary(formatBytes(totalSize))}`
|
|
1588
|
+
];
|
|
1589
|
+
if (options.expiry) {
|
|
1590
|
+
boxLines.push(`${c.secondary("Expiry:")} ${c.primary(String(options.expiry))} days`);
|
|
1591
|
+
}
|
|
1592
|
+
if (options.maxDownloads) {
|
|
1593
|
+
boxLines.push(`${c.secondary("Max downloads:")} ${c.primary(String(options.maxDownloads))}`);
|
|
1594
|
+
}
|
|
1595
|
+
if (options.hideBranding) {
|
|
1596
|
+
boxLines.push(`${c.secondary("Branding:")} ${c.muted("hidden")}`);
|
|
1597
|
+
}
|
|
1598
|
+
if (options.notify) {
|
|
1599
|
+
boxLines.push(`${c.secondary("Notifications:")} ${c.accent("enabled")}`);
|
|
1600
|
+
}
|
|
1601
|
+
successBox("Drop Created", boxLines.join("\n"));
|
|
1602
|
+
spacer();
|
|
1603
|
+
if (!customKey) {
|
|
1604
|
+
warn(
|
|
1605
|
+
"Save this URL \u2014 the key after # is required to decrypt."
|
|
1606
|
+
);
|
|
1607
|
+
} else {
|
|
1608
|
+
info("Password-protected. Share the URL and password separately.");
|
|
1609
|
+
}
|
|
1610
|
+
} catch (err) {
|
|
1611
|
+
error(err instanceof Error ? err.message : "Upload failed");
|
|
1612
|
+
process.exit(1);
|
|
1613
|
+
} finally {
|
|
1614
|
+
if (stdinTmpFile) {
|
|
1615
|
+
try {
|
|
1616
|
+
fs2.unlinkSync(stdinTmpFile);
|
|
1617
|
+
} catch {
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
// src/commands/drop-list.ts
|
|
1624
|
+
init_api();
|
|
1625
|
+
import { Command as Command5 } from "commander";
|
|
1626
|
+
var dropListCommand = new Command5("list").alias("ls").description("List your drops").option("--limit <n>", "Number of drops to fetch", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--expired", "Show only expired drops").option("--disabled", "Show only disabled drops").option("--enabled", "Show only enabled drops").option("--sort <field>", "Sort by: created, expiry, size, downloads", "created").option("--order <dir>", "Sort direction: asc, desc", "desc").option("--json", "Output raw JSON").action(async (options) => {
|
|
1627
|
+
requireAuth();
|
|
1628
|
+
const spin = spinner("Fetching drops...");
|
|
1629
|
+
try {
|
|
1630
|
+
const limit = options.limit ?? 50;
|
|
1631
|
+
const offset = options.offset ?? 0;
|
|
1632
|
+
const result = await apiGetList(
|
|
1633
|
+
`/api/v1/drop?limit=${limit}&offset=${offset}`
|
|
1634
|
+
);
|
|
1635
|
+
spin.stop();
|
|
1636
|
+
if (options.json) {
|
|
1637
|
+
outputJson(result.data);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
let data = result.data;
|
|
1641
|
+
const now = /* @__PURE__ */ new Date();
|
|
1642
|
+
if (options.expired) {
|
|
1643
|
+
data = data.filter((d) => d.expires_at && new Date(d.expires_at) < now);
|
|
1644
|
+
}
|
|
1645
|
+
if (options.disabled) {
|
|
1646
|
+
data = data.filter((d) => d.disabled);
|
|
1647
|
+
}
|
|
1648
|
+
if (options.enabled) {
|
|
1649
|
+
data = data.filter((d) => !d.disabled);
|
|
1650
|
+
}
|
|
1651
|
+
if (options.sort) {
|
|
1652
|
+
data = [...data].sort((a, b) => {
|
|
1653
|
+
let cmp = 0;
|
|
1654
|
+
switch (options.sort) {
|
|
1655
|
+
case "expiry":
|
|
1656
|
+
cmp = (a.expires_at ?? "").localeCompare(b.expires_at ?? "");
|
|
1657
|
+
break;
|
|
1658
|
+
case "size":
|
|
1659
|
+
cmp = parseInt(a.totalSize || "0") - parseInt(b.totalSize || "0");
|
|
1660
|
+
break;
|
|
1661
|
+
case "downloads":
|
|
1662
|
+
cmp = a.downloads - b.downloads;
|
|
1663
|
+
break;
|
|
1664
|
+
default:
|
|
1665
|
+
cmp = a.created_at.localeCompare(b.created_at);
|
|
1666
|
+
}
|
|
1667
|
+
return options.order === "asc" ? cmp : -cmp;
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
if (data.length === 0) {
|
|
1671
|
+
box(
|
|
1672
|
+
`${c.secondary("No drops found.")}
|
|
1673
|
+
${c.muted("Create one with")} ${c.accent("anonli drop upload <file>")}`,
|
|
1674
|
+
{ title: c.info("Drops") }
|
|
1675
|
+
);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
table(
|
|
1679
|
+
["ID", "Status", "Files", "Size", "Downloads", "Expires", "Created"],
|
|
1680
|
+
data.map((d) => [
|
|
1681
|
+
d.id,
|
|
1682
|
+
d.disabled ? statusBadge("Disabled", "inactive") : statusBadge("Active", "active"),
|
|
1683
|
+
String(d.fileCount),
|
|
1684
|
+
formatBytes(parseInt(d.totalSize || "0")),
|
|
1685
|
+
d.maxDownloads ? `${d.downloads}/${d.maxDownloads}` : String(d.downloads),
|
|
1686
|
+
d.expires_at ? formatDate(d.expires_at) : dim("never"),
|
|
1687
|
+
formatDate(d.created_at)
|
|
1688
|
+
])
|
|
1689
|
+
);
|
|
1690
|
+
const storage = result.meta?.storage;
|
|
1691
|
+
if (storage) {
|
|
1692
|
+
console.log(
|
|
1693
|
+
dim(
|
|
1694
|
+
`
|
|
1695
|
+
Storage: ${formatBytes(parseInt(storage.used))}/${formatBytes(parseInt(storage.limit))}`
|
|
1696
|
+
)
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
console.log(dim(` ${result.total} drop(s) total${data.length < result.data.length ? `, ${data.length} shown after filter` : ""}`));
|
|
1700
|
+
showRateLimit(result.rateLimit);
|
|
1701
|
+
} catch (err) {
|
|
1702
|
+
spin.fail("Failed to list drops.");
|
|
1703
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
1704
|
+
process.exit(1);
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
// src/commands/drop-delete.ts
|
|
1709
|
+
init_api();
|
|
1710
|
+
import { Command as Command6 } from "commander";
|
|
1711
|
+
|
|
1712
|
+
// src/lib/drop-url.ts
|
|
1713
|
+
function parseDropIdentifier(input) {
|
|
1714
|
+
try {
|
|
1715
|
+
const parsed = new URL(input);
|
|
1716
|
+
const pathParts = parsed.pathname.split("/").filter(Boolean);
|
|
1717
|
+
const dIndex = pathParts.indexOf("d");
|
|
1718
|
+
if (dIndex === -1 || !pathParts[dIndex + 1]) {
|
|
1719
|
+
throw new Error("Invalid URL format");
|
|
1720
|
+
}
|
|
1721
|
+
return {
|
|
1722
|
+
dropId: pathParts[dIndex + 1],
|
|
1723
|
+
key: parsed.hash.slice(1) || null
|
|
1724
|
+
};
|
|
1725
|
+
} catch {
|
|
1726
|
+
return { dropId: input, key: null };
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/commands/drop-delete.ts
|
|
1731
|
+
var dropDeleteCommand = new Command6("delete").alias("rm").description("Delete a drop").argument("<target>", "Drop URL or drop ID").option("-f, --force", "Skip confirmation").action(async (target, options) => {
|
|
1732
|
+
const { dropId } = parseDropIdentifier(target);
|
|
1733
|
+
requireAuth();
|
|
1734
|
+
if (!options.force) {
|
|
1735
|
+
const confirmed = await confirm(
|
|
1736
|
+
`Delete drop ${bold(dropId)}? This cannot be undone.`
|
|
1737
|
+
);
|
|
1738
|
+
if (!confirmed) {
|
|
1739
|
+
info("Cancelled.");
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
const spin = spinner("Deleting drop...");
|
|
1744
|
+
try {
|
|
1745
|
+
const result = await apiDelete(`/api/v1/drop/${dropId}`);
|
|
1746
|
+
spin.succeed(`Deleted drop ${dropId}`);
|
|
1747
|
+
showRateLimit(result.rateLimit);
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
spin.fail("Failed to delete drop.");
|
|
1750
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
1751
|
+
process.exit(1);
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
// src/commands/drop-download.ts
|
|
1756
|
+
import { Command as Command7 } from "commander";
|
|
1757
|
+
import fs3 from "fs";
|
|
1758
|
+
import path3 from "path";
|
|
1759
|
+
init_constants();
|
|
1760
|
+
init_errors();
|
|
1761
|
+
init_config2();
|
|
1762
|
+
function sanitizeFilename(name) {
|
|
1763
|
+
return name.replace(/\.\.[/\\]/g, "").replace(/\0/g, "").replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").slice(0, 200) || "unnamed_file";
|
|
1764
|
+
}
|
|
1765
|
+
function resolveOutputPath(dir, safeName, overwrite, skip) {
|
|
1766
|
+
const outPath = path3.join(dir, safeName);
|
|
1767
|
+
if (!fs3.existsSync(outPath)) return outPath;
|
|
1768
|
+
if (overwrite) return outPath;
|
|
1769
|
+
if (skip) {
|
|
1770
|
+
info(`Skipping ${safeName} (already exists)`);
|
|
1771
|
+
return null;
|
|
1772
|
+
}
|
|
1773
|
+
const ext = path3.extname(safeName);
|
|
1774
|
+
const base = path3.basename(safeName, ext);
|
|
1775
|
+
for (let n = 1; n <= 1e3; n++) {
|
|
1776
|
+
const candidate = path3.join(dir, `${base}.${n}${ext}`);
|
|
1777
|
+
if (!fs3.existsSync(candidate)) return candidate;
|
|
1778
|
+
}
|
|
1779
|
+
throw new Error(`Cannot find available filename for ${safeName}`);
|
|
1780
|
+
}
|
|
1781
|
+
var dropDownloadCommand = new Command7("download").alias("dl").description("Download and decrypt a drop").argument("<target>", "Drop URL or drop ID").option("-o, --output <dir>", "Output directory", ".").option("-k, --key <key>", "Decryption key (required when using a drop ID)").option("-p, --password <password>", "Password for protected drops").option("--overwrite", "Overwrite existing files without prompting").option("--skip", "Skip files that already exist").action(async (target, options) => {
|
|
1782
|
+
const { dropId, key: urlKey } = parseDropIdentifier(target);
|
|
1783
|
+
let keyString = options.key || urlKey;
|
|
1784
|
+
const spin = spinner("Fetching drop metadata...");
|
|
1785
|
+
try {
|
|
1786
|
+
const baseUrl = getBaseUrl();
|
|
1787
|
+
const metaRes = await fetch(`${baseUrl}/api/v1/drop/${dropId}`);
|
|
1788
|
+
if (!metaRes.ok) {
|
|
1789
|
+
spin.fail("Drop not found or unavailable.");
|
|
1790
|
+
process.exit(metaRes.status === 404 ? EXIT_NOT_FOUND : 1);
|
|
1791
|
+
}
|
|
1792
|
+
const drop = await metaRes.json();
|
|
1793
|
+
spin.succeed(
|
|
1794
|
+
`Drop found: ${drop.files.length} file(s)`
|
|
1795
|
+
);
|
|
1796
|
+
if (drop.customKey && !keyString) {
|
|
1797
|
+
if (!drop.customKeyData || !drop.customKeyIv || !drop.salt) {
|
|
1798
|
+
error("Drop requires a password but key data is missing.");
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
}
|
|
1801
|
+
const password = options.password || await prompt("Password:", { mask: true });
|
|
1802
|
+
try {
|
|
1803
|
+
keyString = await decryptKeyWithPassword(
|
|
1804
|
+
drop.customKeyData,
|
|
1805
|
+
password,
|
|
1806
|
+
drop.salt,
|
|
1807
|
+
drop.customKeyIv
|
|
1808
|
+
);
|
|
1809
|
+
} catch {
|
|
1810
|
+
errorBox("Decryption Failed", "Incorrect password.");
|
|
1811
|
+
process.exit(1);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
if (!keyString) {
|
|
1815
|
+
errorBox(
|
|
1816
|
+
"Missing Key",
|
|
1817
|
+
"No decryption key provided. Use a URL with #<key> or pass --key <key>."
|
|
1818
|
+
);
|
|
1819
|
+
process.exit(1);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const key = await importKey(keyString);
|
|
1823
|
+
const iv = new Uint8Array(
|
|
1824
|
+
base64UrlToArrayBuffer(drop.iv)
|
|
1825
|
+
);
|
|
1826
|
+
const outDir = path3.resolve(options.output);
|
|
1827
|
+
fs3.mkdirSync(outDir, { recursive: true });
|
|
1828
|
+
let downloadedCount = 0;
|
|
1829
|
+
for (const file of drop.files) {
|
|
1830
|
+
let filename;
|
|
1831
|
+
try {
|
|
1832
|
+
filename = await decryptFilename(
|
|
1833
|
+
file.encryptedName,
|
|
1834
|
+
key,
|
|
1835
|
+
iv
|
|
1836
|
+
);
|
|
1837
|
+
} catch {
|
|
1838
|
+
warn(`Could not decrypt filename for file ${file.id.slice(0, 8)} \u2014 using fallback name`);
|
|
1839
|
+
filename = `file_${file.id}`;
|
|
1840
|
+
}
|
|
1841
|
+
const encryptedSize = parseInt(file.size);
|
|
1842
|
+
const chunkSize = file.chunkSize || MIN_CHUNK_SIZE;
|
|
1843
|
+
const chunkCount = file.chunkCount || Math.ceil(encryptedSize / (chunkSize + AUTH_TAG_SIZE));
|
|
1844
|
+
const encryptedChunkSize = chunkSize + AUTH_TAG_SIZE;
|
|
1845
|
+
const expectedDecryptedSize = encryptedSize - chunkCount * AUTH_TAG_SIZE;
|
|
1846
|
+
const safeName = sanitizeFilename(filename);
|
|
1847
|
+
fs3.mkdirSync(path3.dirname(path3.join(outDir, safeName)), { recursive: true });
|
|
1848
|
+
const outPath = resolveOutputPath(outDir, safeName, options.overwrite ?? false, options.skip ?? false);
|
|
1849
|
+
if (outPath === null) continue;
|
|
1850
|
+
const partPath = `${outPath}.anonli-dl`;
|
|
1851
|
+
let resumeFromChunk = 0;
|
|
1852
|
+
let resumeFromByte = 0;
|
|
1853
|
+
if (!options.overwrite && fs3.existsSync(partPath)) {
|
|
1854
|
+
const partSize = fs3.statSync(partPath).size;
|
|
1855
|
+
resumeFromChunk = Math.floor(partSize / chunkSize);
|
|
1856
|
+
resumeFromByte = resumeFromChunk * encryptedChunkSize;
|
|
1857
|
+
if (resumeFromChunk > 0) {
|
|
1858
|
+
info(`Resuming ${filename} from chunk ${resumeFromChunk}/${chunkCount}`);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
info(`Downloading ${c.accent(filename)} (${formatBytes(expectedDecryptedSize)})`);
|
|
1862
|
+
const downloadUrl = `${baseUrl}/api/v1/drop/${dropId}/file/${file.id}`;
|
|
1863
|
+
const fetchHeaders = {};
|
|
1864
|
+
if (resumeFromByte > 0) {
|
|
1865
|
+
fetchHeaders["Range"] = `bytes=${resumeFromByte}-`;
|
|
1866
|
+
}
|
|
1867
|
+
const response = await fetch(downloadUrl, { headers: fetchHeaders });
|
|
1868
|
+
if (!response.ok && response.status !== 206) {
|
|
1869
|
+
error(`Failed to download ${filename}: ${response.statusText}`);
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
if (resumeFromChunk > 0 && response.status === 200) {
|
|
1873
|
+
warn("Server doesn't support resume \u2014 restarting from beginning.");
|
|
1874
|
+
resumeFromChunk = 0;
|
|
1875
|
+
resumeFromByte = 0;
|
|
1876
|
+
}
|
|
1877
|
+
if (!response.body) {
|
|
1878
|
+
error(`No response body for ${filename}`);
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
const contentLength = parseInt(response.headers.get("Content-Length") || "0");
|
|
1882
|
+
let totalBytesReceived = 0;
|
|
1883
|
+
const writeStream = fs3.createWriteStream(partPath, {
|
|
1884
|
+
flags: resumeFromChunk > 0 ? "a" : "w"
|
|
1885
|
+
});
|
|
1886
|
+
const reader = response.body.getReader();
|
|
1887
|
+
let buffer = new Uint8Array(0);
|
|
1888
|
+
let chunkIndex = resumeFromChunk;
|
|
1889
|
+
let totalDecryptedBytes = resumeFromChunk * chunkSize;
|
|
1890
|
+
const bar = progressBar(chunkCount, filename);
|
|
1891
|
+
if (resumeFromChunk > 0) bar.update(resumeFromChunk);
|
|
1892
|
+
try {
|
|
1893
|
+
while (true) {
|
|
1894
|
+
const { done, value } = await reader.read();
|
|
1895
|
+
if (value) {
|
|
1896
|
+
const newBuffer = new Uint8Array(buffer.length + value.length);
|
|
1897
|
+
newBuffer.set(buffer);
|
|
1898
|
+
newBuffer.set(value, buffer.length);
|
|
1899
|
+
buffer = newBuffer;
|
|
1900
|
+
totalBytesReceived += value.length;
|
|
1901
|
+
}
|
|
1902
|
+
while (buffer.length >= encryptedChunkSize) {
|
|
1903
|
+
const encryptedChunk = buffer.slice(0, encryptedChunkSize);
|
|
1904
|
+
buffer = buffer.slice(encryptedChunkSize);
|
|
1905
|
+
const decrypted = await decryptChunk(
|
|
1906
|
+
encryptedChunk.buffer.slice(
|
|
1907
|
+
encryptedChunk.byteOffset,
|
|
1908
|
+
encryptedChunk.byteOffset + encryptedChunk.byteLength
|
|
1909
|
+
),
|
|
1910
|
+
key,
|
|
1911
|
+
iv,
|
|
1912
|
+
chunkIndex
|
|
1913
|
+
);
|
|
1914
|
+
writeStream.write(Buffer.from(decrypted));
|
|
1915
|
+
totalDecryptedBytes += decrypted.byteLength;
|
|
1916
|
+
chunkIndex++;
|
|
1917
|
+
if (chunkCount > 0) {
|
|
1918
|
+
bar.update(Math.min(chunkIndex, chunkCount));
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (done) {
|
|
1922
|
+
if (buffer.length > 0) {
|
|
1923
|
+
const decrypted = await decryptChunk(
|
|
1924
|
+
buffer.buffer.slice(
|
|
1925
|
+
buffer.byteOffset,
|
|
1926
|
+
buffer.byteOffset + buffer.byteLength
|
|
1927
|
+
),
|
|
1928
|
+
key,
|
|
1929
|
+
iv,
|
|
1930
|
+
chunkIndex
|
|
1931
|
+
);
|
|
1932
|
+
writeStream.write(Buffer.from(decrypted));
|
|
1933
|
+
totalDecryptedBytes += decrypted.byteLength;
|
|
1934
|
+
}
|
|
1935
|
+
break;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
await new Promise((resolve, reject) => {
|
|
1939
|
+
writeStream.end((err) => {
|
|
1940
|
+
if (err) reject(err);
|
|
1941
|
+
else resolve();
|
|
1942
|
+
});
|
|
1943
|
+
});
|
|
1944
|
+
bar.update(chunkCount);
|
|
1945
|
+
bar.stop();
|
|
1946
|
+
if (contentLength > 0 && totalBytesReceived !== contentLength) {
|
|
1947
|
+
warn(
|
|
1948
|
+
`Content-Length mismatch for ${filename}: expected ${contentLength} bytes, received ${totalBytesReceived} bytes`
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
if (expectedDecryptedSize > 0 && totalDecryptedBytes !== expectedDecryptedSize) {
|
|
1952
|
+
warn(
|
|
1953
|
+
`Size mismatch for ${filename}: expected ${formatBytes(expectedDecryptedSize)}, got ${formatBytes(totalDecryptedBytes)}`
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
fs3.renameSync(partPath, outPath);
|
|
1957
|
+
downloadedCount++;
|
|
1958
|
+
} catch (err) {
|
|
1959
|
+
bar.stop();
|
|
1960
|
+
writeStream.destroy();
|
|
1961
|
+
error(`Failed to decrypt ${filename}: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
successBox(
|
|
1965
|
+
"Download Complete",
|
|
1966
|
+
`${c.primary(String(downloadedCount))} ${c.secondary("file(s) decrypted to")} ${c.accent(path3.resolve(outDir))}`
|
|
1967
|
+
);
|
|
1968
|
+
} catch (err) {
|
|
1969
|
+
error(
|
|
1970
|
+
err instanceof Error ? err.message : "Download failed"
|
|
1971
|
+
);
|
|
1972
|
+
process.exit(1);
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
// src/commands/drop-toggle.ts
|
|
1977
|
+
init_api();
|
|
1978
|
+
import { Command as Command8 } from "commander";
|
|
1979
|
+
var dropToggleCommand = new Command8("toggle").description("Toggle a drop's enabled/disabled state").argument("<target>", "Drop URL or drop ID").action(async (target) => {
|
|
1980
|
+
const { dropId } = parseDropIdentifier(target);
|
|
1981
|
+
requireAuth();
|
|
1982
|
+
const spin = spinner("Toggling drop...");
|
|
1983
|
+
try {
|
|
1984
|
+
const result = await apiPatch(
|
|
1985
|
+
`/api/v1/drop/${dropId}?action=toggle`
|
|
1986
|
+
);
|
|
1987
|
+
if (result.data.disabled) {
|
|
1988
|
+
spin.succeed(`Drop ${dropId} is now ${statusBadge("disabled", "inactive")}`);
|
|
1989
|
+
} else {
|
|
1990
|
+
spin.succeed(`Drop ${dropId} is now ${statusBadge("enabled", "active")}`);
|
|
1991
|
+
}
|
|
1992
|
+
showRateLimit(result.rateLimit);
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
spin.fail("Failed to toggle drop.");
|
|
1995
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
1996
|
+
process.exit(1);
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
// src/commands/drop-info.ts
|
|
2001
|
+
import { Command as Command9 } from "commander";
|
|
2002
|
+
init_errors();
|
|
2003
|
+
init_config2();
|
|
2004
|
+
var dropInfoCommand = new Command9("info").alias("get").description("View drop details").argument("<target>", "Drop URL or drop ID").option("-k, --key <key>", "Decryption key (to reveal filenames)").option("-p, --password <password>", "Password for protected drops").option("--json", "Output raw JSON").action(async (target, options) => {
|
|
2005
|
+
const { dropId, key: urlKey } = parseDropIdentifier(target);
|
|
2006
|
+
let keyString = options.key || urlKey;
|
|
2007
|
+
const spin = spinner("Fetching drop info...");
|
|
2008
|
+
try {
|
|
2009
|
+
const baseUrl = getBaseUrl();
|
|
2010
|
+
const res = await fetch(`${baseUrl}/api/v1/drop/${dropId}`);
|
|
2011
|
+
if (!res.ok) {
|
|
2012
|
+
spin.fail("Drop not found or unavailable.");
|
|
2013
|
+
process.exit(res.status === 404 ? EXIT_NOT_FOUND : 1);
|
|
2014
|
+
}
|
|
2015
|
+
const drop = await res.json();
|
|
2016
|
+
spin.stop();
|
|
2017
|
+
if (options.json) {
|
|
2018
|
+
outputJson(drop);
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
if (drop.customKey && !keyString) {
|
|
2022
|
+
if (drop.customKeyData && drop.customKeyIv && drop.salt) {
|
|
2023
|
+
const password = options.password || await prompt("Password:", { mask: true });
|
|
2024
|
+
try {
|
|
2025
|
+
keyString = await decryptKeyWithPassword(
|
|
2026
|
+
drop.customKeyData,
|
|
2027
|
+
password,
|
|
2028
|
+
drop.salt,
|
|
2029
|
+
drop.customKeyIv
|
|
2030
|
+
);
|
|
2031
|
+
} catch {
|
|
2032
|
+
warn("Incorrect password \u2014 showing file IDs instead of names.");
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
let cryptoKey = null;
|
|
2037
|
+
let iv = null;
|
|
2038
|
+
if (keyString) {
|
|
2039
|
+
try {
|
|
2040
|
+
cryptoKey = await importKey(keyString);
|
|
2041
|
+
iv = new Uint8Array(base64UrlToArrayBuffer(drop.iv));
|
|
2042
|
+
} catch {
|
|
2043
|
+
warn("Invalid decryption key \u2014 showing file IDs instead of names.");
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
header("Drop Info");
|
|
2047
|
+
spacer();
|
|
2048
|
+
keyValue("ID", drop.id);
|
|
2049
|
+
keyValue("Files", String(drop.files.length));
|
|
2050
|
+
keyValue("Downloads", String(drop.downloads));
|
|
2051
|
+
if (drop.maxDownloads) {
|
|
2052
|
+
keyValue("Max Downloads", String(drop.maxDownloads));
|
|
2053
|
+
}
|
|
2054
|
+
if (drop.expiresAt) {
|
|
2055
|
+
keyValue("Expires", formatDate(drop.expiresAt));
|
|
2056
|
+
}
|
|
2057
|
+
keyValue("Created", formatDate(drop.createdAt));
|
|
2058
|
+
keyValue("Password Protected", drop.customKey ? "Yes" : "No");
|
|
2059
|
+
keyValue("Hide Branding", drop.hideBranding ? "Yes" : "No");
|
|
2060
|
+
if (drop.files.length > 0) {
|
|
2061
|
+
spacer();
|
|
2062
|
+
sectionTitle("Files");
|
|
2063
|
+
const canDecrypt = cryptoKey !== null && iv !== null;
|
|
2064
|
+
const headers = ["#", canDecrypt ? "Name" : "ID", "Size", "Type"];
|
|
2065
|
+
const rows = [];
|
|
2066
|
+
for (let i = 0; i < drop.files.length; i++) {
|
|
2067
|
+
const file = drop.files[i];
|
|
2068
|
+
let nameOrId;
|
|
2069
|
+
if (cryptoKey && iv) {
|
|
2070
|
+
try {
|
|
2071
|
+
nameOrId = await decryptFilename(file.encryptedName, cryptoKey, iv);
|
|
2072
|
+
} catch {
|
|
2073
|
+
nameOrId = file.id.slice(0, 8) + "...";
|
|
2074
|
+
}
|
|
2075
|
+
} else {
|
|
2076
|
+
nameOrId = file.id.slice(0, 8) + "...";
|
|
2077
|
+
}
|
|
2078
|
+
rows.push([
|
|
2079
|
+
String(i + 1),
|
|
2080
|
+
nameOrId,
|
|
2081
|
+
formatBytes(parseInt(file.size)),
|
|
2082
|
+
file.mimeType || "unknown"
|
|
2083
|
+
]);
|
|
2084
|
+
}
|
|
2085
|
+
table(headers, rows);
|
|
2086
|
+
}
|
|
2087
|
+
spacer();
|
|
2088
|
+
info(`View: ${link(`${baseUrl}/d/${dropId}`)}`);
|
|
2089
|
+
} catch (err) {
|
|
2090
|
+
spin.fail("Failed to fetch drop info.");
|
|
2091
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2092
|
+
process.exit(1);
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
// src/commands/drop-share.ts
|
|
2097
|
+
import { Command as Command10 } from "commander";
|
|
2098
|
+
init_config2();
|
|
2099
|
+
var dropShareCommand = new Command10("share").description("Reconstruct the share URL for a drop (requires the original key)").argument("<id>", "Drop ID").option("-k, --key <key>", "Decryption key (the part after # in the original URL)").action((id, options) => {
|
|
2100
|
+
const baseUrl = getBaseUrl();
|
|
2101
|
+
if (!options.key) {
|
|
2102
|
+
errorBox(
|
|
2103
|
+
"Key Required",
|
|
2104
|
+
"The decryption key is required to reconstruct the share URL.",
|
|
2105
|
+
"Use: anonli drop share <id> --key <key>"
|
|
2106
|
+
);
|
|
2107
|
+
process.exit(1);
|
|
2108
|
+
}
|
|
2109
|
+
const shareUrl = `${baseUrl}/d/${id}#${options.key}`;
|
|
2110
|
+
successBox(
|
|
2111
|
+
"Share URL",
|
|
2112
|
+
`${c.secondary("URL:")} ${link(shareUrl)}`
|
|
2113
|
+
);
|
|
2114
|
+
console.log(shareUrl);
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
// src/commands/drop.ts
|
|
2118
|
+
var dropCommand = new Command11("drop").description("Encrypted file drops").addCommand(dropUploadCommand).addCommand(dropListCommand).addCommand(dropDeleteCommand).addCommand(dropDownloadCommand).addCommand(dropToggleCommand).addCommand(dropInfoCommand).addCommand(dropShareCommand);
|
|
2119
|
+
|
|
2120
|
+
// src/commands/alias.ts
|
|
2121
|
+
import { Command as Command18 } from "commander";
|
|
2122
|
+
|
|
2123
|
+
// src/commands/alias-new.ts
|
|
2124
|
+
init_api();
|
|
2125
|
+
import { Command as Command12 } from "commander";
|
|
2126
|
+
var ALIAS_LOCAL_PART_RE = /^[a-z0-9][a-z0-9._+\-]{0,61}[a-z0-9]$|^[a-z0-9]$/i;
|
|
2127
|
+
function validateLocalPart(localPart) {
|
|
2128
|
+
if (!ALIAS_LOCAL_PART_RE.test(localPart)) {
|
|
2129
|
+
error(
|
|
2130
|
+
`Invalid alias local part: "${localPart}". Must start and end with a letter or digit, and contain only letters, digits, dots, underscores, plus, or hyphens (max 63 chars).`
|
|
2131
|
+
);
|
|
2132
|
+
process.exit(1);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
var aliasNewCommand = new Command12("new").alias("create").description("Create a new alias (default: random)").option("-r, --random", "Generate random alias (default)").option("-c, --custom <name>", "Custom local part").option("-d, --domain <domain>", "Domain (default: anon.li)").option("-l, --label <text>", "Description/label for the alias").option("--recipient <email>", "Recipient email to forward to").action(async (options) => {
|
|
2136
|
+
requireAuth();
|
|
2137
|
+
const domain = options.domain || "anon.li";
|
|
2138
|
+
const isCustom = !!options.custom;
|
|
2139
|
+
if (isCustom) {
|
|
2140
|
+
validateLocalPart(options.custom);
|
|
2141
|
+
}
|
|
2142
|
+
const limitSpin = spinner("Checking plan limits...");
|
|
2143
|
+
const plan = await fetchPlanInfo();
|
|
2144
|
+
limitSpin.stop();
|
|
2145
|
+
if (isCustom) {
|
|
2146
|
+
assertCountLimit("custom alias", plan.aliases.custom.used, plan.aliases.custom.limit);
|
|
2147
|
+
} else {
|
|
2148
|
+
assertCountLimit("random alias", plan.aliases.random.used, plan.aliases.random.limit);
|
|
2149
|
+
}
|
|
2150
|
+
const spin = spinner("Creating alias...");
|
|
2151
|
+
try {
|
|
2152
|
+
let result;
|
|
2153
|
+
const body = {
|
|
2154
|
+
domain,
|
|
2155
|
+
...options.label && { description: options.label },
|
|
2156
|
+
...options.recipient && { recipient_email: options.recipient }
|
|
2157
|
+
};
|
|
2158
|
+
if (isCustom) {
|
|
2159
|
+
result = await apiPost("/api/v1/alias", {
|
|
2160
|
+
...body,
|
|
2161
|
+
format: "custom",
|
|
2162
|
+
local_part: options.custom
|
|
2163
|
+
});
|
|
2164
|
+
} else {
|
|
2165
|
+
result = await apiPost("/api/v1/alias?generate=true", body);
|
|
2166
|
+
}
|
|
2167
|
+
spin.succeed(`Created: ${c.accent(result.data.email)}`);
|
|
2168
|
+
if (options.recipient) {
|
|
2169
|
+
info(`Forwards to: ${c.primary(options.recipient)}`);
|
|
2170
|
+
}
|
|
2171
|
+
if (result.data.description) {
|
|
2172
|
+
info(`Label: ${result.data.description}`);
|
|
2173
|
+
}
|
|
2174
|
+
showRateLimit(result.rateLimit);
|
|
2175
|
+
} catch (err) {
|
|
2176
|
+
spin.fail("Failed to create alias.");
|
|
2177
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2178
|
+
process.exit(1);
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
// src/commands/alias-list.ts
|
|
2183
|
+
init_api();
|
|
2184
|
+
import { Command as Command13 } from "commander";
|
|
2185
|
+
var aliasListCommand = new Command13("list").alias("ls").description("List all aliases").option("--limit <n>", "Number of aliases to fetch", parseInt).option("--offset <n>", "Offset for pagination", parseInt).option("--active", "Show only active aliases").option("--inactive", "Show only inactive aliases").option("--search <term>", "Filter by email address").option("--json", "Output raw JSON").action(async (options) => {
|
|
2186
|
+
requireAuth();
|
|
2187
|
+
const spin = spinner("Fetching aliases...");
|
|
2188
|
+
try {
|
|
2189
|
+
const limit = options.limit ?? 50;
|
|
2190
|
+
const offset = options.offset ?? 0;
|
|
2191
|
+
const result = await apiGetList(`/api/v1/alias?limit=${limit}&offset=${offset}`);
|
|
2192
|
+
spin.stop();
|
|
2193
|
+
if (options.json) {
|
|
2194
|
+
outputJson(result.data);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
let data = result.data;
|
|
2198
|
+
if (options.active) {
|
|
2199
|
+
data = data.filter((a) => a.active);
|
|
2200
|
+
}
|
|
2201
|
+
if (options.inactive) {
|
|
2202
|
+
data = data.filter((a) => !a.active);
|
|
2203
|
+
}
|
|
2204
|
+
if (options.search) {
|
|
2205
|
+
const term = options.search.toLowerCase();
|
|
2206
|
+
data = data.filter((a) => a.email.toLowerCase().includes(term));
|
|
2207
|
+
}
|
|
2208
|
+
if (data.length === 0) {
|
|
2209
|
+
box(
|
|
2210
|
+
`${c.secondary("No aliases found.")}
|
|
2211
|
+
${c.muted("Create one with")} ${c.accent("anonli alias new")}`,
|
|
2212
|
+
{ title: c.info("Aliases") }
|
|
2213
|
+
);
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
table(
|
|
2217
|
+
["Email", "ID", "Status", "Created"],
|
|
2218
|
+
data.map((a) => [
|
|
2219
|
+
a.email,
|
|
2220
|
+
a.id.slice(0, 8) + "...",
|
|
2221
|
+
a.active ? statusBadge("Active", "active") : statusBadge("Inactive", "inactive"),
|
|
2222
|
+
formatDate(a.created_at)
|
|
2223
|
+
])
|
|
2224
|
+
);
|
|
2225
|
+
console.log(dim(
|
|
2226
|
+
`
|
|
2227
|
+
${result.total} alias(es) total${data.length < result.data.length ? `, ${data.length} shown after filter` : ""}`
|
|
2228
|
+
));
|
|
2229
|
+
showRateLimit(result.rateLimit);
|
|
2230
|
+
} catch (err) {
|
|
2231
|
+
spin.fail("Failed to list aliases.");
|
|
2232
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2233
|
+
process.exit(1);
|
|
2234
|
+
}
|
|
2235
|
+
});
|
|
2236
|
+
|
|
2237
|
+
// src/commands/alias-delete.ts
|
|
2238
|
+
init_api();
|
|
2239
|
+
import { Command as Command14 } from "commander";
|
|
2240
|
+
var aliasDeleteCommand = new Command14("delete").alias("rm").description("Delete an alias").argument("<id>", "Alias ID or email").option("-f, --force", "Skip confirmation").action(async (id, options) => {
|
|
2241
|
+
requireAuth();
|
|
2242
|
+
if (!options.force) {
|
|
2243
|
+
const confirmed = await confirm(
|
|
2244
|
+
`Delete alias ${bold(id)}?`
|
|
2245
|
+
);
|
|
2246
|
+
if (!confirmed) {
|
|
2247
|
+
info("Cancelled.");
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
const spin = spinner("Deleting alias...");
|
|
2252
|
+
try {
|
|
2253
|
+
const result = await apiDelete(`/api/v1/alias/${encodeURIComponent(id)}`);
|
|
2254
|
+
spin.succeed(`Deleted alias ${id}`);
|
|
2255
|
+
showRateLimit(result.rateLimit);
|
|
2256
|
+
} catch (err) {
|
|
2257
|
+
spin.fail("Failed to delete alias.");
|
|
2258
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2259
|
+
process.exit(1);
|
|
2260
|
+
}
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
// src/commands/alias-toggle.ts
|
|
2264
|
+
init_api();
|
|
2265
|
+
import { Command as Command15 } from "commander";
|
|
2266
|
+
var aliasToggleCommand = new Command15("toggle").description("Toggle alias active/inactive").argument("<id>", "Alias ID or email").action(async (id) => {
|
|
2267
|
+
requireAuth();
|
|
2268
|
+
const spin = spinner("Toggling alias...");
|
|
2269
|
+
try {
|
|
2270
|
+
const current = await apiGet(
|
|
2271
|
+
`/api/v1/alias/${encodeURIComponent(id)}`
|
|
2272
|
+
);
|
|
2273
|
+
const result = await apiPatch(
|
|
2274
|
+
`/api/v1/alias/${encodeURIComponent(id)}`,
|
|
2275
|
+
{ active: !current.data.active }
|
|
2276
|
+
);
|
|
2277
|
+
const badge = result.data.active ? statusBadge("Active", "active") : statusBadge("Inactive", "inactive");
|
|
2278
|
+
spin.succeed(`Alias ${id} is now ${badge}`);
|
|
2279
|
+
showRateLimit(result.rateLimit);
|
|
2280
|
+
} catch (err) {
|
|
2281
|
+
spin.fail("Failed to toggle alias.");
|
|
2282
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2283
|
+
process.exit(1);
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2287
|
+
// src/commands/alias-update.ts
|
|
2288
|
+
init_api();
|
|
2289
|
+
import { Command as Command16 } from "commander";
|
|
2290
|
+
var aliasUpdateCommand = new Command16("update").alias("edit").description("Update an alias").argument("<id>", "Alias ID or email").option("--enable", "Enable the alias").option("--disable", "Disable the alias").option("-l, --label <text>", "Update description/label").action(async (aliasId, options) => {
|
|
2291
|
+
requireAuth();
|
|
2292
|
+
if (!options.enable && !options.disable && !options.label) {
|
|
2293
|
+
error("Provide at least one option: --enable, --disable, or --label");
|
|
2294
|
+
process.exit(1);
|
|
2295
|
+
}
|
|
2296
|
+
if (options.enable && options.disable) {
|
|
2297
|
+
error("Cannot use both --enable and --disable");
|
|
2298
|
+
process.exit(1);
|
|
2299
|
+
}
|
|
2300
|
+
const spin = spinner("Updating alias...");
|
|
2301
|
+
try {
|
|
2302
|
+
const body = {};
|
|
2303
|
+
if (options.enable) {
|
|
2304
|
+
body.active = true;
|
|
2305
|
+
} else if (options.disable) {
|
|
2306
|
+
body.active = false;
|
|
2307
|
+
}
|
|
2308
|
+
if (options.label !== void 0) {
|
|
2309
|
+
body.description = options.label;
|
|
2310
|
+
}
|
|
2311
|
+
const result = await apiPatch(
|
|
2312
|
+
`/api/v1/alias/${encodeURIComponent(aliasId)}`,
|
|
2313
|
+
body
|
|
2314
|
+
);
|
|
2315
|
+
spin.stop();
|
|
2316
|
+
const status = result.data.active ? statusBadge("Active", "active") : statusBadge("Inactive", "inactive");
|
|
2317
|
+
successBox(
|
|
2318
|
+
"Alias Updated",
|
|
2319
|
+
`${c.accent(result.data.email)} is now ${status}`
|
|
2320
|
+
);
|
|
2321
|
+
if (result.data.description) {
|
|
2322
|
+
info(`Label: ${result.data.description}`);
|
|
2323
|
+
}
|
|
2324
|
+
showRateLimit(result.rateLimit);
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
spin.fail("Failed to update alias.");
|
|
2327
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2328
|
+
process.exit(1);
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
// src/commands/alias-stats.ts
|
|
2333
|
+
init_api();
|
|
2334
|
+
import { Command as Command17 } from "commander";
|
|
2335
|
+
var aliasStatsCommand = new Command17("stats").description("Show forwarding statistics for an alias").argument("[alias-id]", "Alias ID (omit to show summary across all aliases)").option("--all", "Show summary across all aliases").option("--json", "Output raw JSON").action(async (aliasId, options) => {
|
|
2336
|
+
requireAuth();
|
|
2337
|
+
if (aliasId && !options.all) {
|
|
2338
|
+
const spin = spinner("Fetching alias stats...");
|
|
2339
|
+
try {
|
|
2340
|
+
const result = await apiGet(
|
|
2341
|
+
`/api/v1/alias/${encodeURIComponent(aliasId)}`
|
|
2342
|
+
);
|
|
2343
|
+
spin.stop();
|
|
2344
|
+
const a = result.data;
|
|
2345
|
+
if (options.json) {
|
|
2346
|
+
outputJson(a);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
header(`Alias: ${a.email}`);
|
|
2350
|
+
spacer();
|
|
2351
|
+
keyValue("ID", a.id);
|
|
2352
|
+
keyValue("Status", a.active ? "Active" : "Inactive");
|
|
2353
|
+
keyValue("Forwarded", String(a.forwarded ?? 0));
|
|
2354
|
+
keyValue("Blocked", String(a.blocked ?? 0));
|
|
2355
|
+
keyValue("Last Seen", a.last_seen ? formatDate(a.last_seen) : "Never");
|
|
2356
|
+
keyValue("Created", formatDate(a.created_at));
|
|
2357
|
+
showRateLimit(result.rateLimit);
|
|
2358
|
+
} catch (err) {
|
|
2359
|
+
spin.fail("Failed to fetch alias stats.");
|
|
2360
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2361
|
+
process.exit(1);
|
|
2362
|
+
}
|
|
2363
|
+
} else {
|
|
2364
|
+
const spin = spinner("Fetching all aliases...");
|
|
2365
|
+
try {
|
|
2366
|
+
const result = await apiGetList("/api/v1/alias?limit=100");
|
|
2367
|
+
spin.stop();
|
|
2368
|
+
const data = result.data;
|
|
2369
|
+
if (options.json) {
|
|
2370
|
+
const summary = {
|
|
2371
|
+
total: data.length,
|
|
2372
|
+
active: data.filter((a) => a.active).length,
|
|
2373
|
+
totalForwarded: data.reduce((s, a) => s + (a.forwarded ?? 0), 0),
|
|
2374
|
+
totalBlocked: data.reduce((s, a) => s + (a.blocked ?? 0), 0),
|
|
2375
|
+
aliases: data.map((a) => ({
|
|
2376
|
+
id: a.id,
|
|
2377
|
+
email: a.email,
|
|
2378
|
+
forwarded: a.forwarded ?? 0,
|
|
2379
|
+
blocked: a.blocked ?? 0,
|
|
2380
|
+
last_seen: a.last_seen ?? null
|
|
2381
|
+
}))
|
|
2382
|
+
};
|
|
2383
|
+
outputJson(summary);
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
const totalForwarded = data.reduce((s, a) => s + (a.forwarded ?? 0), 0);
|
|
2387
|
+
const totalBlocked = data.reduce((s, a) => s + (a.blocked ?? 0), 0);
|
|
2388
|
+
header("Alias Statistics Summary");
|
|
2389
|
+
spacer();
|
|
2390
|
+
keyValue("Total Aliases", String(data.length));
|
|
2391
|
+
keyValue("Active", String(data.filter((a) => a.active).length));
|
|
2392
|
+
keyValue("Total Forwarded", String(totalForwarded));
|
|
2393
|
+
keyValue("Total Blocked", String(totalBlocked));
|
|
2394
|
+
spacer();
|
|
2395
|
+
if (data.length > 0) {
|
|
2396
|
+
sectionTitle("Per-Alias Breakdown");
|
|
2397
|
+
table(
|
|
2398
|
+
["Email", "Forwarded", "Blocked", "Last Seen"],
|
|
2399
|
+
data.map((a) => [
|
|
2400
|
+
a.email,
|
|
2401
|
+
String(a.forwarded ?? 0),
|
|
2402
|
+
String(a.blocked ?? 0),
|
|
2403
|
+
a.last_seen ? formatDate(a.last_seen) : dim("never")
|
|
2404
|
+
])
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
showRateLimit(result.rateLimit);
|
|
2408
|
+
} catch (err) {
|
|
2409
|
+
spin.fail("Failed to fetch alias stats.");
|
|
2410
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2411
|
+
process.exit(1);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
});
|
|
2415
|
+
|
|
2416
|
+
// src/commands/alias.ts
|
|
2417
|
+
var aliasCommand = new Command18("alias").description("Manage email aliases").addCommand(aliasNewCommand).addCommand(aliasListCommand).addCommand(aliasDeleteCommand).addCommand(aliasToggleCommand).addCommand(aliasUpdateCommand).addCommand(aliasStatsCommand);
|
|
2418
|
+
|
|
2419
|
+
// src/commands/recipient.ts
|
|
2420
|
+
init_api();
|
|
2421
|
+
import { Command as Command19 } from "commander";
|
|
2422
|
+
var recipientCommand = new Command19("recipient").alias("recipients").description("Manage email recipients");
|
|
2423
|
+
recipientCommand.command("list").alias("ls").description("List all recipients").option("--json", "Output raw JSON").action(async (options) => {
|
|
2424
|
+
requireAuth();
|
|
2425
|
+
const spin = spinner("Fetching recipients...");
|
|
2426
|
+
try {
|
|
2427
|
+
const result = await apiGetList("/api/v1/recipient");
|
|
2428
|
+
spin.stop();
|
|
2429
|
+
if (options.json) {
|
|
2430
|
+
outputJson(result.data);
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
if (result.data.length === 0) {
|
|
2434
|
+
box(
|
|
2435
|
+
`${c.secondary("No recipients yet.")}
|
|
2436
|
+
${c.muted("Add one with")} ${c.accent("anonli recipient add <email>")}`,
|
|
2437
|
+
{ title: c.info("Recipients") }
|
|
2438
|
+
);
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
table(
|
|
2442
|
+
["Email", "Status", "Default", "PGP", "Aliases", "Created"],
|
|
2443
|
+
result.data.map((r) => [
|
|
2444
|
+
r.email,
|
|
2445
|
+
r.verified ? statusBadge("verified", "active") : statusBadge("pending", "inactive"),
|
|
2446
|
+
r.is_default ? c.accent("\u2713") : "",
|
|
2447
|
+
r.pgp_fingerprint ? c.accent("\u2713") : "",
|
|
2448
|
+
String(r.alias_count || 0),
|
|
2449
|
+
formatDate(r.created_at)
|
|
2450
|
+
])
|
|
2451
|
+
);
|
|
2452
|
+
console.log(dim(` ${result.total} recipient(s) total`));
|
|
2453
|
+
showRateLimit(result.rateLimit);
|
|
2454
|
+
} catch (err) {
|
|
2455
|
+
spin.fail("Failed to list recipients.");
|
|
2456
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2457
|
+
process.exit(1);
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
recipientCommand.command("add <email>").description("Add a new recipient").action(async (email) => {
|
|
2461
|
+
requireAuth();
|
|
2462
|
+
const limitSpin = spinner("Checking plan limits...");
|
|
2463
|
+
const plan = await fetchPlanInfo();
|
|
2464
|
+
limitSpin.stop();
|
|
2465
|
+
assertCountLimit("recipient", plan.recipients.used, plan.recipients.limit);
|
|
2466
|
+
const spin = spinner("Adding recipient...");
|
|
2467
|
+
try {
|
|
2468
|
+
const result = await apiPost("/api/v1/recipient", { email });
|
|
2469
|
+
spin.succeed(`Added: ${c.accent(result.data.email)}`);
|
|
2470
|
+
info("Verification email sent. Check your inbox.");
|
|
2471
|
+
showRateLimit(result.rateLimit);
|
|
2472
|
+
} catch (err) {
|
|
2473
|
+
spin.fail("Failed to add recipient.");
|
|
2474
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2475
|
+
process.exit(1);
|
|
2476
|
+
}
|
|
2477
|
+
});
|
|
2478
|
+
recipientCommand.command("delete <id>").alias("rm").description("Delete a recipient").option("-f, --force", "Skip confirmation").action(async (id, options) => {
|
|
2479
|
+
requireAuth();
|
|
2480
|
+
if (!options.force) {
|
|
2481
|
+
const confirmed = await confirm(`Delete recipient ${id}?`);
|
|
2482
|
+
if (!confirmed) {
|
|
2483
|
+
info("Cancelled.");
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
const spin = spinner("Deleting recipient...");
|
|
2488
|
+
try {
|
|
2489
|
+
const result = await apiDelete(`/api/v1/recipient/${encodeURIComponent(id)}`);
|
|
2490
|
+
spin.succeed("Recipient deleted.");
|
|
2491
|
+
showRateLimit(result.rateLimit);
|
|
2492
|
+
} catch (err) {
|
|
2493
|
+
spin.fail("Failed to delete recipient.");
|
|
2494
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2495
|
+
process.exit(1);
|
|
2496
|
+
}
|
|
2497
|
+
});
|
|
2498
|
+
recipientCommand.command("default <id>").description("Set a recipient as default").action(async (id) => {
|
|
2499
|
+
requireAuth();
|
|
2500
|
+
const spin = spinner("Setting default...");
|
|
2501
|
+
try {
|
|
2502
|
+
const result = await apiPatch(
|
|
2503
|
+
`/api/v1/recipient/${encodeURIComponent(id)}`,
|
|
2504
|
+
{ is_default: true }
|
|
2505
|
+
);
|
|
2506
|
+
spin.succeed(`${c.accent(result.data.email)} is now your default recipient.`);
|
|
2507
|
+
showRateLimit(result.rateLimit);
|
|
2508
|
+
} catch (err) {
|
|
2509
|
+
spin.fail("Failed to set default.");
|
|
2510
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2511
|
+
process.exit(1);
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
recipientCommand.command("verify <id>").description("Resend verification email").action(async (id) => {
|
|
2515
|
+
requireAuth();
|
|
2516
|
+
const spin = spinner("Sending verification email...");
|
|
2517
|
+
try {
|
|
2518
|
+
const result = await apiPost(`/api/v1/recipient/${encodeURIComponent(id)}/verify`);
|
|
2519
|
+
spin.succeed("Verification email sent.");
|
|
2520
|
+
showRateLimit(result.rateLimit);
|
|
2521
|
+
} catch (err) {
|
|
2522
|
+
spin.fail("Failed to send verification.");
|
|
2523
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2524
|
+
process.exit(1);
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
var pgpCommand = recipientCommand.command("pgp").description("Manage PGP keys for recipients");
|
|
2528
|
+
pgpCommand.command("set <id>").description("Set PGP public key for a recipient").option("-k, --key <file>", "Path to PGP public key file").option("-n, --name <name>", "Name/label for the key").option("-f, --force", "Skip confirmation when replacing existing key").action(async (id, options) => {
|
|
2529
|
+
requireAuth();
|
|
2530
|
+
if (!options.key) {
|
|
2531
|
+
error("--key <file> is required");
|
|
2532
|
+
process.exit(1);
|
|
2533
|
+
}
|
|
2534
|
+
const fs4 = await import("fs");
|
|
2535
|
+
const path4 = await import("path");
|
|
2536
|
+
const keyPath = path4.resolve(options.key);
|
|
2537
|
+
if (!fs4.existsSync(keyPath)) {
|
|
2538
|
+
error(`File not found: ${keyPath}`);
|
|
2539
|
+
process.exit(1);
|
|
2540
|
+
}
|
|
2541
|
+
const keyFileStat = fs4.statSync(keyPath);
|
|
2542
|
+
if (keyFileStat.size > 20 * 1024) {
|
|
2543
|
+
error(`PGP key file is too large (${formatBytes(keyFileStat.size)}). Maximum allowed size is 20 KB.`);
|
|
2544
|
+
process.exit(1);
|
|
2545
|
+
}
|
|
2546
|
+
const publicKey = fs4.readFileSync(keyPath, "utf-8");
|
|
2547
|
+
if (!options.force) {
|
|
2548
|
+
try {
|
|
2549
|
+
const existing = await apiGet(`/api/v1/recipient/${encodeURIComponent(id)}`);
|
|
2550
|
+
if (existing.data.pgp_fingerprint) {
|
|
2551
|
+
const confirmed = await confirm(
|
|
2552
|
+
`Recipient already has a PGP key (${existing.data.pgp_fingerprint.slice(0, 16)}...). Replace it?`
|
|
2553
|
+
);
|
|
2554
|
+
if (!confirmed) {
|
|
2555
|
+
info("Cancelled.");
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
} catch {
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
const spin = spinner("Setting PGP key...");
|
|
2563
|
+
try {
|
|
2564
|
+
const { apiFetch: apiFetch2, extractRateLimit: extractRateLimit2 } = await Promise.resolve().then(() => (init_api(), api_exports));
|
|
2565
|
+
const res = await apiFetch2(`/api/v1/recipient/${encodeURIComponent(id)}/pgp`, {
|
|
2566
|
+
method: "PUT",
|
|
2567
|
+
body: JSON.stringify({
|
|
2568
|
+
public_key: publicKey,
|
|
2569
|
+
name: options.name
|
|
2570
|
+
})
|
|
2571
|
+
});
|
|
2572
|
+
if (!res.ok) {
|
|
2573
|
+
const err = await res.json().catch(() => ({}));
|
|
2574
|
+
throw new Error(err?.error?.message || "Failed to set PGP key");
|
|
2575
|
+
}
|
|
2576
|
+
const rateLimit = extractRateLimit2(res);
|
|
2577
|
+
const data = await res.json();
|
|
2578
|
+
spin.succeed("PGP key set successfully.");
|
|
2579
|
+
if (data.data?.pgp_fingerprint) {
|
|
2580
|
+
info(`Fingerprint: ${data.data.pgp_fingerprint}`);
|
|
2581
|
+
}
|
|
2582
|
+
showRateLimit(rateLimit);
|
|
2583
|
+
} catch (err) {
|
|
2584
|
+
spin.fail("Failed to set PGP key.");
|
|
2585
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2586
|
+
process.exit(1);
|
|
2587
|
+
}
|
|
2588
|
+
});
|
|
2589
|
+
pgpCommand.command("remove <id>").alias("rm").description("Remove PGP key from a recipient").action(async (id) => {
|
|
2590
|
+
requireAuth();
|
|
2591
|
+
const spin = spinner("Removing PGP key...");
|
|
2592
|
+
try {
|
|
2593
|
+
const result = await apiDelete(`/api/v1/recipient/${encodeURIComponent(id)}/pgp`);
|
|
2594
|
+
spin.succeed("PGP key removed.");
|
|
2595
|
+
showRateLimit(result.rateLimit);
|
|
2596
|
+
} catch (err) {
|
|
2597
|
+
spin.fail("Failed to remove PGP key.");
|
|
2598
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2599
|
+
process.exit(1);
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
// src/commands/domain.ts
|
|
2604
|
+
init_api();
|
|
2605
|
+
import { Command as Command20 } from "commander";
|
|
2606
|
+
var domainCommand = new Command20("domain").alias("domains").description("Manage custom domains");
|
|
2607
|
+
domainCommand.command("list").alias("ls").description("List all domains").option("--json", "Output raw JSON").action(async (options) => {
|
|
2608
|
+
requireAuth();
|
|
2609
|
+
const spin = spinner("Fetching domains...");
|
|
2610
|
+
try {
|
|
2611
|
+
const result = await apiGetList("/api/v1/domain");
|
|
2612
|
+
spin.stop();
|
|
2613
|
+
if (options.json) {
|
|
2614
|
+
outputJson(result.data);
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
if (result.data.length === 0) {
|
|
2618
|
+
box(
|
|
2619
|
+
`${c.secondary("No custom domains yet.")}
|
|
2620
|
+
${c.muted("Add one with")} ${c.accent("anonli domain add <domain>")}`,
|
|
2621
|
+
{ title: c.info("Domains") }
|
|
2622
|
+
);
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
table(
|
|
2626
|
+
["Domain", "Status", "MX", "SPF", "DKIM", "Created"],
|
|
2627
|
+
result.data.map((d) => [
|
|
2628
|
+
d.domain,
|
|
2629
|
+
d.verified ? statusBadge("verified", "active") : statusBadge("pending", "inactive"),
|
|
2630
|
+
d.mx_verified ? c.accent("\u2713") : c.error("\u2717"),
|
|
2631
|
+
d.spf_verified ? c.accent("\u2713") : c.error("\u2717"),
|
|
2632
|
+
d.dkim_verified ? c.accent("\u2713") : c.error("\u2717"),
|
|
2633
|
+
formatDate(d.created_at)
|
|
2634
|
+
])
|
|
2635
|
+
);
|
|
2636
|
+
console.log(dim(` ${result.total} domain(s) total`));
|
|
2637
|
+
showRateLimit(result.rateLimit);
|
|
2638
|
+
} catch (err) {
|
|
2639
|
+
spin.fail("Failed to list domains.");
|
|
2640
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2641
|
+
process.exit(1);
|
|
2642
|
+
}
|
|
2643
|
+
});
|
|
2644
|
+
var FQDN_RE = /^(?:[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i;
|
|
2645
|
+
domainCommand.command("add <domain>").description("Add a custom domain").action(async (domain) => {
|
|
2646
|
+
requireAuth();
|
|
2647
|
+
if (!FQDN_RE.test(domain)) {
|
|
2648
|
+
error(`Invalid domain: "${domain}". Please enter a valid fully-qualified domain name (e.g. mail.example.com).`);
|
|
2649
|
+
process.exit(1);
|
|
2650
|
+
}
|
|
2651
|
+
const limitSpin = spinner("Checking plan limits...");
|
|
2652
|
+
const plan = await fetchPlanInfo();
|
|
2653
|
+
limitSpin.stop();
|
|
2654
|
+
assertCountLimit("domain", plan.domains.used, plan.domains.limit);
|
|
2655
|
+
const spin = spinner("Adding domain...");
|
|
2656
|
+
try {
|
|
2657
|
+
const result = await apiPost("/api/v1/domain", {
|
|
2658
|
+
domain: domain.toLowerCase()
|
|
2659
|
+
});
|
|
2660
|
+
spin.succeed(`Added: ${c.accent(result.data.domain)}`);
|
|
2661
|
+
spacer();
|
|
2662
|
+
sectionTitle("DNS Records to Configure");
|
|
2663
|
+
spacer();
|
|
2664
|
+
console.log(c.secondary("1. Ownership TXT record:"));
|
|
2665
|
+
console.log(` Host: ${c.accent(result.data.domain)}`);
|
|
2666
|
+
console.log(` Value: ${c.primary(`anon.li=${result.data.verification_token}`)}`);
|
|
2667
|
+
spacer();
|
|
2668
|
+
console.log(c.secondary("2. MX record:"));
|
|
2669
|
+
console.log(` Host: ${c.accent(result.data.domain)}`);
|
|
2670
|
+
console.log(` Value: ${c.primary("mx.anon.li")} (priority 10)`);
|
|
2671
|
+
spacer();
|
|
2672
|
+
console.log(c.secondary("3. SPF TXT record:"));
|
|
2673
|
+
console.log(` Host: ${c.accent(result.data.domain)}`);
|
|
2674
|
+
console.log(` Value: ${c.primary("v=spf1 include:anon.li ~all")}`);
|
|
2675
|
+
spacer();
|
|
2676
|
+
if (result.data.dkim_selector && result.data.dkim_public_key) {
|
|
2677
|
+
const cleanKey = result.data.dkim_public_key.replace(/-----BEGIN PUBLIC KEY-----/g, "").replace(/-----END PUBLIC KEY-----/g, "").replace(/[\n\r\s]/g, "");
|
|
2678
|
+
console.log(c.secondary("4. DKIM TXT record:"));
|
|
2679
|
+
console.log(` Host: ${c.accent(`${result.data.dkim_selector}._domainkey.${result.data.domain}`)}`);
|
|
2680
|
+
console.log(` Value: ${c.primary(`v=DKIM1; k=rsa; p=${cleanKey}`)}`);
|
|
2681
|
+
spacer();
|
|
2682
|
+
}
|
|
2683
|
+
info(`Run ${c.accent(`anonli domain verify ${result.data.id}`)} after configuring DNS.`);
|
|
2684
|
+
showRateLimit(result.rateLimit);
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
spin.fail("Failed to add domain.");
|
|
2687
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2688
|
+
process.exit(1);
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
domainCommand.command("verify <id>").description("Verify domain DNS records").action(async (id) => {
|
|
2692
|
+
requireAuth();
|
|
2693
|
+
const spin = spinner("Checking DNS records...");
|
|
2694
|
+
try {
|
|
2695
|
+
const result = await apiPost(`/api/v1/domain/${encodeURIComponent(id)}/verify`);
|
|
2696
|
+
spin.stop();
|
|
2697
|
+
const d = result.data;
|
|
2698
|
+
const statusIcon = (ok) => ok ? c.accent("\u2713") : c.error("\u2717");
|
|
2699
|
+
header("DNS Verification Results");
|
|
2700
|
+
spacer();
|
|
2701
|
+
console.log(` Ownership: ${statusIcon(d.ownership_verified)}`);
|
|
2702
|
+
console.log(` MX: ${statusIcon(d.mx_verified)}`);
|
|
2703
|
+
console.log(` SPF: ${statusIcon(d.spf_verified)}`);
|
|
2704
|
+
console.log(` DKIM: ${statusIcon(d.dkim_verified)}`);
|
|
2705
|
+
spacer();
|
|
2706
|
+
if (d.verified) {
|
|
2707
|
+
successBox("Domain Verified", "All DNS records are correctly configured.");
|
|
2708
|
+
} else {
|
|
2709
|
+
warn("Some DNS records are missing or incorrect. Check your DNS configuration.");
|
|
2710
|
+
}
|
|
2711
|
+
showRateLimit(result.rateLimit);
|
|
2712
|
+
} catch (err) {
|
|
2713
|
+
spin.fail("Failed to verify domain.");
|
|
2714
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2715
|
+
process.exit(1);
|
|
2716
|
+
}
|
|
2717
|
+
});
|
|
2718
|
+
domainCommand.command("info <id>").alias("get").description("Show domain details and DNS records").action(async (id) => {
|
|
2719
|
+
requireAuth();
|
|
2720
|
+
const spin = spinner("Fetching domain...");
|
|
2721
|
+
try {
|
|
2722
|
+
const result = await apiGet(
|
|
2723
|
+
`/api/v1/domain/${encodeURIComponent(id)}`
|
|
2724
|
+
);
|
|
2725
|
+
spin.stop();
|
|
2726
|
+
const d = result.data;
|
|
2727
|
+
header(`Domain: ${d.domain}`);
|
|
2728
|
+
spacer();
|
|
2729
|
+
keyValue("ID", d.id);
|
|
2730
|
+
keyValue("Status", d.verified ? "Verified" : "Pending");
|
|
2731
|
+
keyValue("Ownership", d.ownership_verified ? "\u2713" : "\u2717");
|
|
2732
|
+
keyValue("MX", d.mx_verified ? "\u2713" : "\u2717");
|
|
2733
|
+
keyValue("SPF", d.spf_verified ? "\u2713" : "\u2717");
|
|
2734
|
+
keyValue("DKIM", d.dkim_verified ? "\u2713" : "\u2717");
|
|
2735
|
+
keyValue("Created", formatDate(d.created_at));
|
|
2736
|
+
showRateLimit(result.rateLimit);
|
|
2737
|
+
} catch (err) {
|
|
2738
|
+
spin.fail("Failed to fetch domain.");
|
|
2739
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2740
|
+
process.exit(1);
|
|
2741
|
+
}
|
|
2742
|
+
});
|
|
2743
|
+
domainCommand.command("delete <id>").alias("rm").description("Delete a domain").option("-f, --force", "Skip confirmation").action(async (id, options) => {
|
|
2744
|
+
requireAuth();
|
|
2745
|
+
if (!options.force) {
|
|
2746
|
+
const confirmed = await confirm(
|
|
2747
|
+
`Delete domain ${id}? This will also delete all aliases on this domain.`
|
|
2748
|
+
);
|
|
2749
|
+
if (!confirmed) {
|
|
2750
|
+
info("Cancelled.");
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
const spin = spinner("Deleting domain...");
|
|
2755
|
+
try {
|
|
2756
|
+
const result = await apiDelete(`/api/v1/domain/${encodeURIComponent(id)}`);
|
|
2757
|
+
spin.succeed("Domain deleted.");
|
|
2758
|
+
showRateLimit(result.rateLimit);
|
|
2759
|
+
} catch (err) {
|
|
2760
|
+
spin.fail("Failed to delete domain.");
|
|
2761
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2762
|
+
process.exit(1);
|
|
2763
|
+
}
|
|
2764
|
+
});
|
|
2765
|
+
domainCommand.command("dkim <id>").description("Regenerate DKIM keys").action(async (id) => {
|
|
2766
|
+
requireAuth();
|
|
2767
|
+
const spin = spinner("Regenerating DKIM keys...");
|
|
2768
|
+
try {
|
|
2769
|
+
const result = await apiPost(`/api/v1/domain/${encodeURIComponent(id)}/dkim`);
|
|
2770
|
+
spin.succeed("DKIM keys regenerated.");
|
|
2771
|
+
spacer();
|
|
2772
|
+
const rec = result.data.dkim_record;
|
|
2773
|
+
console.log(c.secondary("Update your DKIM TXT record:"));
|
|
2774
|
+
console.log(` Host: ${c.accent(rec.host)}`);
|
|
2775
|
+
console.log(` Value: ${c.primary(rec.value)}`);
|
|
2776
|
+
showRateLimit(result.rateLimit);
|
|
2777
|
+
} catch (err) {
|
|
2778
|
+
spin.fail("Failed to regenerate DKIM keys.");
|
|
2779
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2780
|
+
process.exit(1);
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
// src/commands/apikey.ts
|
|
2785
|
+
init_api();
|
|
2786
|
+
import { Command as Command21 } from "commander";
|
|
2787
|
+
var apikeyCommand = new Command21("apikey").alias("api-key").description("Manage API keys");
|
|
2788
|
+
apikeyCommand.command("list").alias("ls").description("List all API keys").option("--json", "Output raw JSON").action(async (options) => {
|
|
2789
|
+
requireAuth();
|
|
2790
|
+
const spin = spinner("Fetching API keys...");
|
|
2791
|
+
try {
|
|
2792
|
+
const result = await apiGetList("/api/v1/api-key");
|
|
2793
|
+
spin.stop();
|
|
2794
|
+
if (options.json) {
|
|
2795
|
+
outputJson(result.data);
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
if (result.data.length === 0) {
|
|
2799
|
+
box(
|
|
2800
|
+
`${c.secondary("No API keys yet.")}
|
|
2801
|
+
${c.muted("Create one with")} ${c.accent("anonli apikey create")}`,
|
|
2802
|
+
{ title: c.info("API Keys") }
|
|
2803
|
+
);
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
table(
|
|
2807
|
+
["Prefix", "Label", "Created"],
|
|
2808
|
+
result.data.map((k) => [
|
|
2809
|
+
c.accent(k.key_prefix + "..."),
|
|
2810
|
+
k.label || dim("(none)"),
|
|
2811
|
+
formatDate(k.created_at)
|
|
2812
|
+
])
|
|
2813
|
+
);
|
|
2814
|
+
console.log(dim(` ${result.total} API key(s) total`));
|
|
2815
|
+
showRateLimit(result.rateLimit);
|
|
2816
|
+
} catch (err) {
|
|
2817
|
+
spin.fail("Failed to list API keys.");
|
|
2818
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2819
|
+
process.exit(1);
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2822
|
+
apikeyCommand.command("create").alias("new").description("Create a new API key").option("-l, --label <label>", "Label for the key").action(async (options) => {
|
|
2823
|
+
requireAuth();
|
|
2824
|
+
const spin = spinner("Creating API key...");
|
|
2825
|
+
try {
|
|
2826
|
+
const result = await apiPost("/api/v1/api-key", {
|
|
2827
|
+
label: options.label
|
|
2828
|
+
});
|
|
2829
|
+
spin.stop();
|
|
2830
|
+
successBox(
|
|
2831
|
+
"API Key Created",
|
|
2832
|
+
`${c.warning("Save this key now - it won't be shown again!")}
|
|
2833
|
+
|
|
2834
|
+
${c.accent(result.data.key)}`
|
|
2835
|
+
);
|
|
2836
|
+
if (result.data.label) {
|
|
2837
|
+
info(`Label: ${result.data.label}`);
|
|
2838
|
+
}
|
|
2839
|
+
showRateLimit(result.rateLimit);
|
|
2840
|
+
} catch (err) {
|
|
2841
|
+
spin.fail("Failed to create API key.");
|
|
2842
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2843
|
+
process.exit(1);
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
apikeyCommand.command("delete <id>").alias("rm").description("Delete an API key").option("-f, --force", "Skip confirmation").action(async (id, options) => {
|
|
2847
|
+
requireAuth();
|
|
2848
|
+
if (!options.force) {
|
|
2849
|
+
const confirmed = await confirm(`Delete API key ${id}?`);
|
|
2850
|
+
if (!confirmed) {
|
|
2851
|
+
info("Cancelled.");
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
const spin = spinner("Deleting API key...");
|
|
2856
|
+
try {
|
|
2857
|
+
const result = await apiDelete(`/api/v1/api-key/${encodeURIComponent(id)}`);
|
|
2858
|
+
spin.succeed("API key deleted.");
|
|
2859
|
+
showRateLimit(result.rateLimit);
|
|
2860
|
+
} catch (err) {
|
|
2861
|
+
spin.fail("Failed to delete API key.");
|
|
2862
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
2863
|
+
process.exit(1);
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
|
|
2867
|
+
// src/commands/config-cmd.ts
|
|
2868
|
+
init_config2();
|
|
2869
|
+
import { Command as Command22 } from "commander";
|
|
2870
|
+
import figures2 from "figures";
|
|
2871
|
+
var configCommand = new Command22("config").description("View or update CLI configuration").argument("[action]", "get, set, or validate").argument("[key]", "Config key (e.g. baseUrl)").argument("[value]", "Value to set").action(async (action, key, value) => {
|
|
2872
|
+
const config = loadConfig();
|
|
2873
|
+
if (!action || action === "get") {
|
|
2874
|
+
sectionTitle("Configuration");
|
|
2875
|
+
keyValue(
|
|
2876
|
+
"apiKey",
|
|
2877
|
+
config.apiKey ? maskApiKey(config.apiKey) : dim("(not set)")
|
|
2878
|
+
);
|
|
2879
|
+
keyValue("baseUrl", config.baseUrl);
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
if (action === "validate") {
|
|
2883
|
+
header("Config Validation");
|
|
2884
|
+
spacer();
|
|
2885
|
+
const apiKey = getApiKey();
|
|
2886
|
+
const baseUrl = getBaseUrl();
|
|
2887
|
+
const keyOk = !!apiKey && apiKey.startsWith("ak_") && apiKey.length > 10;
|
|
2888
|
+
console.log(
|
|
2889
|
+
` ${keyOk ? c.success(figures2.tick) : c.error(figures2.cross)} API key: ${apiKey ? maskApiKey(apiKey) : c.error("not set")}`
|
|
2890
|
+
);
|
|
2891
|
+
let urlOk = false;
|
|
2892
|
+
try {
|
|
2893
|
+
new URL(baseUrl);
|
|
2894
|
+
urlOk = true;
|
|
2895
|
+
} catch {
|
|
2896
|
+
}
|
|
2897
|
+
console.log(
|
|
2898
|
+
` ${urlOk ? c.success(figures2.tick) : c.error(figures2.cross)} Base URL: ${baseUrl}`
|
|
2899
|
+
);
|
|
2900
|
+
if (keyOk && urlOk) {
|
|
2901
|
+
const spin = spinner("Checking API connectivity...");
|
|
2902
|
+
try {
|
|
2903
|
+
const res = await fetch(`${baseUrl}/api/v1/me`, {
|
|
2904
|
+
headers: {
|
|
2905
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2906
|
+
"Content-Type": "application/json"
|
|
2907
|
+
}
|
|
2908
|
+
});
|
|
2909
|
+
if (res.ok) {
|
|
2910
|
+
const data = await res.json();
|
|
2911
|
+
spin.succeed(`Authenticated as ${c.accent(data.data?.email ?? "unknown")}`);
|
|
2912
|
+
} else if (res.status === 401) {
|
|
2913
|
+
spin.fail("API key is invalid or expired \u2014 run `anonli login` to re-authenticate");
|
|
2914
|
+
process.exit(1);
|
|
2915
|
+
} else {
|
|
2916
|
+
spin.fail(`API returned unexpected status: ${res.status}`);
|
|
2917
|
+
process.exit(1);
|
|
2918
|
+
}
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
spin.fail(`Cannot reach ${baseUrl}: ${err instanceof Error ? err.message : "network error"}`);
|
|
2921
|
+
process.exit(1);
|
|
2922
|
+
}
|
|
2923
|
+
} else {
|
|
2924
|
+
const issues = [];
|
|
2925
|
+
if (!keyOk) issues.push("API key missing or invalid format");
|
|
2926
|
+
if (!urlOk) issues.push("Base URL is not a valid URL");
|
|
2927
|
+
spacer();
|
|
2928
|
+
errorBox("Config Invalid", issues.join("\n"), "Run `anonli login` to set up authentication.");
|
|
2929
|
+
process.exit(1);
|
|
2930
|
+
}
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
if (action === "set") {
|
|
2934
|
+
if (!key || !value) {
|
|
2935
|
+
error("Usage: anonli config set <key> <value>");
|
|
2936
|
+
process.exit(1);
|
|
2937
|
+
}
|
|
2938
|
+
if (key === "baseUrl") {
|
|
2939
|
+
try {
|
|
2940
|
+
new URL(value);
|
|
2941
|
+
} catch {
|
|
2942
|
+
error("Invalid URL format.");
|
|
2943
|
+
process.exit(1);
|
|
2944
|
+
}
|
|
2945
|
+
config.baseUrl = value;
|
|
2946
|
+
saveConfig(config);
|
|
2947
|
+
success(`Set baseUrl to ${value}`);
|
|
2948
|
+
} else if (key === "apiKey") {
|
|
2949
|
+
error(
|
|
2950
|
+
'Use "anonli login" to set your API key.'
|
|
2951
|
+
);
|
|
2952
|
+
process.exit(1);
|
|
2953
|
+
} else {
|
|
2954
|
+
error(`Unknown config key: ${key}`);
|
|
2955
|
+
process.exit(1);
|
|
2956
|
+
}
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
error(`Unknown action: ${action}. Use "get", "set", or "validate".`);
|
|
2960
|
+
process.exit(1);
|
|
2961
|
+
});
|
|
2962
|
+
|
|
2963
|
+
// src/commands/update.ts
|
|
2964
|
+
import { Command as Command23 } from "commander";
|
|
2965
|
+
import { execSync } from "child_process";
|
|
2966
|
+
var updateCommand = new Command23("update").description("Update anonli to the latest version").action(() => {
|
|
2967
|
+
const spin = spinner("Updating anonli...");
|
|
2968
|
+
try {
|
|
2969
|
+
let cmd;
|
|
2970
|
+
try {
|
|
2971
|
+
execSync("bun --version", { stdio: "ignore" });
|
|
2972
|
+
cmd = "bun install -g anonli@latest";
|
|
2973
|
+
} catch {
|
|
2974
|
+
cmd = "npm install -g anonli@latest";
|
|
2975
|
+
}
|
|
2976
|
+
execSync(cmd, { stdio: "inherit" });
|
|
2977
|
+
spin.stop();
|
|
2978
|
+
try {
|
|
2979
|
+
const newVersion = execSync("anonli --version", { encoding: "utf8" }).trim();
|
|
2980
|
+
successBox("Updated", `anonli is now up to date.
|
|
2981
|
+
Installed version: ${newVersion}`);
|
|
2982
|
+
} catch {
|
|
2983
|
+
successBox("Updated", "anonli was updated. Run `anonli --version` to confirm.");
|
|
2984
|
+
}
|
|
2985
|
+
} catch (err) {
|
|
2986
|
+
spin.stop();
|
|
2987
|
+
errorBox(
|
|
2988
|
+
"Update Failed",
|
|
2989
|
+
err instanceof Error ? err.message : "Failed to update.",
|
|
2990
|
+
"Try: npm install -g anonli@latest"
|
|
2991
|
+
);
|
|
2992
|
+
process.exit(1);
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
|
|
2996
|
+
// src/commands/subscribe.ts
|
|
2997
|
+
init_api();
|
|
2998
|
+
import { Command as Command24 } from "commander";
|
|
2999
|
+
import { execFile } from "child_process";
|
|
3000
|
+
var PRICING = {
|
|
3001
|
+
bundle: {
|
|
3002
|
+
name: "Bundle",
|
|
3003
|
+
description: "Alias + Drop combined",
|
|
3004
|
+
plus: { monthly: "$6/mo", yearly: "$60/yr" },
|
|
3005
|
+
pro: { monthly: "$12/mo", yearly: "$120/yr" }
|
|
3006
|
+
},
|
|
3007
|
+
alias: {
|
|
3008
|
+
name: "Alias",
|
|
3009
|
+
description: "Anonymous email forwarding",
|
|
3010
|
+
plus: { monthly: "$3/mo", yearly: "$30/yr" },
|
|
3011
|
+
pro: { monthly: "$6/mo", yearly: "$60/yr" }
|
|
3012
|
+
},
|
|
3013
|
+
drop: {
|
|
3014
|
+
name: "Drop",
|
|
3015
|
+
description: "Encrypted file sharing",
|
|
3016
|
+
plus: { monthly: "$3/mo", yearly: "$30/yr" },
|
|
3017
|
+
pro: { monthly: "$6/mo", yearly: "$60/yr" }
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
function openUrl(url) {
|
|
3021
|
+
const platform = process.platform;
|
|
3022
|
+
let cmd;
|
|
3023
|
+
let args;
|
|
3024
|
+
if (platform === "darwin") {
|
|
3025
|
+
cmd = "open";
|
|
3026
|
+
args = [url];
|
|
3027
|
+
} else if (platform === "win32") {
|
|
3028
|
+
cmd = "cmd";
|
|
3029
|
+
args = ["/c", "start", "", url];
|
|
3030
|
+
} else {
|
|
3031
|
+
cmd = "xdg-open";
|
|
3032
|
+
args = [url];
|
|
3033
|
+
}
|
|
3034
|
+
execFile(cmd, args, (err) => {
|
|
3035
|
+
if (err) {
|
|
3036
|
+
warn(`Could not open browser automatically. Visit this URL manually:
|
|
3037
|
+
${url}`);
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
function showPricingTable() {
|
|
3042
|
+
header("Plans & Pricing");
|
|
3043
|
+
spacer();
|
|
3044
|
+
const headers = ["Product", "Tier", "Monthly", "Yearly"];
|
|
3045
|
+
const rows = [];
|
|
3046
|
+
for (const [key, product] of Object.entries(PRICING)) {
|
|
3047
|
+
rows.push([
|
|
3048
|
+
`${c.accent(product.name)} ${c.muted(`(${key})`)}`,
|
|
3049
|
+
c.accent("Plus"),
|
|
3050
|
+
product.plus.monthly,
|
|
3051
|
+
`${product.plus.yearly} ${c.success("(save up to 25%)")}`
|
|
3052
|
+
]);
|
|
3053
|
+
rows.push([
|
|
3054
|
+
"",
|
|
3055
|
+
c.gold("Pro"),
|
|
3056
|
+
product.pro.monthly,
|
|
3057
|
+
`${product.pro.yearly} ${c.success("(save up to 25%)")}`
|
|
3058
|
+
]);
|
|
3059
|
+
}
|
|
3060
|
+
table(headers, rows);
|
|
3061
|
+
spacer();
|
|
3062
|
+
info("Subscribe with:");
|
|
3063
|
+
console.log(c.muted(" anonli subscribe --product bundle --tier plus --frequency monthly"));
|
|
3064
|
+
console.log(c.muted(" anonli subscribe --product alias --tier pro --frequency yearly"));
|
|
3065
|
+
console.log(c.muted(" anonli subscribe --product drop --tier plus --frequency yearly --promo CODE"));
|
|
3066
|
+
}
|
|
3067
|
+
var subscribeCommand = new Command24("subscribe").description("Subscribe to a paid plan").option("--product <product>", "Product: bundle, alias, or drop").option("--tier <tier>", "Tier: plus or pro").option("--frequency <frequency>", "Billing: monthly or yearly").option("--promo <code>", "Promotion code").option("--no-open", "Don't open checkout URL in browser").action(async (options) => {
|
|
3068
|
+
if (!options.product && !options.tier && !options.frequency) {
|
|
3069
|
+
showPricingTable();
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
3072
|
+
if (!options.product || !options.tier || !options.frequency) {
|
|
3073
|
+
errorBox(
|
|
3074
|
+
"Missing Options",
|
|
3075
|
+
"All three options are required: --product, --tier, --frequency",
|
|
3076
|
+
"Run 'anonli subscribe' without options to see available plans."
|
|
3077
|
+
);
|
|
3078
|
+
process.exit(1);
|
|
3079
|
+
}
|
|
3080
|
+
const validProducts = ["bundle", "alias", "drop"];
|
|
3081
|
+
const validTiers = ["plus", "pro"];
|
|
3082
|
+
const validFrequencies = ["monthly", "yearly"];
|
|
3083
|
+
if (!validProducts.includes(options.product)) {
|
|
3084
|
+
error(`Invalid product: ${options.product}. Must be one of: ${validProducts.join(", ")}`);
|
|
3085
|
+
process.exit(1);
|
|
3086
|
+
}
|
|
3087
|
+
if (!validTiers.includes(options.tier)) {
|
|
3088
|
+
error(`Invalid tier: ${options.tier}. Must be one of: ${validTiers.join(", ")}`);
|
|
3089
|
+
process.exit(1);
|
|
3090
|
+
}
|
|
3091
|
+
if (!validFrequencies.includes(options.frequency)) {
|
|
3092
|
+
error(`Invalid frequency: ${options.frequency}. Must be one of: ${validFrequencies.join(", ")}`);
|
|
3093
|
+
process.exit(1);
|
|
3094
|
+
}
|
|
3095
|
+
requireAuth();
|
|
3096
|
+
const spin = spinner("Creating checkout session...");
|
|
3097
|
+
try {
|
|
3098
|
+
const body = {
|
|
3099
|
+
product: options.product,
|
|
3100
|
+
tier: options.tier,
|
|
3101
|
+
frequency: options.frequency
|
|
3102
|
+
};
|
|
3103
|
+
if (options.promo) {
|
|
3104
|
+
body.promoCode = options.promo;
|
|
3105
|
+
}
|
|
3106
|
+
const result = await apiPost("/api/v1/checkout", body);
|
|
3107
|
+
spin.stop();
|
|
3108
|
+
const url = result.data.url;
|
|
3109
|
+
successBox(
|
|
3110
|
+
"Checkout Ready",
|
|
3111
|
+
`${c.secondary("Complete your subscription in the browser:")}
|
|
3112
|
+
${link(url)}`
|
|
3113
|
+
);
|
|
3114
|
+
if (options.open !== false) {
|
|
3115
|
+
openUrl(url);
|
|
3116
|
+
info("Opening checkout in your default browser...");
|
|
3117
|
+
}
|
|
3118
|
+
showRateLimit(result.rateLimit);
|
|
3119
|
+
} catch (err) {
|
|
3120
|
+
spin.fail("Failed to create checkout session.");
|
|
3121
|
+
error(err instanceof Error ? err.message : "Unknown error");
|
|
3122
|
+
process.exit(1);
|
|
3123
|
+
}
|
|
3124
|
+
});
|
|
3125
|
+
|
|
3126
|
+
// src/commands/completions.ts
|
|
3127
|
+
import { Command as Command25 } from "commander";
|
|
3128
|
+
var BASH_COMPLETION = `# anonli bash completion
|
|
3129
|
+
# Add to ~/.bashrc: source <(anonli completions bash)
|
|
3130
|
+
_anonli_completions() {
|
|
3131
|
+
local cur prev words cword
|
|
3132
|
+
_init_completion 2>/dev/null || {
|
|
3133
|
+
COMPREPLY=()
|
|
3134
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
3135
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
local commands="login logout whoami drop alias recipient domain apikey config update subscribe completions"
|
|
3139
|
+
local drop_cmds="upload list info download delete toggle share"
|
|
3140
|
+
local alias_cmds="new list delete toggle update stats"
|
|
3141
|
+
local recipient_cmds="list add delete default verify pgp"
|
|
3142
|
+
local domain_cmds="list add verify info delete dkim"
|
|
3143
|
+
local apikey_cmds="list create delete"
|
|
3144
|
+
local config_cmds="get set validate"
|
|
3145
|
+
|
|
3146
|
+
case "\${COMP_WORDS[1]}" in
|
|
3147
|
+
drop)
|
|
3148
|
+
COMPREPLY=($(compgen -W "\${drop_cmds}" -- "\${cur}"))
|
|
3149
|
+
;;
|
|
3150
|
+
alias)
|
|
3151
|
+
COMPREPLY=($(compgen -W "\${alias_cmds}" -- "\${cur}"))
|
|
3152
|
+
;;
|
|
3153
|
+
recipient|recipients)
|
|
3154
|
+
COMPREPLY=($(compgen -W "\${recipient_cmds}" -- "\${cur}"))
|
|
3155
|
+
;;
|
|
3156
|
+
domain|domains)
|
|
3157
|
+
COMPREPLY=($(compgen -W "\${domain_cmds}" -- "\${cur}"))
|
|
3158
|
+
;;
|
|
3159
|
+
apikey|api-key)
|
|
3160
|
+
COMPREPLY=($(compgen -W "\${apikey_cmds}" -- "\${cur}"))
|
|
3161
|
+
;;
|
|
3162
|
+
config)
|
|
3163
|
+
COMPREPLY=($(compgen -W "\${config_cmds}" -- "\${cur}"))
|
|
3164
|
+
;;
|
|
3165
|
+
*)
|
|
3166
|
+
COMPREPLY=($(compgen -W "\${commands}" -- "\${cur}"))
|
|
3167
|
+
;;
|
|
3168
|
+
esac
|
|
3169
|
+
}
|
|
3170
|
+
complete -F _anonli_completions anonli
|
|
3171
|
+
`;
|
|
3172
|
+
var ZSH_COMPLETION = `# anonli zsh completion
|
|
3173
|
+
# Add to ~/.zshrc: source <(anonli completions zsh)
|
|
3174
|
+
_anonli() {
|
|
3175
|
+
local state
|
|
3176
|
+
typeset -A opt_args
|
|
3177
|
+
|
|
3178
|
+
_arguments \\
|
|
3179
|
+
'1: :->command' \\
|
|
3180
|
+
'*: :->args'
|
|
3181
|
+
|
|
3182
|
+
case $state in
|
|
3183
|
+
command)
|
|
3184
|
+
local commands=(
|
|
3185
|
+
'login:Authenticate with your API key'
|
|
3186
|
+
'logout:Remove stored credentials'
|
|
3187
|
+
'whoami:Show current user info'
|
|
3188
|
+
'drop:Encrypted file drops'
|
|
3189
|
+
'alias:Manage email aliases'
|
|
3190
|
+
'recipient:Manage email recipients'
|
|
3191
|
+
'domain:Manage custom domains'
|
|
3192
|
+
'apikey:Manage API keys'
|
|
3193
|
+
'config:View or update CLI configuration'
|
|
3194
|
+
'update:Update anonli to latest version'
|
|
3195
|
+
'subscribe:Subscribe to a paid plan'
|
|
3196
|
+
'completions:Generate shell completion script'
|
|
3197
|
+
)
|
|
3198
|
+
_describe 'command' commands
|
|
3199
|
+
;;
|
|
3200
|
+
args)
|
|
3201
|
+
case $words[2] in
|
|
3202
|
+
drop)
|
|
3203
|
+
local drop_cmds=(
|
|
3204
|
+
'upload:Create an encrypted drop'
|
|
3205
|
+
'list:List your drops'
|
|
3206
|
+
'info:View drop details'
|
|
3207
|
+
'download:Download and decrypt a drop'
|
|
3208
|
+
'delete:Delete a drop'
|
|
3209
|
+
'toggle:Toggle enabled/disabled state'
|
|
3210
|
+
'share:Reconstruct a share URL'
|
|
3211
|
+
)
|
|
3212
|
+
_describe 'drop command' drop_cmds
|
|
3213
|
+
;;
|
|
3214
|
+
alias)
|
|
3215
|
+
local alias_cmds=(
|
|
3216
|
+
'new:Create a new alias'
|
|
3217
|
+
'list:List all aliases'
|
|
3218
|
+
'delete:Delete an alias'
|
|
3219
|
+
'toggle:Toggle alias active state'
|
|
3220
|
+
'update:Update alias settings'
|
|
3221
|
+
'stats:Show forwarding statistics'
|
|
3222
|
+
)
|
|
3223
|
+
_describe 'alias command' alias_cmds
|
|
3224
|
+
;;
|
|
3225
|
+
config)
|
|
3226
|
+
local config_cmds=('get:Show config' 'set:Update config value' 'validate:Check config health')
|
|
3227
|
+
_describe 'config command' config_cmds
|
|
3228
|
+
;;
|
|
3229
|
+
esac
|
|
3230
|
+
;;
|
|
3231
|
+
esac
|
|
3232
|
+
}
|
|
3233
|
+
compdef _anonli anonli
|
|
3234
|
+
`;
|
|
3235
|
+
var FISH_COMPLETION = `# anonli fish completion
|
|
3236
|
+
# Add to ~/.config/fish/completions/anonli.fish or run: anonli completions fish | source
|
|
3237
|
+
|
|
3238
|
+
# Disable file completions by default
|
|
3239
|
+
complete -c anonli -f
|
|
3240
|
+
|
|
3241
|
+
# Main commands
|
|
3242
|
+
complete -c anonli -n '__fish_use_subcommand' -a login -d 'Authenticate with your API key'
|
|
3243
|
+
complete -c anonli -n '__fish_use_subcommand' -a logout -d 'Remove stored credentials'
|
|
3244
|
+
complete -c anonli -n '__fish_use_subcommand' -a whoami -d 'Show current user info'
|
|
3245
|
+
complete -c anonli -n '__fish_use_subcommand' -a drop -d 'Encrypted file drops'
|
|
3246
|
+
complete -c anonli -n '__fish_use_subcommand' -a alias -d 'Manage email aliases'
|
|
3247
|
+
complete -c anonli -n '__fish_use_subcommand' -a recipient -d 'Manage email recipients'
|
|
3248
|
+
complete -c anonli -n '__fish_use_subcommand' -a domain -d 'Manage custom domains'
|
|
3249
|
+
complete -c anonli -n '__fish_use_subcommand' -a apikey -d 'Manage API keys'
|
|
3250
|
+
complete -c anonli -n '__fish_use_subcommand' -a config -d 'View or update CLI configuration'
|
|
3251
|
+
complete -c anonli -n '__fish_use_subcommand' -a update -d 'Update anonli to latest version'
|
|
3252
|
+
complete -c anonli -n '__fish_use_subcommand' -a subscribe -d 'Subscribe to a paid plan'
|
|
3253
|
+
complete -c anonli -n '__fish_use_subcommand' -a completions -d 'Generate shell completion script'
|
|
3254
|
+
|
|
3255
|
+
# Quiet flag
|
|
3256
|
+
complete -c anonli -s q -l quiet -d 'Suppress non-essential output'
|
|
3257
|
+
|
|
3258
|
+
# drop subcommands
|
|
3259
|
+
complete -c anonli -n '__fish_seen_subcommand_from drop' -a upload -d 'Create an encrypted drop'
|
|
3260
|
+
complete -c anonli -n '__fish_seen_subcommand_from drop' -a list -d 'List your drops'
|
|
3261
|
+
complete -c anonli -n '__fish_seen_subcommand_from drop' -a info -d 'View drop details'
|
|
3262
|
+
complete -c anonli -n '__fish_seen_subcommand_from drop' -a download -d 'Download and decrypt a drop'
|
|
3263
|
+
complete -c anonli -n '__fish_seen_subcommand_from drop' -a delete -d 'Delete a drop'
|
|
3264
|
+
complete -c anonli -n '__fish_seen_subcommand_from drop' -a toggle -d 'Toggle enabled/disabled state'
|
|
3265
|
+
complete -c anonli -n '__fish_seen_subcommand_from drop' -a share -d 'Reconstruct a share URL'
|
|
3266
|
+
|
|
3267
|
+
# alias subcommands
|
|
3268
|
+
complete -c anonli -n '__fish_seen_subcommand_from alias' -a new -d 'Create a new alias'
|
|
3269
|
+
complete -c anonli -n '__fish_seen_subcommand_from alias' -a list -d 'List all aliases'
|
|
3270
|
+
complete -c anonli -n '__fish_seen_subcommand_from alias' -a delete -d 'Delete an alias'
|
|
3271
|
+
complete -c anonli -n '__fish_seen_subcommand_from alias' -a toggle -d 'Toggle alias active state'
|
|
3272
|
+
complete -c anonli -n '__fish_seen_subcommand_from alias' -a update -d 'Update alias settings'
|
|
3273
|
+
complete -c anonli -n '__fish_seen_subcommand_from alias' -a stats -d 'Show forwarding statistics'
|
|
3274
|
+
|
|
3275
|
+
# config subcommands
|
|
3276
|
+
complete -c anonli -n '__fish_seen_subcommand_from config' -a get -d 'Show current config'
|
|
3277
|
+
complete -c anonli -n '__fish_seen_subcommand_from config' -a set -d 'Update a config value'
|
|
3278
|
+
complete -c anonli -n '__fish_seen_subcommand_from config' -a validate -d 'Check config health'
|
|
3279
|
+
|
|
3280
|
+
# completions shells
|
|
3281
|
+
complete -c anonli -n '__fish_seen_subcommand_from completions' -a bash -d 'Bash completion script'
|
|
3282
|
+
complete -c anonli -n '__fish_seen_subcommand_from completions' -a zsh -d 'Zsh completion script'
|
|
3283
|
+
complete -c anonli -n '__fish_seen_subcommand_from completions' -a fish -d 'Fish completion script'
|
|
3284
|
+
`;
|
|
3285
|
+
var completionsCommand = new Command25("completions").description("Generate shell completion script").argument("<shell>", "Shell type: bash, zsh, or fish").addHelpText("after", `
|
|
3286
|
+
Examples:
|
|
3287
|
+
# bash \u2014 add to ~/.bashrc
|
|
3288
|
+
source <(anonli completions bash)
|
|
3289
|
+
|
|
3290
|
+
# zsh \u2014 add to ~/.zshrc
|
|
3291
|
+
source <(anonli completions zsh)
|
|
3292
|
+
|
|
3293
|
+
# fish \u2014 save to completions directory
|
|
3294
|
+
anonli completions fish > ~/.config/fish/completions/anonli.fish`).action((shell) => {
|
|
3295
|
+
switch (shell.toLowerCase()) {
|
|
3296
|
+
case "bash":
|
|
3297
|
+
process.stdout.write(BASH_COMPLETION);
|
|
3298
|
+
break;
|
|
3299
|
+
case "zsh":
|
|
3300
|
+
process.stdout.write(ZSH_COMPLETION);
|
|
3301
|
+
break;
|
|
3302
|
+
case "fish":
|
|
3303
|
+
process.stdout.write(FISH_COMPLETION);
|
|
3304
|
+
break;
|
|
3305
|
+
default:
|
|
3306
|
+
error(`Unknown shell: ${shell}. Supported shells: bash, zsh, fish`);
|
|
3307
|
+
process.exit(1);
|
|
3308
|
+
}
|
|
3309
|
+
});
|
|
3310
|
+
|
|
3311
|
+
// src/lib/version-check.ts
|
|
3312
|
+
init_config2();
|
|
3313
|
+
import semver from "semver";
|
|
3314
|
+
var CACHE_DURATION_MS = 24 * 60 * 60 * 1e3;
|
|
3315
|
+
async function checkForUpdates(currentVersion) {
|
|
3316
|
+
if (process.env.ANONLI_NO_UPDATE_CHECK) return;
|
|
3317
|
+
const config = loadConfig();
|
|
3318
|
+
if (config.lastVersionCheck && config.latestVersion && Date.now() - config.lastVersionCheck < CACHE_DURATION_MS) {
|
|
3319
|
+
printUpdateNotice(currentVersion, config.latestVersion);
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
try {
|
|
3323
|
+
const controller = new AbortController();
|
|
3324
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
3325
|
+
const res = await fetch("https://registry.npmjs.org/anonli/latest", {
|
|
3326
|
+
signal: controller.signal
|
|
3327
|
+
});
|
|
3328
|
+
clearTimeout(timeout);
|
|
3329
|
+
if (!res.ok) return;
|
|
3330
|
+
const data = await res.json();
|
|
3331
|
+
const latestVersion = data.version;
|
|
3332
|
+
config.lastVersionCheck = Date.now();
|
|
3333
|
+
config.latestVersion = latestVersion;
|
|
3334
|
+
saveConfig(config);
|
|
3335
|
+
printUpdateNotice(currentVersion, latestVersion);
|
|
3336
|
+
} catch {
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
function printUpdateNotice(current, latest) {
|
|
3340
|
+
if (semver.gt(latest, current)) {
|
|
3341
|
+
updateNotice(current, latest);
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
// src/index.ts
|
|
3346
|
+
init_errors();
|
|
3347
|
+
init_constants();
|
|
3348
|
+
init_config2();
|
|
3349
|
+
var VERSION = CLI_VERSION;
|
|
3350
|
+
var helpConfig = createHelpConfig(VERSION);
|
|
3351
|
+
var program = new Command26().name("anonli").description("anon.li CLI - encrypted file drops & anonymous email aliases").version(VERSION, "-v, --version").option("-q, --quiet", "Suppress all non-essential output (spinners, tables, boxes)").configureHelp(helpConfig);
|
|
3352
|
+
program.addCommand(loginCommand);
|
|
3353
|
+
program.addCommand(logoutCommand);
|
|
3354
|
+
program.addCommand(whoamiCommand);
|
|
3355
|
+
program.addCommand(dropCommand);
|
|
3356
|
+
program.addCommand(aliasCommand);
|
|
3357
|
+
program.addCommand(recipientCommand);
|
|
3358
|
+
program.addCommand(domainCommand);
|
|
3359
|
+
program.addCommand(apikeyCommand);
|
|
3360
|
+
program.addCommand(configCommand);
|
|
3361
|
+
program.addCommand(updateCommand);
|
|
3362
|
+
program.addCommand(subscribeCommand);
|
|
3363
|
+
program.addCommand(completionsCommand);
|
|
3364
|
+
program.exitOverride();
|
|
3365
|
+
function propagateSettings(cmd) {
|
|
3366
|
+
for (const sub of cmd.commands) {
|
|
3367
|
+
sub.configureHelp(helpConfig);
|
|
3368
|
+
sub.exitOverride();
|
|
3369
|
+
propagateSettings(sub);
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
propagateSettings(program);
|
|
3373
|
+
var AUTH_EXEMPT = /* @__PURE__ */ new Set([
|
|
3374
|
+
"login",
|
|
3375
|
+
"logout",
|
|
3376
|
+
"config",
|
|
3377
|
+
"update",
|
|
3378
|
+
"completions",
|
|
3379
|
+
"drop info",
|
|
3380
|
+
"drop get",
|
|
3381
|
+
"drop download",
|
|
3382
|
+
"drop dl",
|
|
3383
|
+
"drop share"
|
|
3384
|
+
]);
|
|
3385
|
+
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
3386
|
+
const rootOpts = program.opts();
|
|
3387
|
+
if (rootOpts.quiet) {
|
|
3388
|
+
setQuiet(true);
|
|
3389
|
+
}
|
|
3390
|
+
const parts = [];
|
|
3391
|
+
let cmd = actionCommand;
|
|
3392
|
+
while (cmd && cmd !== program) {
|
|
3393
|
+
parts.unshift(cmd.name());
|
|
3394
|
+
cmd = cmd.parent;
|
|
3395
|
+
}
|
|
3396
|
+
const commandPath = parts.join(" ");
|
|
3397
|
+
if (AUTH_EXEMPT.has(commandPath)) return;
|
|
3398
|
+
if (getApiKey()) return;
|
|
3399
|
+
if (!process.stdin.isTTY) {
|
|
3400
|
+
throw new AuthError(
|
|
3401
|
+
"Not authenticated. Set ANONLI_API_KEY or run `anonli login` interactively."
|
|
3402
|
+
);
|
|
3403
|
+
}
|
|
3404
|
+
warn("Authentication required. Let's set up your API key.");
|
|
3405
|
+
spacer();
|
|
3406
|
+
const success2 = await runAuthFlow();
|
|
3407
|
+
if (!success2) {
|
|
3408
|
+
throw new AuthError("Authentication failed.");
|
|
3409
|
+
}
|
|
3410
|
+
spacer();
|
|
3411
|
+
});
|
|
3412
|
+
program.hook("postAction", async () => {
|
|
3413
|
+
await checkForUpdates(VERSION).catch(() => {
|
|
3414
|
+
});
|
|
3415
|
+
});
|
|
3416
|
+
async function main() {
|
|
3417
|
+
try {
|
|
3418
|
+
await program.parseAsync(process.argv);
|
|
3419
|
+
} catch (err) {
|
|
3420
|
+
if (err instanceof PlanLimitError) {
|
|
3421
|
+
errorBox("Plan Limit", err.message, err.suggestion);
|
|
3422
|
+
process.exit(err.exitCode);
|
|
3423
|
+
}
|
|
3424
|
+
if (err instanceof AuthError) {
|
|
3425
|
+
error(err.message);
|
|
3426
|
+
process.exit(err.exitCode);
|
|
3427
|
+
}
|
|
3428
|
+
if (err instanceof CliError) {
|
|
3429
|
+
process.exit(err.exitCode);
|
|
3430
|
+
}
|
|
3431
|
+
const code = err?.code;
|
|
3432
|
+
if (typeof code === "string" && code.startsWith("commander.")) {
|
|
3433
|
+
process.exit(0);
|
|
3434
|
+
}
|
|
3435
|
+
if (err instanceof Error) {
|
|
3436
|
+
console.error(`Error: ${err.message}`);
|
|
3437
|
+
}
|
|
3438
|
+
process.exit(1);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
main();
|
|
3442
|
+
//# sourceMappingURL=index.js.map
|