droppage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +117 -0
  4. package/dist/cli.js +1292 -0
  5. package/package.json +64 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1292 @@
1
+ #!/usr/bin/env node
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 __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // package.json
13
+ var package_default;
14
+ var init_package = __esm({
15
+ "package.json"() {
16
+ package_default = {
17
+ name: "droppage",
18
+ version: "0.1.0",
19
+ description: "Publish HTML files as shareable, access-controlled live pages",
20
+ type: "module",
21
+ license: "Apache-2.0",
22
+ author: "Vilancer",
23
+ repository: {
24
+ type: "git",
25
+ url: "git+https://github.com/Vilancer/droppage-cli.git"
26
+ },
27
+ homepage: "https://droppage.dev",
28
+ bugs: {
29
+ url: "https://github.com/Vilancer/droppage-cli/issues"
30
+ },
31
+ keywords: [
32
+ "html",
33
+ "publish",
34
+ "share",
35
+ "report",
36
+ "cli",
37
+ "link",
38
+ "access-control",
39
+ "agents"
40
+ ],
41
+ engines: {
42
+ node: ">=22"
43
+ },
44
+ bin: {
45
+ droppage: "dist/cli.js"
46
+ },
47
+ files: [
48
+ "dist",
49
+ "NOTICE"
50
+ ],
51
+ publishConfig: {
52
+ access: "public"
53
+ },
54
+ scripts: {
55
+ dev: "tsx src/cli.ts",
56
+ build: "tsup",
57
+ test: "vitest run",
58
+ "test:watch": "vitest",
59
+ prepublishOnly: `node -e "require('fs').accessSync('.publish-ok')" && npm run test && npm run build`
60
+ },
61
+ dependencies: {
62
+ "@inquirer/prompts": "8.5.2",
63
+ "@supabase/supabase-js": "2.110.8",
64
+ bcryptjs: "3.0.3",
65
+ commander: "15.0.0",
66
+ conf: "15.1.0",
67
+ dotenv: "17.4.2",
68
+ nanoid: "5.1.16",
69
+ open: "11.0.0",
70
+ zod: "4.4.3"
71
+ },
72
+ devDependencies: {
73
+ "@types/node": "^22.15.0",
74
+ tsup: "8.5.1",
75
+ tsx: "4.23.1",
76
+ typescript: "5.9.3",
77
+ vitest: "4.1.10"
78
+ }
79
+ };
80
+ }
81
+ });
82
+
83
+ // src/lib/callback-server.ts
84
+ import { createServer } from "http";
85
+ function closeServer(server) {
86
+ return new Promise((resolve) => {
87
+ server.close(() => resolve());
88
+ });
89
+ }
90
+ function tryListen(port) {
91
+ return new Promise((resolve, reject) => {
92
+ const server = createServer();
93
+ const onError = (err) => {
94
+ server.off("listening", onListening);
95
+ reject(err);
96
+ };
97
+ const onListening = () => {
98
+ server.off("error", onError);
99
+ resolve(server);
100
+ };
101
+ server.once("error", onError);
102
+ server.once("listening", onListening);
103
+ server.listen(port, LOOPBACK);
104
+ });
105
+ }
106
+ async function pickCallbackPort() {
107
+ const candidates = [PREFERRED_PORT, ...FALLBACK_PORTS];
108
+ for (const port of candidates) {
109
+ try {
110
+ const server = await tryListen(port);
111
+ await closeServer(server);
112
+ return port;
113
+ } catch (err) {
114
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
115
+ if (code !== "EADDRINUSE") {
116
+ throw err;
117
+ }
118
+ }
119
+ }
120
+ throw new Error(
121
+ `No free localhost callback port in range ${PREFERRED_PORT}\u2013${FALLBACK_PORTS[FALLBACK_PORTS.length - 1]}`
122
+ );
123
+ }
124
+ function normalizeArgs(portOrOptions, pathArg) {
125
+ if (typeof portOrOptions === "number") {
126
+ return {
127
+ port: portOrOptions,
128
+ path: pathArg ?? DEFAULT_PATH,
129
+ timeoutMs: DEFAULT_TIMEOUT_MS
130
+ };
131
+ }
132
+ return {
133
+ port: portOrOptions.port,
134
+ path: portOrOptions.path ?? DEFAULT_PATH,
135
+ timeoutMs: portOrOptions.timeoutMs ?? DEFAULT_TIMEOUT_MS
136
+ };
137
+ }
138
+ function waitForAuthCode(portOrOptions, pathArg) {
139
+ const { port, path, timeoutMs } = normalizeArgs(portOrOptions, pathArg);
140
+ return new Promise((resolve, reject) => {
141
+ let settled = false;
142
+ let timeout;
143
+ const settle = (fn) => {
144
+ if (settled) return;
145
+ settled = true;
146
+ if (timeout) clearTimeout(timeout);
147
+ fn();
148
+ };
149
+ const server = createServer((req, res) => {
150
+ const url = new URL(req.url ?? "/", `http://${LOOPBACK}:${port}`);
151
+ if (url.pathname !== path) {
152
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
153
+ res.end("Not found");
154
+ return;
155
+ }
156
+ const code = url.searchParams.get("code");
157
+ const error = url.searchParams.get("error");
158
+ const errorDescription = url.searchParams.get("error_description");
159
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
160
+ res.end(SUCCESS_BODY);
161
+ void closeServer(server).then(() => {
162
+ if (code) {
163
+ settle(() => resolve(code));
164
+ return;
165
+ }
166
+ const message = errorDescription || error || "No auth code in callback";
167
+ settle(() => reject(new Error(message)));
168
+ });
169
+ });
170
+ server.on("error", (err) => {
171
+ settle(() => reject(err));
172
+ });
173
+ timeout = setTimeout(() => {
174
+ void closeServer(server).then(() => {
175
+ settle(
176
+ () => reject(new Error(`Timed out waiting for auth callback on port ${port}`))
177
+ );
178
+ });
179
+ }, timeoutMs);
180
+ server.listen(port, LOOPBACK);
181
+ });
182
+ }
183
+ var LOOPBACK, PREFERRED_PORT, FALLBACK_PORTS, DEFAULT_PATH, DEFAULT_TIMEOUT_MS, SUCCESS_BODY;
184
+ var init_callback_server = __esm({
185
+ "src/lib/callback-server.ts"() {
186
+ "use strict";
187
+ LOOPBACK = "127.0.0.1";
188
+ PREFERRED_PORT = 19876;
189
+ FALLBACK_PORTS = [19877, 19878, 19879, 19880, 19881, 19882, 19883, 19884, 19885];
190
+ DEFAULT_PATH = "/callback";
191
+ DEFAULT_TIMEOUT_MS = 3e5;
192
+ SUCCESS_BODY = "You can close this window and return to the terminal.";
193
+ }
194
+ });
195
+
196
+ // src/lib/supabase-defaults.ts
197
+ var DEFAULT_SUPABASE_URL, DEFAULT_SUPABASE_ANON_KEY;
198
+ var init_supabase_defaults = __esm({
199
+ "src/lib/supabase-defaults.ts"() {
200
+ "use strict";
201
+ DEFAULT_SUPABASE_URL = "https://kqumkzpwufiepbbtmsbv.supabase.co";
202
+ DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtxdW1renB3dWZpZXBiYnRtc2J2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODQ2NjE4OTUsImV4cCI6MjEwMDIzNzg5NX0.Od7YN0Rks8BfrEmcpiijBfdogg6x2jUbsYf_QeyC1lU";
203
+ }
204
+ });
205
+
206
+ // src/lib/env.ts
207
+ import { config as loadDotenv } from "dotenv";
208
+ import { join } from "path";
209
+ import { z } from "zod";
210
+ function rejectSecrets(label, value) {
211
+ if (SECRET_PATTERN.test(value)) {
212
+ throw new Error(
213
+ `${label} must not contain service_role or sb_secret (ARCH-02)`
214
+ );
215
+ }
216
+ }
217
+ function resolveSupabaseEnv() {
218
+ loadDotenv({ path: join(process.cwd(), ".env") });
219
+ const url = process.env.DROPPAGE_SUPABASE_URL || process.env.SUPABASE_URL || DEFAULT_SUPABASE_URL;
220
+ const anonKey = process.env.DROPPAGE_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
221
+ if (!url.trim() || !anonKey.trim()) {
222
+ throw new Error(
223
+ "Missing Supabase configuration. Set DROPPAGE_SUPABASE_URL and DROPPAGE_SUPABASE_ANON_KEY (or SUPABASE_URL / SUPABASE_ANON_KEY)."
224
+ );
225
+ }
226
+ rejectSecrets("DROPPAGE_SUPABASE_URL / URL", url);
227
+ rejectSecrets("DROPPAGE_SUPABASE_ANON_KEY / anon key", anonKey);
228
+ return {
229
+ url: urlSchema.parse(url),
230
+ anonKey: anonKeySchema.parse(anonKey)
231
+ };
232
+ }
233
+ var SECRET_PATTERN, urlSchema, anonKeySchema;
234
+ var init_env = __esm({
235
+ "src/lib/env.ts"() {
236
+ "use strict";
237
+ init_supabase_defaults();
238
+ SECRET_PATTERN = /service_role|sb_secret/i;
239
+ urlSchema = z.string().url();
240
+ anonKeySchema = z.string().min(1);
241
+ }
242
+ });
243
+
244
+ // src/lib/config.ts
245
+ import Conf from "conf";
246
+ import { homedir } from "os";
247
+ import { join as join2 } from "path";
248
+ function validateConfig(data) {
249
+ for (const [key, value] of Object.entries(data)) {
250
+ if (FORBIDDEN.test(key)) {
251
+ throw new Error(
252
+ `Config key rejected: must not contain service_role or sb_secret (${key})`
253
+ );
254
+ }
255
+ if (typeof value === "string" && FORBIDDEN.test(value)) {
256
+ throw new Error(
257
+ "Config value rejected: must not contain service_role or sb_secret"
258
+ );
259
+ }
260
+ }
261
+ }
262
+ function getConf() {
263
+ const home = homedir();
264
+ if (!confInstance || confHome !== home) {
265
+ confHome = home;
266
+ confInstance = new Conf({
267
+ cwd: join2(home, ".droppage"),
268
+ configName: "config",
269
+ configFileMode: 384
270
+ });
271
+ }
272
+ return confInstance;
273
+ }
274
+ function createConfStorage() {
275
+ const conf = getConf();
276
+ return {
277
+ getItem: (key) => {
278
+ const value = conf.get(key);
279
+ return typeof value === "string" ? value : null;
280
+ },
281
+ setItem: (key, value) => {
282
+ validateConfig({ [key]: value });
283
+ conf.set(key, value);
284
+ },
285
+ removeItem: (key) => {
286
+ conf.delete(key);
287
+ }
288
+ };
289
+ }
290
+ var FORBIDDEN, confInstance, confHome;
291
+ var init_config = __esm({
292
+ "src/lib/config.ts"() {
293
+ "use strict";
294
+ FORBIDDEN = /service_role|sb_secret/i;
295
+ }
296
+ });
297
+
298
+ // src/lib/supabase.ts
299
+ import { createClient } from "@supabase/supabase-js";
300
+ function createSupabaseClient() {
301
+ if (client) {
302
+ return client;
303
+ }
304
+ const { url, anonKey } = resolveSupabaseEnv();
305
+ client = createClient(url, anonKey, {
306
+ auth: {
307
+ flowType: "pkce",
308
+ persistSession: true,
309
+ autoRefreshToken: true,
310
+ detectSessionInUrl: false,
311
+ storage: createConfStorage()
312
+ }
313
+ });
314
+ return client;
315
+ }
316
+ var client;
317
+ var init_supabase = __esm({
318
+ "src/lib/supabase.ts"() {
319
+ "use strict";
320
+ init_config();
321
+ init_env();
322
+ }
323
+ });
324
+
325
+ // src/services/auth.ts
326
+ import { input } from "@inquirer/prompts";
327
+ import open from "open";
328
+ import { z as z2 } from "zod";
329
+ function isLocalSupabaseUrl(url) {
330
+ try {
331
+ const parsed = new URL(url);
332
+ const localHost = parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost";
333
+ if (!localHost) return false;
334
+ return parsed.port === "54321" || parsed.port === "";
335
+ } catch {
336
+ return false;
337
+ }
338
+ }
339
+ async function login() {
340
+ const supabase = createSupabaseClient();
341
+ const { data, error } = await supabase.auth.getUser();
342
+ if (!error && data.user?.email) {
343
+ console.log(`Logged in as ${data.user.email}`);
344
+ process.exitCode = 0;
345
+ return;
346
+ }
347
+ if (error) {
348
+ await supabase.auth.signOut({ scope: "local" });
349
+ }
350
+ const emailRaw = await input({
351
+ message: "Email:",
352
+ validate: (value) => {
353
+ const result = emailSchema.safeParse(value.trim());
354
+ return result.success ? true : "Enter a valid email address";
355
+ }
356
+ });
357
+ const email = emailRaw.trim();
358
+ const port = await pickCallbackPort();
359
+ const emailRedirectTo = `http://127.0.0.1:${port}/callback`;
360
+ const { error: otpError } = await supabase.auth.signInWithOtp({
361
+ email,
362
+ options: {
363
+ shouldCreateUser: true,
364
+ emailRedirectTo
365
+ }
366
+ });
367
+ if (otpError) {
368
+ console.error(`Failed to send magic link: ${otpError.message}`);
369
+ process.exitCode = 1;
370
+ throw otpError;
371
+ }
372
+ const codePromise = waitForAuthCode({ port, path: "/callback" });
373
+ console.log("Check your email for a magic link to sign in.");
374
+ console.log(
375
+ `(debug) After you click the link, Auth redirects to: ${emailRedirectTo}`
376
+ );
377
+ const { url } = resolveSupabaseEnv();
378
+ if (isLocalSupabaseUrl(url)) {
379
+ try {
380
+ await open(INBUCKET_URL);
381
+ } catch {
382
+ }
383
+ }
384
+ try {
385
+ const code = await codePromise;
386
+ const { data: sessionData, error: exchangeError } = await supabase.auth.exchangeCodeForSession(code);
387
+ if (exchangeError || !sessionData.user?.email) {
388
+ const message = exchangeError?.message ?? "No user after code exchange";
389
+ console.error(`Login failed: ${message}`);
390
+ process.exitCode = 1;
391
+ throw exchangeError ?? new Error(message);
392
+ }
393
+ console.log(`Logged in as ${sessionData.user.email}`);
394
+ process.exitCode = 0;
395
+ } catch (err) {
396
+ if (process.exitCode !== 1) {
397
+ console.error(
398
+ `Login failed: ${err instanceof Error ? err.message : String(err)}`
399
+ );
400
+ process.exitCode = 1;
401
+ }
402
+ throw err;
403
+ }
404
+ }
405
+ async function logout() {
406
+ const supabase = createSupabaseClient();
407
+ const { data } = await supabase.auth.getSession();
408
+ if (!data.session) {
409
+ console.log("Already logged out");
410
+ process.exitCode = 0;
411
+ return;
412
+ }
413
+ await supabase.auth.signOut();
414
+ console.log("Logged out");
415
+ process.exitCode = 0;
416
+ }
417
+ async function requireAuth() {
418
+ const supabase = createSupabaseClient();
419
+ const { data, error } = await supabase.auth.getUser();
420
+ if (error || !data.user) {
421
+ await supabase.auth.signOut({ scope: "local" });
422
+ console.error(AUTH_REQUIRED_MESSAGE);
423
+ process.exitCode = 1;
424
+ throw new AuthRequiredError();
425
+ }
426
+ return data.user;
427
+ }
428
+ var emailSchema, INBUCKET_URL, AUTH_REQUIRED_MESSAGE, AuthRequiredError;
429
+ var init_auth = __esm({
430
+ "src/services/auth.ts"() {
431
+ "use strict";
432
+ init_callback_server();
433
+ init_env();
434
+ init_supabase();
435
+ emailSchema = z2.email();
436
+ INBUCKET_URL = "http://127.0.0.1:54324";
437
+ AUTH_REQUIRED_MESSAGE = "Not logged in. Run `droppage login` and try again.";
438
+ AuthRequiredError = class extends Error {
439
+ constructor(message = AUTH_REQUIRED_MESSAGE) {
440
+ super(message);
441
+ this.name = "AuthRequiredError";
442
+ }
443
+ };
444
+ }
445
+ });
446
+
447
+ // src/commands/login.ts
448
+ function registerLogin(program2) {
449
+ program2.command("login").description("Authenticate with Supabase").action(async () => {
450
+ await login();
451
+ });
452
+ }
453
+ var init_login = __esm({
454
+ "src/commands/login.ts"() {
455
+ "use strict";
456
+ init_auth();
457
+ }
458
+ });
459
+
460
+ // src/commands/logout.ts
461
+ function registerLogout(program2) {
462
+ program2.command("logout").description("Sign out and clear local credentials").action(async () => {
463
+ await logout();
464
+ });
465
+ }
466
+ var init_logout = __esm({
467
+ "src/commands/logout.ts"() {
468
+ "use strict";
469
+ init_auth();
470
+ }
471
+ });
472
+
473
+ // src/lib/password.ts
474
+ import bcrypt from "bcryptjs";
475
+ async function hashPassword(plain) {
476
+ return bcrypt.hash(plain, BCRYPT_ROUNDS);
477
+ }
478
+ var BCRYPT_ROUNDS;
479
+ var init_password = __esm({
480
+ "src/lib/password.ts"() {
481
+ "use strict";
482
+ BCRYPT_ROUNDS = 10;
483
+ }
484
+ });
485
+
486
+ // src/lib/share-url.ts
487
+ function resolveShareBaseUrl() {
488
+ const raw = process.env.DROPPAGE_SHARE_BASE_URL?.trim();
489
+ return (raw || DEFAULT_SHARE_BASE_URL).replace(/\/+$/, "");
490
+ }
491
+ function buildShareUrl(slug) {
492
+ return `${resolveShareBaseUrl()}/p/${slug}`;
493
+ }
494
+ var DEFAULT_SHARE_BASE_URL;
495
+ var init_share_url = __esm({
496
+ "src/lib/share-url.ts"() {
497
+ "use strict";
498
+ DEFAULT_SHARE_BASE_URL = "https://droppage.dev";
499
+ }
500
+ });
501
+
502
+ // src/lib/slug.ts
503
+ import { customAlphabet } from "nanoid";
504
+ function createSlug() {
505
+ return generate();
506
+ }
507
+ var SLUG_LENGTH, alphabet, generate;
508
+ var init_slug = __esm({
509
+ "src/lib/slug.ts"() {
510
+ "use strict";
511
+ SLUG_LENGTH = 10;
512
+ alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
513
+ generate = customAlphabet(alphabet, SLUG_LENGTH);
514
+ }
515
+ });
516
+
517
+ // src/lib/validate-html.ts
518
+ import { extname } from "path";
519
+ function formatBytes(bytes) {
520
+ const mb = bytes / (1024 * 1024);
521
+ return `${mb.toFixed(1)} MB`;
522
+ }
523
+ function validateHtmlUpload(path, bytes) {
524
+ const ext = extname(path).toLowerCase();
525
+ if (!ALLOWED_EXTENSIONS.has(ext)) {
526
+ throw new Error(
527
+ `Unsupported file type: ${path} (expected .html or .htm)`
528
+ );
529
+ }
530
+ if (bytes.length === 0) {
531
+ throw new Error(`File is empty: ${path}`);
532
+ }
533
+ if (bytes.length > MAX_UPLOAD_BYTES) {
534
+ throw new Error(
535
+ `File too large: ${formatBytes(bytes.length)} (max ${formatBytes(
536
+ MAX_UPLOAD_BYTES
537
+ )})`
538
+ );
539
+ }
540
+ const head = Buffer.from(
541
+ bytes.subarray(0, Math.min(bytes.length, SNIFF_BYTES))
542
+ ).toString("utf8");
543
+ const looksLikeHtml = HTML_MARKERS.some((re) => re.test(head));
544
+ if (!looksLikeHtml) {
545
+ throw new Error(
546
+ `Content does not look like HTML: ${path} (no <!doctype html>, <html>, or <body> found)`
547
+ );
548
+ }
549
+ const warnings = [];
550
+ const body = bytes.length <= SNIFF_BYTES ? head : Buffer.from(bytes).toString("utf8");
551
+ if (RELATIVE_ASSET_RE.test(body)) {
552
+ warnings.push(
553
+ "HTML references relative assets (src/href beginning with ./ or /); they will not resolve in a single-file share. Inline them for a self-contained page."
554
+ );
555
+ }
556
+ if (EXTERNAL_ASSET_RE.test(body)) {
557
+ warnings.push(
558
+ "HTML references external assets (//host or http(s)://); they must be publicly reachable to render in the shared page."
559
+ );
560
+ }
561
+ return { ok: true, warnings };
562
+ }
563
+ var MAX_UPLOAD_BYTES, ALLOWED_EXTENSIONS, SNIFF_BYTES, HTML_MARKERS, RELATIVE_ASSET_RE, EXTERNAL_ASSET_RE;
564
+ var init_validate_html = __esm({
565
+ "src/lib/validate-html.ts"() {
566
+ "use strict";
567
+ MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
568
+ ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".htm"]);
569
+ SNIFF_BYTES = 4096;
570
+ HTML_MARKERS = [
571
+ /<!doctype\s+html/i,
572
+ /<html[\s>]/i,
573
+ /<head[\s>]/i,
574
+ /<body[\s>]/i,
575
+ /<div[\s>]/i
576
+ ];
577
+ RELATIVE_ASSET_RE = /(?:src|href)\s*=\s*["'](?:\.\.?\/|\/(?!\/))/i;
578
+ EXTERNAL_ASSET_RE = /(?:src|href)\s*=\s*["'](\/\/|https?:\/\/)/i;
579
+ }
580
+ });
581
+
582
+ // src/services/upload.ts
583
+ import { readFile } from "fs/promises";
584
+ async function upload({
585
+ file,
586
+ user,
587
+ passwordHash,
588
+ allowedEmails,
589
+ accessMode
590
+ }) {
591
+ const bytes = await readFile(file);
592
+ const { warnings } = validateHtmlUpload(file, bytes);
593
+ for (const w of warnings) console.error(`droppage: warning: ${w}`);
594
+ const supabase = createSupabaseClient();
595
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
596
+ const slug = createSlug();
597
+ const storagePath = `${user.id}/${slug}.html`;
598
+ const up = await supabase.storage.from(BUCKET).upload(storagePath, bytes, {
599
+ contentType: "text/html; charset=utf-8",
600
+ upsert: false
601
+ });
602
+ if (up.error) throw up.error;
603
+ const resolvedMode = accessMode ?? (allowedEmails ? "email" : passwordHash ? "password" : "public");
604
+ const ins = await supabase.rpc("create_page_with_entitlements", {
605
+ p_slug: slug,
606
+ p_storage_path: storagePath,
607
+ p_access_mode: resolvedMode,
608
+ p_password_hash: resolvedMode === "password" ? passwordHash : null,
609
+ p_allowed_emails: allowedEmails ?? null
610
+ });
611
+ if (!ins.error) return buildShareUrl(slug);
612
+ const rm = await supabase.storage.from(BUCKET).remove([storagePath]);
613
+ if (rm.error) {
614
+ console.error(
615
+ `droppage: warning: could not remove orphan ${storagePath}: ${rm.error.message}`
616
+ );
617
+ }
618
+ if (ins.error.code === "23505") continue;
619
+ throw ins.error;
620
+ }
621
+ throw new Error(
622
+ `Could not allocate a unique slug after ${MAX_RETRIES} attempts`
623
+ );
624
+ }
625
+ var BUCKET, MAX_RETRIES;
626
+ var init_upload = __esm({
627
+ "src/services/upload.ts"() {
628
+ "use strict";
629
+ init_share_url();
630
+ init_slug();
631
+ init_supabase();
632
+ init_validate_html();
633
+ BUCKET = "pages";
634
+ MAX_RETRIES = 5;
635
+ }
636
+ });
637
+
638
+ // src/commands/upload.ts
639
+ import { password as passwordPrompt } from "@inquirer/prompts";
640
+ function registerUpload(program2) {
641
+ program2.command("upload").description("Upload an HTML file and get a shareable URL").argument("<file>", "path to .html file").option("--password", "protect the page with a password (prompts securely)").option(
642
+ "--email <address>",
643
+ "restrict access to an email allowlist (repeatable; comma-separated)",
644
+ (val, prev = []) => prev.concat(val.split(","))
645
+ ).option("--private", "restrict access to only you (owner-only draft)").action(
646
+ async (file, options) => {
647
+ let user;
648
+ try {
649
+ user = await requireAuth();
650
+ } catch (err) {
651
+ if (err instanceof AuthRequiredError) return;
652
+ throw err;
653
+ }
654
+ const modeFlags = [
655
+ options.password,
656
+ options.email?.length,
657
+ options.private
658
+ ].filter(Boolean).length;
659
+ if (modeFlags > 1) {
660
+ process.exitCode = 1;
661
+ console.error(
662
+ "droppage: --password, --email, and --private are mutually exclusive \u2014 a page is exactly one access mode."
663
+ );
664
+ return;
665
+ }
666
+ try {
667
+ let passwordHash;
668
+ let allowedEmails;
669
+ let accessMode = "public";
670
+ if (options.password) {
671
+ const pw = await passwordPrompt({ message: "Password:", mask: "*" });
672
+ const confirm = await passwordPrompt({
673
+ message: "Confirm password:",
674
+ mask: "*"
675
+ });
676
+ if (pw !== confirm) throw new Error("Passwords do not match");
677
+ passwordHash = await hashPassword(pw);
678
+ accessMode = "password";
679
+ } else if (options.email?.length) {
680
+ const cleaned = [
681
+ ...new Set(
682
+ options.email.map((e) => e.trim().toLowerCase()).filter(Boolean)
683
+ )
684
+ ];
685
+ if (!cleaned.length) {
686
+ throw new Error(
687
+ "--email was given but contained no email addresses"
688
+ );
689
+ }
690
+ const invalid = cleaned.filter(
691
+ (e) => !emailSchema.safeParse(e).success
692
+ );
693
+ if (invalid.length) {
694
+ throw new Error(
695
+ `invalid email address(es): ${invalid.join(", ")}`
696
+ );
697
+ }
698
+ allowedEmails = cleaned;
699
+ accessMode = "email";
700
+ } else if (options.private) {
701
+ const ownerEmail = user.email?.trim().toLowerCase();
702
+ if (!ownerEmail) {
703
+ throw new Error(
704
+ "--private requires an email on your account, but none was found"
705
+ );
706
+ }
707
+ allowedEmails = [ownerEmail];
708
+ accessMode = "private";
709
+ }
710
+ console.error(`droppage: uploading ${file}...`);
711
+ const url = await upload({
712
+ file,
713
+ user,
714
+ passwordHash,
715
+ allowedEmails,
716
+ accessMode
717
+ });
718
+ console.error("droppage: done");
719
+ process.stdout.write(url + "\n");
720
+ } catch (err) {
721
+ process.exitCode = 1;
722
+ console.error(
723
+ `droppage: upload failed: ${err instanceof Error ? err.message : String(err)}`
724
+ );
725
+ }
726
+ }
727
+ );
728
+ }
729
+ var init_upload2 = __esm({
730
+ "src/commands/upload.ts"() {
731
+ "use strict";
732
+ init_password();
733
+ init_auth();
734
+ init_upload();
735
+ }
736
+ });
737
+
738
+ // src/services/pages.ts
739
+ import { readFile as readFile2 } from "fs/promises";
740
+ async function listPages(user) {
741
+ const supabase = createSupabaseClient();
742
+ const { data, error } = await supabase.from("pages").select(
743
+ "slug, access_mode, allowed_emails, view_count, created_at, last_viewed_at"
744
+ ).eq("owner_id", user.id).order("created_at", { ascending: false });
745
+ if (error) throw error;
746
+ return data ?? [];
747
+ }
748
+ async function deletePage(slug, user) {
749
+ const supabase = createSupabaseClient();
750
+ const { data: row, error: selectError } = await supabase.from("pages").select("slug, storage_path").eq("slug", slug).eq("owner_id", user.id).maybeSingle();
751
+ if (selectError) throw selectError;
752
+ if (!row) return { ok: false, partial: false, message: NOT_FOUND_MESSAGE };
753
+ const storagePath = row.storage_path;
754
+ const removed = await supabase.storage.from(BUCKET2).remove([storagePath]);
755
+ if (removed.error) {
756
+ return {
757
+ ok: false,
758
+ partial: false,
759
+ message: `could not remove ${slug}: ${removed.error.message}. Nothing was deleted and the link still works.`
760
+ };
761
+ }
762
+ const blobWasAbsent = (removed.data ?? []).length === 0;
763
+ const deleted = await supabase.from("pages").delete().eq("slug", slug).eq("owner_id", user.id).select("slug");
764
+ const deletedCount = deleted.data?.length ?? 0;
765
+ if (deleted.error || deletedCount === 0) {
766
+ if (blobWasAbsent && deletedCount === 0 && !deleted.error) {
767
+ return { ok: false, partial: false, message: NOT_FOUND_MESSAGE };
768
+ }
769
+ return {
770
+ ok: false,
771
+ partial: true,
772
+ message: `stored HTML for ${slug} was removed, so the link is dead, but the metadata row could not be deleted (${deleted.error?.message ?? "zero rows affected"}). Re-run droppage delete ${slug}.`
773
+ };
774
+ }
775
+ if (blobWasAbsent) {
776
+ console.error(
777
+ `droppage: warning: no stored HTML was found at ${storagePath}; deleted metadata row for ${slug}.`
778
+ );
779
+ }
780
+ return { ok: true, blobWasAbsent };
781
+ }
782
+ async function updatePageHtml({
783
+ slug,
784
+ file,
785
+ user
786
+ }) {
787
+ const bytes = await readFile2(file);
788
+ const { warnings } = validateHtmlUpload(file, bytes);
789
+ for (const warning of warnings) {
790
+ console.error(`droppage: warning: ${warning}`);
791
+ }
792
+ const supabase = createSupabaseClient();
793
+ const preflight = await supabase.rpc("assert_page_update_allowed", {
794
+ p_slug: slug
795
+ });
796
+ if (preflight.error) throw preflight.error;
797
+ const { data: row, error: selectError } = await supabase.from("pages").select("slug, storage_path").eq("slug", slug).eq("owner_id", user.id).maybeSingle();
798
+ if (selectError) throw selectError;
799
+ if (!row) throw new Error("page not found or not owned");
800
+ const storagePath = row.storage_path;
801
+ if (!storagePath.startsWith(`${user.id}/`)) {
802
+ throw new Error("page storage path is not owner-scoped");
803
+ }
804
+ const replaced = await supabase.storage.from(BUCKET2).update(storagePath, bytes, {
805
+ contentType: "text/html; charset=utf-8"
806
+ });
807
+ if (replaced.error) {
808
+ throw new Error(
809
+ `could not replace stored HTML: ${replaced.error.message}. The existing link may still serve the previous content.`
810
+ );
811
+ }
812
+ return {
813
+ slug,
814
+ storagePath,
815
+ url: buildShareUrl(slug)
816
+ };
817
+ }
818
+ var BUCKET2, NOT_FOUND_MESSAGE;
819
+ var init_pages = __esm({
820
+ "src/services/pages.ts"() {
821
+ "use strict";
822
+ init_share_url();
823
+ init_supabase();
824
+ init_validate_html();
825
+ BUCKET2 = "pages";
826
+ NOT_FOUND_MESSAGE = "page not found or already deleted";
827
+ }
828
+ });
829
+
830
+ // src/commands/update.ts
831
+ function registerUpdate(program2) {
832
+ program2.command("update").description("Replace the HTML behind an existing page URL").argument("<slug>", "slug of the page to update").argument("<file>", "path to replacement .html file").action(async (slug, file) => {
833
+ let user;
834
+ try {
835
+ user = await requireAuth();
836
+ } catch (err) {
837
+ if (err instanceof AuthRequiredError) return;
838
+ throw err;
839
+ }
840
+ try {
841
+ console.error(`droppage: updating ${slug} from ${file}...`);
842
+ const result = await updatePageHtml({ slug, file, user });
843
+ console.error(`droppage: updated ${slug}`);
844
+ process.stdout.write(result.url + "\n");
845
+ } catch (err) {
846
+ process.exitCode = 1;
847
+ console.error(
848
+ `droppage: update failed: ${err instanceof Error ? err.message : String(err)}`
849
+ );
850
+ }
851
+ });
852
+ }
853
+ var init_update = __esm({
854
+ "src/commands/update.ts"() {
855
+ "use strict";
856
+ init_auth();
857
+ init_pages();
858
+ }
859
+ });
860
+
861
+ // src/lib/table.ts
862
+ function renderTable(headers, rows, align = headers.map(() => "left")) {
863
+ const widths = headers.map(
864
+ (header, i) => Math.max(header.length, ...rows.map((row) => row[i]?.length ?? 0))
865
+ );
866
+ const formatRow = (row) => row.map(
867
+ (cell, i) => align[i] === "right" ? cell.padStart(widths[i]) : cell.padEnd(widths[i])
868
+ ).join(" ").trimEnd();
869
+ return [formatRow(headers), ...rows.map(formatRow)].join("\n");
870
+ }
871
+ function formatModeCell(accessMode, allowedEmails) {
872
+ if (accessMode === "email" || accessMode === "private") {
873
+ return `${accessMode}(${allowedEmails?.length ?? 0})`;
874
+ }
875
+ return accessMode;
876
+ }
877
+ function formatDateCell(iso) {
878
+ return iso === null ? "\u2014" : iso.slice(0, 10);
879
+ }
880
+ function pageTableCells(row) {
881
+ return [
882
+ row.slug,
883
+ formatModeCell(row.access_mode, row.allowed_emails),
884
+ formatDateCell(row.created_at),
885
+ String(row.view_count),
886
+ formatDateCell(row.last_viewed_at)
887
+ ];
888
+ }
889
+ var PAGE_TABLE_HEADERS, PAGE_TABLE_ALIGN;
890
+ var init_table = __esm({
891
+ "src/lib/table.ts"() {
892
+ "use strict";
893
+ PAGE_TABLE_HEADERS = [
894
+ "SLUG",
895
+ "MODE",
896
+ "CREATED",
897
+ "VIEWS",
898
+ "LAST VIEWED"
899
+ ];
900
+ PAGE_TABLE_ALIGN = [
901
+ "left",
902
+ "left",
903
+ "left",
904
+ "right",
905
+ "left"
906
+ ];
907
+ }
908
+ });
909
+
910
+ // src/commands/list.ts
911
+ async function fetchPreviewUrls(pages) {
912
+ if (pages.length === 0) return /* @__PURE__ */ new Map();
913
+ const supabase = createSupabaseClient();
914
+ const { data } = await supabase.auth.getSession();
915
+ const token = data.session?.access_token;
916
+ if (!token) return null;
917
+ const response = await fetch(`${resolveShareBaseUrl()}/api/preview`, {
918
+ method: "POST",
919
+ headers: {
920
+ Authorization: `Bearer ${token}`,
921
+ "Content-Type": "application/json"
922
+ },
923
+ body: JSON.stringify({ slugs: pages.map((page) => page.slug) })
924
+ });
925
+ if (!response.ok) return null;
926
+ const body = await response.json();
927
+ return new Map(
928
+ (body.previews ?? []).filter(
929
+ (preview) => typeof preview.slug === "string" && typeof preview.url === "string"
930
+ ).map((preview) => [preview.slug, preview.url])
931
+ );
932
+ }
933
+ function registerList(program2) {
934
+ program2.command("list").description(
935
+ "List your published pages. VIEWS is a raw hit count, including refreshes and crawlers, not unique visitors."
936
+ ).option("--json", "print a machine-readable JSON array").option("--preview", "include temporary preview URLs that do not increment views").action(async (options) => {
937
+ let user;
938
+ try {
939
+ user = await requireAuth();
940
+ } catch (err) {
941
+ if (err instanceof AuthRequiredError) return;
942
+ throw err;
943
+ }
944
+ try {
945
+ const pages = await listPages(user);
946
+ let previews = null;
947
+ if (options.preview) {
948
+ try {
949
+ previews = await fetchPreviewUrls(pages);
950
+ } catch {
951
+ previews = null;
952
+ }
953
+ if (!previews) {
954
+ console.error(
955
+ "droppage: warning: could not fetch preview URLs; showing pages without previews."
956
+ );
957
+ }
958
+ }
959
+ if (pages.length === 0) {
960
+ console.error(
961
+ "droppage: no pages yet. Publish one with `droppage upload <file.html>`."
962
+ );
963
+ }
964
+ if (options.json) {
965
+ process.stdout.write(
966
+ JSON.stringify(
967
+ pages.map((page) => ({
968
+ slug: page.slug,
969
+ url: buildShareUrl(page.slug),
970
+ access_mode: page.access_mode,
971
+ allowed_emails: page.allowed_emails ?? [],
972
+ view_count: page.view_count,
973
+ created_at: page.created_at,
974
+ last_viewed_at: page.last_viewed_at,
975
+ ...options.preview ? { preview_url: previews?.get(page.slug) ?? null } : {}
976
+ })),
977
+ null,
978
+ 2
979
+ ) + "\n"
980
+ );
981
+ return;
982
+ }
983
+ if (pages.length > 0) {
984
+ const headers = options.preview ? [...PAGE_TABLE_HEADERS, "PREVIEW"] : PAGE_TABLE_HEADERS;
985
+ const align = options.preview ? [...PAGE_TABLE_ALIGN, "left"] : PAGE_TABLE_ALIGN;
986
+ process.stdout.write(
987
+ renderTable(
988
+ headers,
989
+ pages.map(
990
+ (page) => options.preview ? [...pageTableCells(page), previews?.get(page.slug) ?? "\u2014"] : pageTableCells(page)
991
+ ),
992
+ align
993
+ ) + "\n"
994
+ );
995
+ }
996
+ } catch (err) {
997
+ console.error(
998
+ `droppage: list failed: ${err instanceof Error ? err.message : String(err)}`
999
+ );
1000
+ process.exitCode = 1;
1001
+ }
1002
+ });
1003
+ }
1004
+ var init_list = __esm({
1005
+ "src/commands/list.ts"() {
1006
+ "use strict";
1007
+ init_share_url();
1008
+ init_table();
1009
+ init_supabase();
1010
+ init_auth();
1011
+ init_pages();
1012
+ }
1013
+ });
1014
+
1015
+ // src/commands/delete.ts
1016
+ import { confirm as confirmPrompt } from "@inquirer/prompts";
1017
+ function registerDelete(program2) {
1018
+ program2.command("delete").description("Delete a published page").argument("<slug>", "slug of the page to delete").option("-y, --yes", "skip the confirmation prompt").action(async (slug, options) => {
1019
+ let user;
1020
+ try {
1021
+ user = await requireAuth();
1022
+ } catch (err) {
1023
+ if (err instanceof AuthRequiredError) return;
1024
+ throw err;
1025
+ }
1026
+ if (!options.yes) {
1027
+ const ok = await confirmPrompt({
1028
+ message: `Delete ${slug} (${buildShareUrl(slug)})? This cannot be undone.`,
1029
+ default: false
1030
+ });
1031
+ if (!ok) {
1032
+ console.error("droppage: delete aborted.");
1033
+ return;
1034
+ }
1035
+ }
1036
+ try {
1037
+ const result = await deletePage(slug, user);
1038
+ if (result.ok) {
1039
+ console.error(`droppage: deleted ${slug}; the link no longer works.`);
1040
+ return;
1041
+ }
1042
+ console.error(`droppage: ${result.message}`);
1043
+ process.exitCode = 1;
1044
+ } catch (err) {
1045
+ console.error(
1046
+ `droppage: delete failed: ${err instanceof Error ? err.message : String(err)}`
1047
+ );
1048
+ process.exitCode = 1;
1049
+ }
1050
+ });
1051
+ }
1052
+ var init_delete = __esm({
1053
+ "src/commands/delete.ts"() {
1054
+ "use strict";
1055
+ init_share_url();
1056
+ init_auth();
1057
+ init_pages();
1058
+ }
1059
+ });
1060
+
1061
+ // src/services/recipients.ts
1062
+ async function authedFetch(path, init) {
1063
+ const supabase = createSupabaseClient();
1064
+ const { data } = await supabase.auth.getSession();
1065
+ const token = data.session?.access_token;
1066
+ if (!token) throw new RecipientRequestError("not signed in; run `droppage login`", 401);
1067
+ let response;
1068
+ try {
1069
+ response = await fetch(`${resolveShareBaseUrl()}${path}`, {
1070
+ ...init,
1071
+ headers: {
1072
+ Authorization: `Bearer ${token}`,
1073
+ "Content-Type": "application/json",
1074
+ ...init.headers ?? {}
1075
+ }
1076
+ });
1077
+ } catch (err) {
1078
+ throw new RecipientRequestError(
1079
+ `could not reach ${resolveShareBaseUrl()}: ${err instanceof Error ? err.message : String(err)}`,
1080
+ 0
1081
+ );
1082
+ }
1083
+ const text = await response.text();
1084
+ let body = null;
1085
+ try {
1086
+ body = text ? JSON.parse(text) : null;
1087
+ } catch {
1088
+ }
1089
+ if (!response.ok) {
1090
+ const message = body?.error ?? `request failed (HTTP ${response.status})`;
1091
+ throw new RecipientRequestError(message, response.status);
1092
+ }
1093
+ return body;
1094
+ }
1095
+ function recipientsPath(slug) {
1096
+ return `/api/pages/${encodeURIComponent(slug)}/recipients`;
1097
+ }
1098
+ async function listRecipients(slug, _user, page = 1) {
1099
+ return await authedFetch(`${recipientsPath(slug)}?page=${page}`, {
1100
+ method: "GET"
1101
+ });
1102
+ }
1103
+ async function addRecipients(slug, emails, _user) {
1104
+ return await authedFetch(recipientsPath(slug), {
1105
+ method: "POST",
1106
+ body: JSON.stringify({ emails })
1107
+ });
1108
+ }
1109
+ async function removeRecipient(slug, email, _user) {
1110
+ await authedFetch(`${recipientsPath(slug)}?email=${encodeURIComponent(email)}`, {
1111
+ method: "DELETE"
1112
+ });
1113
+ }
1114
+ async function resendRecipient(slug, email, _user) {
1115
+ await authedFetch(recipientsPath(slug), {
1116
+ method: "POST",
1117
+ body: JSON.stringify({ action: "resend", email })
1118
+ });
1119
+ }
1120
+ var RecipientRequestError;
1121
+ var init_recipients = __esm({
1122
+ "src/services/recipients.ts"() {
1123
+ "use strict";
1124
+ init_share_url();
1125
+ init_supabase();
1126
+ RecipientRequestError = class extends Error {
1127
+ constructor(message, status) {
1128
+ super(message);
1129
+ this.status = status;
1130
+ this.name = "RecipientRequestError";
1131
+ }
1132
+ };
1133
+ }
1134
+ });
1135
+
1136
+ // src/commands/recipients.ts
1137
+ import { confirm as confirmPrompt2 } from "@inquirer/prompts";
1138
+ import { z as z3 } from "zod";
1139
+ function formatDateCell2(iso) {
1140
+ return iso === null ? "\u2014" : iso.slice(0, 10);
1141
+ }
1142
+ function recipientCells(row) {
1143
+ return [row.email, row.status, row.invitation_status, formatDateCell2(row.last_sent_at)];
1144
+ }
1145
+ function cleanEmails(values) {
1146
+ const cleaned = [...new Set(values.map((e) => e.trim().toLowerCase()).filter(Boolean))];
1147
+ if (!cleaned.length) throw new Error("no email addresses given");
1148
+ const invalid = cleaned.filter((e) => !emailSchema2.safeParse(e).success);
1149
+ if (invalid.length) throw new Error(`invalid email address(es): ${invalid.join(", ")}`);
1150
+ return cleaned;
1151
+ }
1152
+ async function run(action) {
1153
+ let user;
1154
+ try {
1155
+ user = await requireAuth();
1156
+ } catch (err) {
1157
+ if (err instanceof AuthRequiredError) return;
1158
+ throw err;
1159
+ }
1160
+ try {
1161
+ await action(user);
1162
+ } catch (err) {
1163
+ const message = err instanceof RecipientRequestError || err instanceof Error ? err.message : String(err);
1164
+ console.error(`droppage: ${message}`);
1165
+ process.exitCode = 1;
1166
+ }
1167
+ }
1168
+ function registerRecipients(program2) {
1169
+ const recipients = program2.command("recipients").description("Manage who can open an email-gated page");
1170
+ recipients.command("list").description("List recipients on a page").argument("<slug>", "slug of the page").option("--json", "print a machine-readable JSON object").option("--page <n>", "page number for paginated results", "1").action(async (slug, options) => {
1171
+ await run(async (user) => {
1172
+ const page = Number(options.page);
1173
+ const result = await listRecipients(
1174
+ slug,
1175
+ user,
1176
+ Number.isSafeInteger(page) && page > 0 ? page : 1
1177
+ );
1178
+ if (options.json) {
1179
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1180
+ return;
1181
+ }
1182
+ if (result.recipients.length === 0) {
1183
+ console.error(`droppage: no recipients on ${slug} yet.`);
1184
+ return;
1185
+ }
1186
+ process.stdout.write(
1187
+ renderTable(
1188
+ RECIPIENT_HEADERS,
1189
+ result.recipients.map(recipientCells),
1190
+ RECIPIENT_ALIGN
1191
+ ) + "\n"
1192
+ );
1193
+ const shown = result.recipients.length;
1194
+ console.error(
1195
+ `droppage: ${shown} of ${result.total} recipient(s), page ${result.page}.`
1196
+ );
1197
+ });
1198
+ });
1199
+ recipients.command("add").description("Add recipients and queue their invitations").argument("<slug>", "slug of the page").argument("<emails...>", "one or more email addresses").action(async (slug, emails) => {
1200
+ await run(async (user) => {
1201
+ const cleaned = cleanEmails(emails);
1202
+ const result = await addRecipients(slug, cleaned, user);
1203
+ console.error(
1204
+ `droppage: ${result.queued} invitation(s) queued, ${result.unchanged} already had access.`
1205
+ );
1206
+ });
1207
+ });
1208
+ recipients.command("remove").description("Revoke a recipient; their invitation link stops working").argument("<slug>", "slug of the page").argument("<email>", "email address to revoke").option("-y, --yes", "skip the confirmation prompt").action(async (slug, email, options) => {
1209
+ await run(async (user) => {
1210
+ const [cleaned] = cleanEmails([email]);
1211
+ if (!options.yes) {
1212
+ const ok = await confirmPrompt2({
1213
+ message: `Revoke ${cleaned} from ${slug}? Their link will stop working.`,
1214
+ default: false
1215
+ });
1216
+ if (!ok) {
1217
+ console.error("droppage: remove aborted.");
1218
+ return;
1219
+ }
1220
+ }
1221
+ await removeRecipient(slug, cleaned, user);
1222
+ console.error(`droppage: revoked ${cleaned}; their link no longer works.`);
1223
+ });
1224
+ });
1225
+ recipients.command("resend").description("Queue a fresh invitation for one existing recipient").argument("<slug>", "slug of the page").argument("<email>", "email address to resend to").action(async (slug, email) => {
1226
+ await run(async (user) => {
1227
+ const [cleaned] = cleanEmails([email]);
1228
+ await resendRecipient(slug, cleaned, user);
1229
+ console.error(`droppage: invitation for ${cleaned} queued.`);
1230
+ });
1231
+ });
1232
+ }
1233
+ var emailSchema2, RECIPIENT_HEADERS, RECIPIENT_ALIGN;
1234
+ var init_recipients2 = __esm({
1235
+ "src/commands/recipients.ts"() {
1236
+ "use strict";
1237
+ init_table();
1238
+ init_auth();
1239
+ init_recipients();
1240
+ emailSchema2 = z3.email();
1241
+ RECIPIENT_HEADERS = ["EMAIL", "STATUS", "INVITATION", "LAST SENT"];
1242
+ RECIPIENT_ALIGN = ["left", "left", "left", "left"];
1243
+ }
1244
+ });
1245
+
1246
+ // src/index.ts
1247
+ var index_exports = {};
1248
+ __export(index_exports, {
1249
+ createProgram: () => createProgram
1250
+ });
1251
+ import { Command } from "commander";
1252
+ function createProgram() {
1253
+ const program2 = new Command().name("droppage").description(
1254
+ "Publish HTML files as shareable, access-controlled live pages"
1255
+ ).version(package_default.version);
1256
+ registerLogin(program2);
1257
+ registerLogout(program2);
1258
+ registerUpload(program2);
1259
+ registerUpdate(program2);
1260
+ registerList(program2);
1261
+ registerDelete(program2);
1262
+ registerRecipients(program2);
1263
+ return program2;
1264
+ }
1265
+ var init_index = __esm({
1266
+ "src/index.ts"() {
1267
+ "use strict";
1268
+ init_package();
1269
+ init_login();
1270
+ init_logout();
1271
+ init_upload2();
1272
+ init_update();
1273
+ init_list();
1274
+ init_delete();
1275
+ init_recipients2();
1276
+ }
1277
+ });
1278
+
1279
+ // src/cli.ts
1280
+ var MIN_NODE_MAJOR = 22;
1281
+ var nodeMajor = Number(process.versions.node.split(".")[0]);
1282
+ if (nodeMajor < MIN_NODE_MAJOR) {
1283
+ process.stderr.write(
1284
+ `droppage requires Node.js ${MIN_NODE_MAJOR} or newer (found ${process.versions.node}).
1285
+ Upgrade Node.js: https://nodejs.org/
1286
+ `
1287
+ );
1288
+ process.exit(1);
1289
+ }
1290
+ var { createProgram: createProgram2 } = await Promise.resolve().then(() => (init_index(), index_exports));
1291
+ var program = createProgram2();
1292
+ await program.parseAsync(process.argv);