papermark 0.7.0 → 0.8.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
@@ -269,6 +269,13 @@ var api = {
269
269
  createLink: (body) => request("/v1/links", { method: "POST", body }),
270
270
  updateLink: (id, body) => request(`/v1/links/${id}`, { method: "PATCH", body }),
271
271
  deleteLink: (id) => request(`/v1/links/${id}`, { method: "DELETE" }),
272
+ getLinkPermissions: (id) => request(`/v1/links/${id}/permissions`),
273
+ // Full-replace semantics: the payload is the complete desired state;
274
+ // items not listed lose their override.
275
+ setLinkPermissions: (id, body) => request(`/v1/links/${id}/permissions`, {
276
+ method: "PUT",
277
+ body
278
+ }),
272
279
  // Views
273
280
  listViews: (linkId, query) => request(`/v1/links/${linkId}/views`, { query }),
274
281
  // Datarooms
@@ -308,6 +315,44 @@ var api = {
308
315
  method: "POST",
309
316
  body
310
317
  }),
318
+ // Dataroom groups
319
+ listDataroomGroups: (id, query) => request(`/v1/datarooms/${id}/groups`, { query }),
320
+ createDataroomGroup: (id, body) => request(`/v1/datarooms/${id}/groups`, {
321
+ method: "POST",
322
+ body
323
+ }),
324
+ getDataroomGroup: (id, gid) => request(`/v1/datarooms/${id}/groups/${gid}`),
325
+ updateDataroomGroup: (id, gid, body) => request(`/v1/datarooms/${id}/groups/${gid}`, {
326
+ method: "PATCH",
327
+ body
328
+ }),
329
+ // Also deletes every share link pointing at the group (matching the
330
+ // dashboard).
331
+ deleteDataroomGroup: (id, gid) => request(`/v1/datarooms/${id}/groups/${gid}`, {
332
+ method: "DELETE"
333
+ }),
334
+ listDataroomGroupMembers: (id, gid, query) => request(
335
+ `/v1/datarooms/${id}/groups/${gid}/members`,
336
+ { query }
337
+ ),
338
+ addDataroomGroupMembers: (id, gid, body) => request(
339
+ `/v1/datarooms/${id}/groups/${gid}/members`,
340
+ { method: "POST", body }
341
+ ),
342
+ removeDataroomGroupMember: (id, gid, mid) => request(
343
+ `/v1/datarooms/${id}/groups/${gid}/members/${mid}`,
344
+ { method: "DELETE" }
345
+ ),
346
+ listDataroomGroupPermissions: (id, gid, query) => request(
347
+ `/v1/datarooms/${id}/groups/${gid}/permissions`,
348
+ { query }
349
+ ),
350
+ // Upsert-delta semantics: entries sent are upserted, entries omitted keep
351
+ // their current state; ancestor folders of visible items are auto-shown.
352
+ setDataroomGroupPermissions: (id, gid, body) => request(
353
+ `/v1/datarooms/${id}/groups/${gid}/permissions`,
354
+ { method: "PUT", body }
355
+ ),
311
356
  // Visitors
312
357
  listVisitors: (query) => request("/v1/visitors", { query }),
313
358
  getVisitor: (id) => request(`/v1/visitors/${id}`),
