papermark 0.9.0 → 0.10.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/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { readFileSync as readFileSync3 } from "fs";
4
5
  import { Command as Command2 } from "commander";
5
6
 
6
7
  // src/commands/analytics.ts
@@ -790,6 +791,28 @@ async function tryOpenBrowser(url) {
790
791
  } catch {
791
792
  }
792
793
  }
794
+ async function revokeToken(token) {
795
+ try {
796
+ const issuer = deriveIssuer();
797
+ const as = await discover(issuer);
798
+ if (!as.revocation_endpoint) return "unsupported";
799
+ const res = await fetch(as.revocation_endpoint, {
800
+ method: "POST",
801
+ headers: {
802
+ "Content-Type": "application/x-www-form-urlencoded",
803
+ Accept: "application/json"
804
+ },
805
+ body: new URLSearchParams({
806
+ token,
807
+ token_type_hint: "access_token",
808
+ client_id: CLIENT_ID
809
+ })
810
+ });
811
+ return res.ok ? "revoked" : "failed";
812
+ } catch {
813
+ return "failed";
814
+ }
815
+ }
793
816
 
794
817
  // src/commands/auth.ts
795
818
  function registerAuthCommands(program2) {
@@ -814,9 +837,24 @@ function registerAuthCommands(program2) {
814
837
  }
815
838
  }
816
839
  );