@@ -1073,6 +1118,92 @@ function registerConfigCommands(program2) {
1073
1118
 
1074
1119
  // src/commands/datarooms.ts
1075
1120
  import chalk5 from "chalk";
1121
+
1122
+ // src/commands/permissions-input.ts
1123
+ import { readFileSync as readFileSync2 } from "fs";
1124
+ var ITEM_TYPES = {
1125
+ document: "dataroom_document",
1126
+ dataroom_document: "dataroom_document",
1127
+ folder: "dataroom_folder",
1128
+ dataroom_folder: "dataroom_folder"
1129
+ };
1130
+ function onOff(v, flag, fallback) {
1131
+ if (v === void 0) return fallback;
1132
+ const norm = v.toLowerCase();
1133
+ if (norm === "on") return true;
1134
+ if (norm === "off") return false;
1135
+ throw new Error(
1136
+ `Invalid value for ${flag}: ${JSON.stringify(v)} (expected "on" or "off").`
1137
+ );
1138
+ }
1139
+ function parsePermissionEntries(opts) {
1140
+ if (opts.file && opts.item) {
1141
+ throw new Error("Pass either --file or --item, not both.");
1142
+ }
1143
+ if (opts.file) {
1144
+ let raw;
1145
+ try {
1146
+ raw = readFileSync2(opts.file, "utf8");
1147
+ } catch (err) {
1148
+ throw new Error(
1149
+ `Could not read ${opts.file}: ${err instanceof Error ? err.message : String(err)}`
1150
+ );
1151
+ }
1152
+ let parsed;
1153
+ try {
1154
+ parsed = JSON.parse(raw);
1155
+ } catch {
1156
+ throw new Error(`${opts.file} is not valid JSON.`);
1157
+ }
1158
+ if (!Array.isArray(parsed)) {
1159
+ throw new Error(
1160
+ `${opts.file} must contain a JSON array of { item_id, item_type, can_view, can_download }.`
1161
+ );
1162
+ }
1163
+ return parsed.map((entry, i) => {
1164
+ const e = entry;
1165
+ const itemType = ITEM_TYPES[String(e.item_type)];
1166
+ if (typeof e.item_id !== "string" || !e.item_id) {
1167
+ throw new Error(`Entry ${i}: missing or invalid item_id.`);
1168
+ }
1169
+ if (!itemType) {
1170
+ throw new Error(
1171
+ `Entry ${i}: invalid item_type ${JSON.stringify(e.item_type)} (expected dataroom_document or dataroom_folder).`
1172
+ );
1173
+ }
1174
+ if (typeof e.can_view !== "boolean" || typeof e.can_download !== "boolean") {
1175
+ throw new Error(`Entry ${i}: can_view and can_download must be booleans.`);
1176
+ }
1177
+ return {
1178
+ item_id: e.item_id,
1179
+ item_type: itemType,
1180
+ can_view: e.can_view,
1181
+ can_download: e.can_download
1182
+ };
1183
+ });
1184
+ }
1185
+ if (opts.item) {
1186
+ const itemType = ITEM_TYPES[String(opts.type ?? "")];
1187
+ if (!itemType) {
1188
+ throw new Error(
1189
+ "Pass --type document or --type folder together with --item."
1190
+ );
1191
+ }
1192
+ return [
1193
+ {
1194
+ item_id: opts.item,
1195
+ item_type: itemType,
1196
+ can_view: onOff(opts.view, "--view", true),
1197
+ can_download: onOff(opts.download, "--download", false)
1198
+ }
1199
+ ];
1200
+ }
1201
+ throw new Error(
1202
+ "Pass --file <path> (JSON array) or --item <id> --type <document|folder> [--view on|off] [--download on|off]."
1203
+ );
1204
+ }
1205
+
1206
+ // src/commands/datarooms.ts
1076
1207
  function registerDataroomsCommands(program2) {
1077
1208
  const dr = program2.command("datarooms").alias("dr").description("Manage datarooms");
1078
1209
  dr.command("list").description("List datarooms").option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("-q, --query <substring>", "filter by dataroom name").option("--json", "output raw JSON (same as --json on the root command)").action(
@@ -1344,6 +1475,249 @@ More results: --cursor ${res.next_cursor}`) : out;
1344
1475
  }
1345
1476
  }
1346
1477
  );
1478
+ const drGroups = dr.command("groups").description(
1479
+ "Manage viewer groups: audiences with per-item permissions, shared via group links"
1480
+ );
1481
+ drGroups.command("list <id>").description("List viewer groups in a dataroom").option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("--json", "output raw JSON").action(
1482
+ async (id, opts) => {
1483
+ if (opts.json) setRuntime({ json: true });
1484
+ try {
1485
+ const res = await api.listDataroomGroups(id, {
1486
+ limit: opts.limit ? Number(opts.limit) : void 0,
1487
+ cursor: opts.cursor
1488
+ });
1489
+ emit(res, () => {
1490
+ const rows = res.data.map((g) => ({
1491
+ id: g.id,
1492
+ name: truncate2(g.name, 30),
1493
+ members: String(g.member_count),
1494
+ links: String(g.link_count),
1495
+ domains: truncate2(g.domains.join(","), 30),
1496
+ allow_all: g.allow_all ? "yes" : ""
1497
+ }));
1498
+ const out = table(rows, [
1499
+ "id",
1500
+ "name",
1501
+ "members",
1502
+ "links",
1503
+ "domains",
1504
+ "allow_all"
1505
+ ]);
1506
+ return res.next_cursor ? out + chalk5.dim(`
1507
+
1508
+ More results: --cursor ${res.next_cursor}`) : out;
1509
+ });
1510
+ } catch (err) {
1511
+ die(err);
1512
+ }
1513
+ }
1514
+ );
1515
+ drGroups.command("get <id> <gid>").description("Fetch one viewer group").action(async (id, gid) => {
1516
+ try {
1517
+ const g = await api.getDataroomGroup(id, gid);
1518
+ emit(g, () => json(g));
1519
+ } catch (err) {
1520
+ die(err);
1521
+ }
1522
+ });
1523
+ drGroups.command("create <id>").description("Create a viewer group in a dataroom").requiredOption("-n, --name <name>", "group name").option(
1524
+ "--domains <domains>",
1525
+ "comma-separated email domains treated as members (e.g. 'acme.com')"
1526
+ ).option(
1527
+ "--allow-all",
1528
+ "treat anyone passing the link's access gates as a member",
1529
+ false
1530
+ ).action(
1531
+ async (id, opts) => {
1532
+ try {
1533
+ const g = await api.createDataroomGroup(id, {
1534
+ name: opts.name,
1535
+ allow_all: opts.allowAll || void 0,
1536
+ domains: opts.domains ? opts.domains.split(",").map((s) => s.trim()).filter(Boolean) : void 0
1537
+ });
1538
+ emit(
1539
+ g,
1540
+ () => chalk5.green("\u2713") + ` Group created: ${chalk5.bold(g.id)}
1541
+ ` + json(g)
1542
+ );
1543
+ } catch (err) {
1544
+ die(err);
1545
+ }
1546
+ }
1547
+ );
1548
+ drGroups.command("update <id> <gid>").description("Update a viewer group").option("-n, --name <name>").option(
1549
+ "--domains <domains>",
1550
+ "comma-separated email domains (empty string to clear)"
1551
+ ).option("--allow-all <on|off>", "toggle the allow-anyone flag").action(
1552
+ async (id, gid, opts) => {
1553
+ try {
1554
+ const body = {};
1555
+ if (opts.name !== void 0) body.name = opts.name;
1556
+ if (opts.domains !== void 0)
1557
+ body.domains = opts.domains === "" ? [] : opts.domains.split(",").map((s) => s.trim()).filter(Boolean);
1558
+ if (opts.allowAll !== void 0) {
1559
+ const norm = opts.allowAll.toLowerCase();
1560
+ if (norm !== "on" && norm !== "off") {
1561
+ throw new Error(
1562
+ `Invalid value for --allow-all: ${JSON.stringify(opts.allowAll)} (expected "on" or "off").`
1563
+ );
1564
+ }
1565
+ body.allow_all = norm === "on";
1566
+ }
1567
+ if (Object.keys(body).length === 0) {
1568
+ throw new Error(
1569
+ "Pass at least one of --name, --domains, --allow-all to update."
1570
+ );
1571
+ }
1572
+ const g = await api.updateDataroomGroup(id, gid, body);
1573
+ emit(
1574
+ g,
1575
+ () => chalk5.green("\u2713") + ` Group updated: ${chalk5.bold(g.id)}
1576
+ ` + json(g)
1577
+ );
1578
+ } catch (err) {
1579
+ die(err);
1580
+ }
1581
+ }
1582
+ );
1583
+ drGroups.command("delete <id> <gid>").description(
1584
+ "Delete a viewer group. WARNING: also deletes every share link pointing at the group \u2014 active group links stop resolving immediately."
1585
+ ).action(async (id, gid) => {
1586
+ try {
1587
+ const deleted = await api.deleteDataroomGroup(id, gid);
1588
+ emit(
1589
+ deleted,
1590
+ () => chalk5.green("\u2713") + ` Group ${gid} deleted (including its share links).`
1591
+ );
1592
+ } catch (err) {
1593
+ die(err);
1594
+ }
1595
+ });
1596
+ const drGroupMembers = drGroups.command("members").description("Manage a group's email members");
1597
+ drGroupMembers.command("list <id> <gid>").description(
1598
+ "List a group's explicit email members (domain/allow-all admissions are not listed)"
1599
+ ).option("-l, --limit <n>", "max results (1-100)", "50").option("-c, --cursor <id>", "pagination cursor").option("--json", "output raw JSON").action(
1600
+ async (id, gid, opts) => {
1601
+ if (opts.json) setRuntime({ json: true });
1602
+ try {
1603
+ const res = await api.listDataroomGroupMembers(id, gid, {
1604
+ limit: opts.limit ? Number(opts.limit) : void 0,
1605
+ cursor: opts.cursor
1606
+ });
1607
+ emit(res, () => {
1608
+ const rows = res.data.map((m) => ({
1609
+ id: m.id,
1610
+ email: sanitizeForTerminal(m.email),
1611
+ added: m.created.slice(0, 10)
1612
+ }));
1613
+ const out = table(rows, ["id", "email", "added"]);
1614
+ return res.next_cursor ? out + chalk5.dim(`
1615
+
1616
+ More results: --cursor ${res.next_cursor}`) : out;
1617
+ });
1618
+ } catch (err) {
1619
+ die(err);
1620
+ }
1621
+ }
1622
+ );
1623
+ drGroupMembers.command("add <id> <gid>").description(
1624
+ "Add members by email (idempotent; no invitation emails are sent)"
1625
+ ).requiredOption(
1626
+ "-e, --emails <emails>",
1627
+ "comma-separated email addresses (max 500)"
1628
+ ).action(async (id, gid, opts) => {
1629
+ try {
1630
+ const emails = opts.emails.split(",").map((s) => s.trim()).filter(Boolean);
1631
+ if (emails.length === 0) {
1632
+ throw new Error("Pass at least one email via --emails.");
1633
+ }
1634
+ const res = await api.addDataroomGroupMembers(id, gid, { emails });
1635
+ emit(
1636
+ res,
1637
+ () => chalk5.green("\u2713") + ` ${res.data.length} member(s) in group after add:
1638
+ ` + table(
1639
+ res.data.map((m) => ({
1640
+ id: m.id,
1641
+ email: sanitizeForTerminal(m.email)
1642
+ })),
1643
+ ["id", "email"]
1644
+ )
1645
+ );
1646
+ } catch (err) {
1647
+ die(err);
1648
+ }
1649
+ });
1650
+ drGroupMembers.command("remove <id> <gid> <mid>").description(
1651
+ "Remove a member by membership id (from `members list`). The viewer itself is kept."
1652
+ ).action(async (id, gid, mid) => {
1653
+ try {
1654
+ const deleted = await api.removeDataroomGroupMember(id, gid, mid);
1655
+ emit(
1656
+ deleted,
1657
+ () => chalk5.green("\u2713") + ` Member ${mid} removed from group.`
1658
+ );
1659
+ } catch (err) {
1660
+ die(err);
1661
+ }
1662
+ });
1663
+ const drGroupPermissions = drGroups.command("permissions").description("Manage a group's per-item view/download permissions");
1664
+ drGroupPermissions.command("get <id> <gid>").description(
1665
+ "List the group's access controls. Items without an entry are invisible to the group."
1666
+ ).option("-l, --limit <n>", "max results (1-100)", "100").option("-c, --cursor <id>", "pagination cursor").option("--json", "output raw JSON").action(
1667
+ async (id, gid, opts) => {
1668
+ if (opts.json) setRuntime({ json: true });
1669
+ try {
1670
+ const res = await api.listDataroomGroupPermissions(id, gid, {
1671
+ limit: opts.limit ? Number(opts.limit) : void 0,
1672
+ cursor: opts.cursor
1673
+ });
1674
+ emit(res, () => {
1675
+ const rows = res.data.map((p) => ({
1676
+ item_id: p.item_id,
1677
+ type: p.item_type === "dataroom_document" ? "document" : "folder",
1678
+ view: p.can_view ? "yes" : "",
1679
+ download: p.can_download ? "yes" : ""
1680
+ }));
1681
+ const out = table(rows, ["item_id", "type", "view", "download"]);
1682
+ return res.next_cursor ? out + chalk5.dim(`
1683
+
1684
+ More results: --cursor ${res.next_cursor}`) : out;
1685
+ });
1686
+ } catch (err) {
1687
+ die(err);
1688
+ }
1689
+ }
1690
+ );
1691
+ drGroupPermissions.command("set <id> <gid>").description(
1692
+ "Upsert per-item permissions (delta: omitted items keep their state; ancestor folders of visible items are auto-shown). Item ids come from `datarooms documents` / `datarooms folders list`."
1693
+ ).option(
1694
+ "--file <path>",
1695
+ "JSON array of { item_id, item_type, can_view, can_download }"
1696
+ ).option("--item <itemId>", "single item id (use with --type/--view/--download)").option("--type <document|folder>", "item type for --item").option("--view <on|off>", "allow viewing (default on)").option("--download <on|off>", "allow downloading (default off)").action(
1697
+ async (id, gid, opts) => {
1698
+ try {
1699
+ const permissions = parsePermissionEntries(opts);
1700
+ const res = await api.setDataroomGroupPermissions(id, gid, {
1701
+ permissions
1702
+ });
1703
+ emit(
1704
+ res,
1705
+ () => chalk5.green("\u2713") + ` ${res.data.length} permission row(s) now effective:
1706
+ ` + table(
1707
+ res.data.map((p) => ({
1708
+ item_id: p.item_id,
1709
+ type: p.item_type === "dataroom_document" ? "document" : "folder",
1710
+ view: p.can_view ? "yes" : "",
1711
+ download: p.can_download ? "yes" : ""
1712
+ })),
1713
+ ["item_id", "type", "view", "download"]
1714
+ )
1715
+ );
1716
+ } catch (err) {
1717
+ die(err);
1718
+ }
1719
+ }
1720
+ );
1347
1721
  const drFolders = dr.command("folders").description("Manage folders inside a dataroom");
1348
1722
  drFolders.command("list <id>").description("List folders in a dataroom").option(
1349
1723
  "--parent <fid>",
@@ -2164,7 +2538,7 @@ function addWatermarkOptions(cmd) {
2164
2538
  }
2165
2539
  function registerLinksCommands(program2) {
2166
2540
  const links = program2.command("links").description("Manage share links");
2167
- links.command("list").description("List share links").option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("--document <id>", "filter by document id").option("--dataroom <id>", "filter by dataroom id").option("--json", "output raw JSON (same as --json on the root command)").action(
2541
+ links.command("list").description("List share links").option("-l, --limit <n>", "max results (1-100)", "25").option("-c, --cursor <id>", "pagination cursor").option("--document <id>", "filter by document id").option("--dataroom <id>", "filter by dataroom id").option("--group <gid>", "filter by dataroom group id (group links only)").option("--json", "output raw JSON (same as --json on the root command)").action(
2168
2542
  async (opts) => {
2169
2543
  if (opts.json) setRuntime({ json: true });
2170
2544
  try {
@@ -2172,7 +2546,8 @@ function registerLinksCommands(program2) {
2172
2546
  limit: opts.limit ? Number(opts.limit) : void 0,
2173
2547
  cursor: opts.cursor,
2174
2548
  document_id: opts.document,
2175
- dataroom_id: opts.dataroom
2549
+ dataroom_id: opts.dataroom,
2550
+ group_id: opts.group
2176
2551
  });
2177
2552
  emit(res, () => {
2178
2553
  const rows = res.data.map((l) => ({
@@ -2201,7 +2576,10 @@ More results: --cursor ${res.next_cursor}`) : out;
2201
2576
  }
2202
2577
  });