817
- program2.command("logout").description("Remove the stored API token").action(() => {
840
+ program2.command("logout").description("Remove the stored API token").action(async () => {
841
+ const token = getToken();
842
+ const source = getTokenSource();
843
+ const revocation = token && source === "config" ? await revokeToken(token) : "skipped";
818
844
  clearToken();
819
- emit({ removed: true }, () => chalk3.green("\u2713") + " Token removed.");
845
+ if (revocation === "skipped" && (source === "env" || source === "credentials-file")) {
846
+ process.stderr.write(
847
+ chalk3.dim(`Note: token comes from ${source}; not revoked server-side.`) + "\n"
848
+ );
849
+ } else if (revocation === "failed") {
850
+ process.stderr.write(
851
+ chalk3.yellow("! ") + "Could not revoke the token server-side; it remains valid until expiry.\n"
852
+ );
853
+ }
854
+ emit(
855
+ { removed: true, revocation },
856
+ () => revocation === "revoked" ? chalk3.green("\u2713") + " Token revoked and removed." : chalk3.green("\u2713") + " Token removed."
857
+ );
820
858
  });
821
859
  program2.command("whoami").description("Show the active token and API URL").action(() => {
822
860
  const token = getToken();
@@ -1203,6 +1241,18 @@ function parsePermissionEntries(opts) {
1203
1241
  );
1204
1242
  }
1205
1243
 
1244
+ // src/validate.ts
1245
+ function parseLimit(value) {
1246
+ if (value === void 0) return void 0;
1247
+ const n = Number(value);
1248
+ if (!Number.isInteger(n) || n < 1 || n > 100) {
1249
+ throw new ValidationError(
1250
+ `Invalid --limit ${JSON.stringify(value)}. Expected an integer 1-100.`
1251
+ );
1252
+ }
1253
+ return n;
1254
+ }
1255
+
1206
1256
  // src/commands/datarooms.ts
1207
1257
  function registerDataroomsCommands(program2) {
1208
1258
  const dr = program2.command("datarooms").alias("dr").description("Manage datarooms");
@@ -1211,7 +1261,7 @@ function registerDataroomsCommands(program2) {
1211
1261
  if (opts.json) setRuntime({ json: true });
1212
1262
  try {
1213
1263
  const res = await api.listDatarooms({
1214
- limit: opts.limit ? Number(opts.limit) : void 0,
1264
+ limit: parseLimit(opts.limit),
1215
1265
  cursor: opts.cursor,
1216
1266
  query: opts.query
1217
1267
  });
@@ -1280,14 +1330,14 @@ More results: --cursor ${res.next_cursor}`) : out;
1280
1330
  const norm = v.toLowerCase();
1281
1331
  if (norm === "on") return true;
1282
1332
  if (norm === "off") return false;
1283
- throw new Error(
1333
+ throw new ValidationError(
1284
1334
  `Invalid value for ${flag}: ${JSON.stringify(v)} (expected "on" or "off").`
1285
1335
  );
1286
1336
  };
1287
1337
  const bulk = tri(opts.bulkDownload, "--bulk-download");
1288
1338
  if (bulk !== void 0) body.allow_bulk_download = bulk;
1289
1339
  if (Object.keys(body).length === 0) {
1290
- throw new Error(
1340
+ throw new ValidationError(
1291
1341
  "Pass at least one field to update (e.g. --name, --description, --bulk-download off)."
1292
1342
  );
1293
1343
  }
@@ -1310,7 +1360,7 @@ More results: --cursor ${res.next_cursor}`) : out;
1310
1360
  try {
1311
1361
  const res = await api.listLinks({
1312
1362
  dataroom_id: id,
1313
- limit: opts.limit ? Number(opts.limit) : void 0,
1363
+ limit: parseLimit(opts.limit),
1314
1364
  cursor: opts.cursor
1315
1365
  });
1316
1366
  emit(res, () => {
@@ -1339,7 +1389,7 @@ More results: --cursor ${res.next_cursor}`) : out;
1339
1389
  try {
1340
1390
  const res = await api.listVisitors({
1341
1391
  dataroom_id: id,
1342
- limit: opts.limit ? Number(opts.limit) : void 0,
1392
+ limit: parseLimit(opts.limit),
1343
1393
  cursor: opts.cursor,
1344
1394
  email: opts.email
1345
1395
  });
@@ -1454,12 +1504,12 @@ More results: --cursor ${res.next_cursor}`) : out;
1454
1504
  try {
1455
1505
  const folderIsRoot = opts.folderId === "" || opts.folderId === "root";
1456
1506
  if (opts.root && opts.folderId !== void 0 && !folderIsRoot) {
1457
- throw new Error(
1507
+ throw new ValidationError(
1458
1508
  "Pass either --root or --folder-id <fid>, not both."
1459
1509
  );
1460
1510
  }
1461
1511
  if (!opts.root && opts.folderId === void 0) {
1462
- throw new Error(
1512
+ throw new ValidationError(
1463
1513
  'Pass --folder-id <fid>, or --root (or --folder-id "") to move to the dataroom root.'
1464
1514
  );
1465
1515
  }
@@ -1727,7 +1777,7 @@ More results: --cursor ${res.next_cursor}`) : out;
1727
1777
  if (opts.json) setRuntime({ json: true });
1728
1778
  try {
1729
1779
  const res = await api.listDataroomFolders(id, {
1730
- limit: opts.limit ? Number(opts.limit) : void 0,
1780
+ limit: parseLimit(opts.limit),
1731
1781
  cursor: opts.cursor,
1732
1782
  parent_id: opts.parent
1733
1783
  });
@@ -1786,7 +1836,7 @@ More results: --cursor ${res.next_cursor}`) : out;
1786
1836
  if (opts.color !== void 0)
1787
1837
  body.color = opts.color === "" ? null : opts.color;
1788
1838
  if (Object.keys(body).length === 0) {
1789
- throw new Error(
1839
+ throw new ValidationError(
1790
1840
  "Pass at least one of --name, --icon, --color to update."
1791
1841
  );
1792
1842
  }
@@ -1827,7 +1877,7 @@ More results: --cursor ${res.next_cursor}`) : out;
1827
1877
  async (id, fid, opts) => {
1828
1878
  try {
1829
1879
  if (opts.parent === void 0) {
1830
- throw new Error(
1880
+ throw new ValidationError(
1831
1881
  "Pass --parent <fid> (use empty string to move to the root)."
1832
1882
  );
1833
1883
  }
@@ -1850,7 +1900,7 @@ More results: --cursor ${res.next_cursor}`) : out;
1850
1900
  if (opts.json) setRuntime({ json: true });
1851
1901
  try {
1852
1902
  const res = await api.listDataroomDocuments(id, {
1853
- limit: opts.limit ? Number(opts.limit) : void 0,
1903
+ limit: parseLimit(opts.limit),
1854
1904
  cursor: opts.cursor,
1855
1905
  folder_id: opts.folder
1856
1906
  });
@@ -1986,7 +2036,7 @@ function registerDocumentsCommands(program2) {
1986
2036
  if (opts.json) setRuntime({ json: true });
1987
2037
  try {
1988
2038
  const res = await api.listDocuments({
1989
- limit: opts.limit ? Number(opts.limit) : void 0,
2039
+ limit: parseLimit(opts.limit),
1990
2040
  cursor: opts.cursor
1991
2041
  });
1992
2042
  emit(res, () => {
@@ -2019,7 +2069,7 @@ More results: --cursor ${res.next_cursor}`) : out;
2019
2069
  try {
2020
2070
  const res = await api.searchDocuments(
2021
2071
  query,
2022
- opts.limit ? Number(opts.limit) : void 0
2072
+ parseLimit(opts.limit)
2023
2073
  );
2024
2074
  emit(res, () => {
2025
2075
  const rows = res.data.map((d) => ({
@@ -2043,7 +2093,7 @@ More results: --cursor ${res.next_cursor}`) : out;
2043
2093
  body.folder_id = opts.folderId === "" ? null : opts.folderId;
2044
2094
  }
2045
2095
  if (Object.keys(body).length === 0) {
2046
- throw new Error(
2096
+ throw new ValidationError(
2047
2097
  "Pass at least one of --name or --folder-id to update."
2048
2098
  );
2049
2099
  }
@@ -2073,8 +2123,13 @@ More results: --cursor ${res.next_cursor}`) : out;
2073
2123
  async (filePath, opts) => {
2074
2124
  const spinner = ora2();
2075
2125
  try {
2076
- const stats = await stat(filePath);
2077
- if (!stats.isFile()) throw new Error(`${filePath} is not a file.`);
2126
+ const stats = await stat(filePath).catch((statErr) => {
2127
+ throw new ValidationError(
2128
+ `Cannot read ${filePath}: ${statErr.message}`,
2129
+ { code: statErr.code }
2130
+ );
2131
+ });
2132
+ if (!stats.isFile()) throw new ValidationError(`${filePath} is not a file.`);
2078
2133
  const fileName = opts.name ?? basename(filePath);
2079
2134
  const contentType = lookupMimeType(filePath) || "application/octet-stream";
2080
2135
  spinner.start("Requesting upload URL");
@@ -2083,6 +2138,15 @@ More results: --cursor ${res.next_cursor}`) : out;
2083
2138
  content_type: contentType,
2084
2139
  content_length: stats.size
2085
2140
  });
2141
+ if (getRuntime().dryRun) {
2142
+ spinner.stop();
2143
+ process.stderr.write(
2144
+ `[dry-run] PUT <presigned upload_url from the response above> (file: ${filePath}, ${humanSize(stats.size)})
2145
+ [dry-run] POST ${"/v1/documents"} (name, upload_id, folder_id, create_link)
2146
+ `
2147
+ );
2148
+ return;
2149
+ }
2086
2150
  spinner.succeed("Got presigned URL");
2087
2151
  spinner.start(`Uploading ${humanSize(stats.size)}`);
2088
2152
  const fileStream = Readable.toWeb(createReadStream(filePath));
@@ -2193,8 +2257,13 @@ More results: --cursor ${res.next_cursor}`) : out;
2193
2257
  versions.command("add <id> <file>").description("Upload a new version of an existing document").action(async (id, filePath) => {
2194
2258
  const spinner = ora2();
2195
2259
  try {
2196
- const stats = await stat(filePath);
2197
- if (!stats.isFile()) throw new Error(`${filePath} is not a file.`);
2260
+ const stats = await stat(filePath).catch((statErr) => {
2261
+ throw new ValidationError(
2262
+ `Cannot read ${filePath}: ${statErr.message}`,
2263
+ { code: statErr.code }
2264
+ );
2265
+ });
2266
+ if (!stats.isFile()) throw new ValidationError(`${filePath} is not a file.`);
2198
2267
  const fileName = basename(filePath);
2199
2268
  const contentType = lookupMimeType(filePath) || "application/octet-stream";
2200
2269
  spinner.start("Requesting upload URL");
@@ -2203,6 +2272,15 @@ More results: --cursor ${res.next_cursor}`) : out;
2203
2272
  content_type: contentType,
2204
2273
  content_length: stats.size
2205
2274
  });
2275
+ if (getRuntime().dryRun) {
2276
+ spinner.stop();
2277
+ process.stderr.write(
2278
+ `[dry-run] PUT <presigned upload_url from the response above> (file: ${filePath}, ${humanSize(stats.size)})
2279
+ [dry-run] POST /v1/documents/${id}/versions (upload_id)
2280
+ `
2281
+ );
2282
+ return;
2283
+ }
2206
2284
  spinner.succeed("Got presigned URL");
2207
2285
  spinner.start(`Uploading ${humanSize(stats.size)}`);
2208
2286
  const fileStream = Readable.toWeb(createReadStream(filePath));
@@ -2271,6 +2349,7 @@ More results: --cursor ${res.next_cursor}`) : out;
2271
2349
  });
2272
2350
  }
2273
2351
  async function finalizeDocumentCreation(doc, wantLink) {
2352
+ if (getRuntime().dryRun) return;
2274
2353
  let link = null;
2275
2354
  let linkWarning = null;
2276
2355
  if (wantLink) {
@@ -2322,7 +2401,7 @@ function registerFoldersCommands(program2) {
2322
2401
  if (opts.json) setRuntime({ json: true });
2323
2402
  try {
2324
2403
  const res = await api.listFolders({
2325
- limit: opts.limit ? Number(opts.limit) : void 0,
2404
+ limit: parseLimit(opts.limit),
2326
2405
  cursor: opts.cursor,
2327
2406
  parent_id: opts.parent
2328
2407
  });
@@ -2381,7 +2460,7 @@ More results: --cursor ${res.next_cursor}`) : out;
2381
2460
  if (opts.color !== void 0)
2382
2461
  body.color = opts.color === "" ? null : opts.color;
2383
2462
  if (Object.keys(body).length === 0) {
2384
- throw new Error(
2463
+ throw new ValidationError(
2385
2464
  "Pass at least one of --name, --icon, --color to update."
2386
2465
  );
2387
2466
  }
@@ -2417,7 +2496,7 @@ More results: --cursor ${res.next_cursor}`) : out;
2417
2496
  ).action(async (id, opts) => {
2418
2497
  try {
2419
2498
  if (opts.parent === void 0) {
2420
- throw new Error(
2499
+ throw new ValidationError(
2421
2500
  "Pass --parent <id> (use empty string to move to the root)."
2422
2501
  );
2423
2502
  }
@@ -2475,42 +2554,42 @@ function parseWatermarkConfig(opts) {
2475
2554
  if (provided.length === 0) return void 0;
2476
2555
  const missing = Object.entries(fields).filter(([, v]) => v === void 0).map(([k]) => flagNames[k]);
2477
2556
  if (missing.length > 0) {
2478
- throw new Error(
2557
+ throw new ValidationError(
2479
2558
  `Watermark config is partial. Missing: ${missing.join(", ")}.`
2480
2559
  );
2481
2560
  }
2482
2561
  const tiled = fields.is_tiled.toLowerCase() === "true" || fields.is_tiled.toLowerCase() === "on";
2483
2562
  const tiledOff = fields.is_tiled.toLowerCase() === "false" || fields.is_tiled.toLowerCase() === "off";
2484
2563
  if (!tiled && !tiledOff) {
2485
- throw new Error(
2564
+ throw new ValidationError(
2486
2565
  `Invalid --watermark-tiled value ${JSON.stringify(fields.is_tiled)} (expected on/off).`
2487
2566
  );
2488
2567
  }
2489
2568
  if (!WATERMARK_POSITIONS.includes(fields.position)) {
2490
- throw new Error(
2569
+ throw new ValidationError(
2491
2570
  `Invalid --watermark-position ${JSON.stringify(fields.position)}. Expected one of: ${WATERMARK_POSITIONS.join(", ")}.`
2492
2571
  );
2493
2572
  }
2494
2573
  const rotation = Number(fields.rotation);
2495
2574
  if (!Number.isFinite(rotation) || !WATERMARK_ROTATIONS.includes(rotation)) {
2496
- throw new Error(
2575
+ throw new ValidationError(
2497
2576
  `Invalid --watermark-rotation ${JSON.stringify(fields.rotation)}. Expected one of: ${WATERMARK_ROTATIONS.join(", ")}.`
2498
2577
  );
2499
2578
  }
2500
2579
  if (!/^#([0-9A-Fa-f]{3}){1,2}$/.test(fields.color)) {
2501
- throw new Error(
2580
+ throw new ValidationError(
2502
2581
  `Invalid --watermark-color ${JSON.stringify(fields.color)}. Expected #RGB or #RRGGBB.`
2503
2582
  );
2504
2583
  }
2505
2584
  const fontSize = Number(fields.font_size);
2506
2585
  if (!Number.isInteger(fontSize) || fontSize < 1 || fontSize > 96) {
2507
- throw new Error(
2586
+ throw new ValidationError(
2508
2587
  `Invalid --watermark-font-size ${JSON.stringify(fields.font_size)}. Expected integer 1-96.`
2509
2588
  );
2510
2589
  }
2511
2590
  const opacity = Number(fields.opacity);
2512
2591
  if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
2513
- throw new Error(
2592
+ throw new ValidationError(
2514
2593
  `Invalid --watermark-opacity ${JSON.stringify(fields.opacity)}. Expected 0-1.`
2515
2594
  );
2516
2595
  }
@@ -2543,7 +2622,7 @@ function registerLinksCommands(program2) {
2543
2622
  if (opts.json) setRuntime({ json: true });
2544
2623
  try {
2545
2624
  const res = await api.listLinks({
2546
- limit: opts.limit ? Number(opts.limit) : void 0,
2625
+ limit: parseLimit(opts.limit),
2547
2626
  cursor: opts.cursor,
2548
2627
  document_id: opts.document,
2549
2628
  dataroom_id: opts.dataroom,
@@ -2608,7 +2687,7 @@ More results: --cursor ${res.next_cursor}`) : out;
2608
2687
  async (opts) => {
2609
2688
  try {
2610
2689
  if (!opts.document && !opts.dataroom) {
2611
- throw new Error("Either --document <id> or --dataroom <id> is required.");
2690
+ throw new ValidationError("Either --document <id> or --dataroom <id> is required.");
2612
2691
  }
2613
2692
  if (opts.group && !opts.dataroom) {
2614
2693
  throw new Error("--group requires --dataroom <id>.");
@@ -2677,7 +2756,7 @@ More results: --cursor ${res.next_cursor}`) : out;
2677
2756
  const norm = v.toLowerCase();
2678
2757
  if (norm === "on") return true;
2679
2758
  if (norm === "off") return false;
2680
- throw new Error(
2759
+ throw new ValidationError(
2681
2760
  `Invalid value for ${flag}: ${JSON.stringify(v)} (expected "on" or "off").`
2682
2761
  );
2683
2762
  };
@@ -2712,7 +2791,7 @@ More results: --cursor ${res.next_cursor}`) : out;
2712
2791
  if (wm !== void 0) body.enable_watermark = wm;
2713
2792
  const watermarkConfig = parseWatermarkConfig(opts);
2714
2793
  if (opts.watermarkClear && watermarkConfig !== void 0) {
2715
- throw new Error(
2794
+ throw new ValidationError(
2716
2795
  "Pass either --watermark-clear or --watermark-* flags, not both."
2717
2796
  );
2718
2797
  }
@@ -2733,7 +2812,7 @@ More results: --cursor ${res.next_cursor}`) : out;
2733
2812
  if (opts.domain !== void 0) body.domain = opts.domain;
2734
2813
  if (opts.slug !== void 0) body.slug = opts.slug;
2735
2814
  if (Object.keys(body).length === 0) {
2736
- throw new Error(
2815
+ throw new ValidationError(
2737
2816
  "Pass at least one field to update (e.g. --name, --expires, --password)."
2738
2817
  );
2739
2818
  }
@@ -2832,7 +2911,7 @@ function registerViewsCommands(program2) {
2832
2911
  if (opts.json) setRuntime({ json: true });
2833
2912
  try {
2834
2913
  const res = await api.listViews(opts.link, {
2835
- limit: opts.limit ? Number(opts.limit) : void 0,
2914
+ limit: parseLimit(opts.limit),
2836
2915
  cursor: opts.cursor
2837
2916
  });
2838
2917
  emit(res, () => {
@@ -2864,7 +2943,7 @@ function registerVisitorsCommands(program2) {
2864
2943
  if (opts.json) setRuntime({ json: true });
2865
2944
  try {
2866
2945
  const res = await api.listVisitors({
2867
- limit: opts.limit ? Number(opts.limit) : void 0,
2946
+ limit: parseLimit(opts.limit),
2868
2947
  cursor: opts.cursor,
2869
2948
  email: opts.email,
2870
2949
  dataroom_id: opts.dataroom
@@ -2900,7 +2979,7 @@ More results: --cursor ${res.next_cursor}`) : out;
2900
2979
  if (opts.json) setRuntime({ json: true });
2901
2980
  try {
2902
2981
  const res = await api.listVisitorViews(id, {
2903
- limit: opts.limit ? Number(opts.limit) : void 0,
2982
+ limit: parseLimit(opts.limit),
2904
2983
  cursor: opts.cursor
2905
2984
  });
2906
2985
  emit(res, () => {
@@ -2924,8 +3003,18 @@ More results: --cursor ${res.next_cursor}`) : out;
2924
3003
  }
2925
3004
 
2926
3005
  // src/index.ts
3006
+ function cliVersion() {
3007
+ try {
3008
+ const pkg = JSON.parse(
3009
+ readFileSync3(new URL("../package.json", import.meta.url), "utf8")
3010
+ );
3011
+ return pkg.version ?? "0.0.0";
3012
+ } catch {
3013
+ return "0.0.0";
3014
+ }
3015
+ }
2927
3016
  var program = new Command2();
2928
- program.name("papermark").description("Papermark CLI \u2014 manage documents, links, and views").version("0.1.0").option(
3017
+ program.name("papermark").description("Papermark CLI \u2014 manage documents, links, and views").version(cliVersion()).option(
2929
3018
  "--json",
2930
3019
  "emit machine-readable JSON on stdout (auto-enabled when piped)"
2931
3020
  ).option(