2203
2578
  addWatermarkOptions(
2204
- links.command("create").description("Create a share link for a document or dataroom").option("--document <id>", "document id").option("--dataroom <id>", "dataroom id").option("-n, --name <name>", "link name").option("--expires <iso-date>", "expiration date (ISO 8601)").option("--password <pw>", "require a password to view").option("--email-protected", "require email before viewing", false).option("--allow-download", "allow viewer to download the file", false).option(
2579
+ links.command("create").description("Create a share link for a document or dataroom").option("--document <id>", "document id").option("--dataroom <id>", "dataroom id").option(
2580
+ "--group <gid>",
2581
+ "scope to a dataroom group (dataroom links only; forces email protection)"
2582
+ ).option("-n, --name <name>", "link name").option("--expires <iso-date>", "expiration date (ISO 8601)").option("--password <pw>", "require a password to view").option("--email-protected", "require email before viewing", false).option("--allow-download", "allow viewer to download the file", false).option(
2205
2583
  "--confidential-view",
2206
2584
  "reveal only a narrow band of each page at a time; blurs the rest to prevent full-page screenshots",
2207
2585
  false
@@ -2212,11 +2590,16 @@ More results: --cursor ${res.next_cursor}`) : out;
2212
2590
  if (!opts.document && !opts.dataroom) {
2213
2591
  throw new Error("Either --document <id> or --dataroom <id> is required.");
2214
2592
  }
2593
+ if (opts.group && !opts.dataroom) {
2594
+ throw new Error("--group requires --dataroom <id>.");
2595
+ }
2215
2596
  const watermarkConfig = parseWatermarkConfig(opts);
2216
2597
  const enableWatermark = opts.watermark || watermarkConfig !== void 0;
2217
2598
  const link = await api.createLink({
2218
2599
  document_id: opts.document,
2219
2600
  dataroom_id: opts.dataroom,
2601
+ audience_type: opts.group ? "group" : void 0,
2602
+ group_id: opts.group,
2220
2603
  name: opts.name,
2221
2604
  expires_at: opts.expires,
2222
2605
  password: opts.password,
@@ -2236,7 +2619,13 @@ More results: --cursor ${res.next_cursor}`) : out;
2236
2619
  }
2237
2620
  );
2238
2621
  addWatermarkOptions(
2239
- links.command("update <id>").description("Update an existing share link").option("-n, --name <name>").option("--expires <iso-date>", "expiration date (ISO 8601), or empty string to clear").option("--password <pw>", "set a password (empty string to clear)").option("--email-protected <on|off>").option("--email-authenticated <on|off>").option("--allow-download <on|off>").option(
2622
+ links.command("update <id>").description("Update an existing share link").option(
2623
+ "--audience <general|group>",
2624
+ "switch the link's audience (group links need --group or an existing group)"
2625
+ ).option(
2626
+ "--group <gid>",
2627
+ "dataroom group id (with --audience group, or alone to swap the group of a group link)"
2628
+ ).option("-n, --name <name>").option("--expires <iso-date>", "expiration date (ISO 8601), or empty string to clear").option("--password <pw>", "set a password (empty string to clear)").option("--email-protected <on|off>").option("--email-authenticated <on|off>").option("--allow-download <on|off>").option(
2240
2629
  "--allow-list <items>",
2241
2630
  "comma-separated email/domain allow list (e.g. '@acme.com,bob@x.com')"
2242
2631
  ).option("--deny-list <items>", "comma-separated email/domain deny list").option("--watermark <on|off>", "enable watermark overlay").option("--watermark-clear", "clear the existing watermark config", false).option("--screenshot-protection <on|off>", "blur on screenshot").option(
@@ -2257,6 +2646,16 @@ More results: --cursor ${res.next_cursor}`) : out;
2257
2646
  };
2258
2647
  const list = (v) => v === void 0 ? void 0 : v === "" ? [] : v.split(",").map((s) => s.trim()).filter(Boolean);
2259
2648
  const body = {};
2649
+ if (opts.audience !== void 0) {
2650
+ const norm = opts.audience.toLowerCase();
2651
+ if (norm !== "general" && norm !== "group") {
2652
+ throw new Error(
2653
+ `Invalid value for --audience: ${JSON.stringify(opts.audience)} (expected "general" or "group").`
2654
+ );
2655
+ }
2656
+ body.audience_type = norm;
2657
+ }
2658
+ if (opts.group !== void 0) body.group_id = opts.group;
2260
2659
  if (opts.name !== void 0) body.name = opts.name;
2261
2660
  if (opts.expires !== void 0)
2262
2661
  body.expires_at = opts.expires === "" ? null : opts.expires;
@@ -2317,6 +2716,64 @@ More results: --cursor ${res.next_cursor}`) : out;
2317
2716
  die(err);
2318
2717
  }
2319
2718
  });
2719
+ const linkPermissions = links.command("permissions").description(
2720
+ "Per-item file permissions for dataroom links (ignored on group-audience links \u2014 the group's permissions win)"
2721
+ );
2722
+ linkPermissions.command("get <id>").description("List a dataroom link's per-item permission overrides").option("--json", "output raw JSON").action(async (id, opts) => {
2723
+ if (opts.json) setRuntime({ json: true });
2724
+ try {
2725
+ const res = await api.getLinkPermissions(id);
2726
+ emit(res, () => {
2727
+ if (res.data.length === 0) {
2728
+ return chalk9.dim(
2729
+ "No per-link overrides \u2014 viewers see the full dataroom."
2730
+ );
2731
+ }
2732
+ const rows = res.data.map((p) => ({
2733
+ item_id: p.item_id,
2734
+ type: p.item_type === "dataroom_document" ? "document" : "folder",
2735
+ view: p.can_view ? "yes" : "",
2736
+ download: p.can_download ? "yes" : ""
2737
+ }));
2738
+ return table(rows, ["item_id", "type", "view", "download"]);
2739
+ });
2740
+ } catch (err) {
2741
+ die(err);
2742
+ }
2743
+ });
2744
+ linkPermissions.command("set <id>").description(
2745
+ "Replace a dataroom link's per-item permissions (full replace: items not listed lose their override; --clear removes all overrides and hides every item). Item ids come from `datarooms documents` / `datarooms folders list`."
2746
+ ).option(
2747
+ "--file <path>",
2748
+ "JSON array of { item_id, item_type, can_view, can_download }"
2749
+ ).option("--item <itemId>", "single item id (use with --type/--view/--download)").option("--type <document|folder>", "item type for --item").option("--view <on|off>", "allow viewing (default on)").option("--download <on|off>", "allow downloading (default off)").option("--clear", "remove all overrides (hides every item on this link)", false).action(
2750
+ async (id, opts) => {
2751
+ try {
2752
+ if (opts.clear && (opts.file || opts.item)) {
2753
+ throw new Error("Pass either --clear or permission input, not both.");
2754
+ }
2755
+ const permissions = opts.clear ? [] : parsePermissionEntries(opts);
2756
+ const res = await api.setLinkPermissions(id, { permissions });
2757
+ emit(res, () => {
2758
+ if (res.data.length === 0) {
2759
+ return chalk9.green("\u2713") + " All overrides cleared \u2014 every item on this link is now hidden.";
2760
+ }
2761
+ return chalk9.green("\u2713") + ` ${res.data.length} permission row(s) now effective:
2762
+ ` + table(
2763
+ res.data.map((p) => ({
2764
+ item_id: p.item_id,
2765
+ type: p.item_type === "dataroom_document" ? "document" : "folder",
2766
+ view: p.can_view ? "yes" : "",
2767
+ download: p.can_download ? "yes" : ""
2768
+ })),
2769
+ ["item_id", "type", "view", "download"]
2770
+ );
2771
+ });
2772
+ } catch (err) {
2773
+ die(err);
2774
+ }
2775
+ }
2776
+ );
2320
2777
  }
2321
2778
  function truncate5(s, n) {
2322
2779
  const clean = sanitizeForTerminal(s);