papermark 0.6.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>",
@@ -1736,40 +2110,49 @@ More results: --cursor ${res.next_cursor}`) : out;
1736
2110
  create_link: opts.link ?? false
1737
2111
  });
1738
2112
  spinner.succeed(`Document created: ${chalk7.bold(doc.id)}`);
1739
- let link = null;
1740
- let linkWarning = null;
1741
- if (opts.link) {
1742
- try {
1743
- const links = await api.listLinks({
1744
- document_id: doc.id,
1745
- limit: 1
1746
- });
1747
- link = links.data[0] ?? null;
1748
- if (!link) {
1749
- linkWarning = "--link was set but no link was returned. Try `papermark links list --document " + doc.id + "`.";
1750
- }
1751
- } catch (warnErr) {
1752
- linkWarning = `Document created, but listing the share link failed: ${warnErr instanceof Error ? warnErr.message : String(warnErr)}`;
1753
- }
1754
- if (linkWarning) {
1755
- process.stderr.write(chalk7.yellow("! ") + linkWarning + "\n");
1756
- }
1757
- }
1758
- emit({ document: doc, link }, () => {
1759
- const blocks = [json(doc)];
1760
- if (link) {
1761
- blocks.push(
1762
- chalk7.green("\u2713") + " Share link: " + chalk7.bold(link.url)
1763
- );
1764
- }
1765
- return blocks.join("\n");
1766
- });
2113
+ await finalizeDocumentCreation(doc, opts.link ?? false);
1767
2114
  } catch (err) {
1768
2115
  spinner.fail();
1769
2116
  die(err);
1770
2117
  }
1771
2118
  }
1772
2119
  );
2120
+ docs.command("create-link <url>").description(
2121
+ "Create a document that points to an external URL (no file upload)"
2122
+ ).requiredOption("-n, --name <name>", "document name").option("--link", "also create a default share link for this document").option("--folder-id <id>", "place in folder by id").action(
2123
+ async (url, opts) => {
2124
+ try {
2125
+ const doc = await api.createDocument({
2126
+ name: opts.name,
2127
+ type: "link",
2128
+ external_url: url,
2129
+ folder_id: opts.folderId,
2130
+ create_link: opts.link ?? false
2131
+ });
2132
+ await finalizeDocumentCreation(doc, opts.link ?? false);
2133
+ } catch (err) {
2134
+ die(err);
2135
+ }
2136
+ }
2137
+ );
2138
+ docs.command("create-notion <url>").description(
2139
+ "Create a document from a public Notion page URL (no file upload)"
2140
+ ).requiredOption("-n, --name <name>", "document name").option("--link", "also create a default share link for this document").option("--folder-id <id>", "place in folder by id").action(
2141
+ async (url, opts) => {
2142
+ try {
2143
+ const doc = await api.createDocument({
2144
+ name: opts.name,
2145
+ type: "notion",
2146
+ external_url: url,
2147
+ folder_id: opts.folderId,
2148
+ create_link: opts.link ?? false
2149
+ });
2150
+ await finalizeDocumentCreation(doc, opts.link ?? false);
2151
+ } catch (err) {
2152
+ die(err);
2153
+ }
2154
+ }
2155
+ );
1773
2156
  const versions = docs.command("versions").description("Manage document versions");
1774
2157
  versions.command("list <id>").description("List versions of a document, primary first").option("--json", "output raw JSON").action(async (id, opts) => {
1775
2158
  if (opts.json) setRuntime({ json: true });
@@ -1848,6 +2231,32 @@ More results: --cursor ${res.next_cursor}`) : out;
1848
2231
  die(err);
1849
2232
  }
1850
2233
  });
2234
+ versions.command("add-link <id> <url>").description("Repoint a link document at a new URL (adds a new version)").action(async (id, url) => {
2235
+ try {
2236
+ const v = await api.addDocumentVersion(id, { external_url: url });
2237
+ emit(
2238
+ v,
2239
+ () => chalk7.green("\u2713") + ` Version ${v.version_number} added: ${chalk7.bold(v.id)}
2240
+ ` + json(v)
2241
+ );
2242
+ } catch (err) {
2243
+ die(err);
2244
+ }
2245
+ });
2246
+ versions.command("add-notion <id> <url>").description(
2247
+ "Repoint a notion document at a new Notion page URL (adds a new version)"
2248
+ ).action(async (id, url) => {
2249
+ try {
2250
+ const v = await api.addDocumentVersion(id, { external_url: url });
2251
+ emit(
2252
+ v,
2253
+ () => chalk7.green("\u2713") + ` Version ${v.version_number} added: ${chalk7.bold(v.id)}
2254
+ ` + json(v)
2255
+ );
2256
+ } catch (err) {
2257
+ die(err);
2258
+ }
2259
+ });
1851
2260
  versions.command("promote <id> <vid>").description("Promote a version to primary").action(async (id, vid) => {
1852
2261
  try {
1853
2262
  const v = await api.promoteDocumentVersion(id, vid);
@@ -1861,6 +2270,31 @@ More results: --cursor ${res.next_cursor}`) : out;
1861
2270
  }
1862
2271
  });
1863
2272
  }
2273
+ async function finalizeDocumentCreation(doc, wantLink) {
2274
+ let link = null;
2275
+ let linkWarning = null;
2276
+ if (wantLink) {
2277
+ try {
2278
+ const links = await api.listLinks({ document_id: doc.id, limit: 1 });
2279
+ link = links.data[0] ?? null;
2280
+ if (!link) {
2281
+ linkWarning = "--link was set but no link was returned. Try `papermark links list --document " + doc.id + "`.";
2282
+ }
2283
+ } catch (warnErr) {
2284
+ linkWarning = `Document created, but listing the share link failed: ${warnErr instanceof Error ? warnErr.message : String(warnErr)}`;
2285
+ }
2286
+ if (linkWarning) {
2287
+ process.stderr.write(chalk7.yellow("! ") + linkWarning + "\n");
2288
+ }
2289
+ }
2290
+ emit({ document: doc, link }, () => {
2291
+ const blocks = [json(doc)];
2292
+ if (link) {
2293
+ blocks.push(chalk7.green("\u2713") + " Share link: " + chalk7.bold(link.url));
2294
+ }
2295
+ return blocks.join("\n");
2296
+ });
2297
+ }
1864
2298
  function truncate3(s, n) {
1865
2299
  const clean = sanitizeForTerminal(s);
1866
2300
  return clean.length > n ? clean.slice(0, n - 1) + "\u2026" : clean;
@@ -2104,7 +2538,7 @@ function addWatermarkOptions(cmd) {
2104
2538
  }
2105
2539
  function registerLinksCommands(program2) {
2106
2540
  const links = program2.command("links").description("Manage share links");
2107
- 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(
2108
2542
  async (opts) => {
2109
2543
  if (opts.json) setRuntime({ json: true });
2110
2544
  try {
@@ -2112,7 +2546,8 @@ function registerLinksCommands(program2) {
2112
2546
  limit: opts.limit ? Number(opts.limit) : void 0,
2113
2547
  cursor: opts.cursor,
2114
2548
  document_id: opts.document,
2115
- dataroom_id: opts.dataroom
2549
+ dataroom_id: opts.dataroom,
2550
+ group_id: opts.group
2116
2551
  });
2117
2552
  emit(res, () => {
2118
2553
  const rows = res.data.map((l) => ({
@@ -2141,7 +2576,10 @@ More results: --cursor ${res.next_cursor}`) : out;
2141
2576
  }
2142
2577
  });
2143
2578
  addWatermarkOptions(
2144
- 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(
2145
2583
  "--confidential-view",
2146
2584
  "reveal only a narrow band of each page at a time; blurs the rest to prevent full-page screenshots",
2147
2585
  false
@@ -2152,11 +2590,16 @@ More results: --cursor ${res.next_cursor}`) : out;
2152
2590
  if (!opts.document && !opts.dataroom) {
2153
2591
  throw new Error("Either --document <id> or --dataroom <id> is required.");
2154
2592
  }
2593
+ if (opts.group && !opts.dataroom) {
2594
+ throw new Error("--group requires --dataroom <id>.");
2595
+ }
2155
2596
  const watermarkConfig = parseWatermarkConfig(opts);
2156
2597
  const enableWatermark = opts.watermark || watermarkConfig !== void 0;
2157
2598
  const link = await api.createLink({
2158
2599
  document_id: opts.document,
2159
2600
  dataroom_id: opts.dataroom,
2601
+ audience_type: opts.group ? "group" : void 0,
2602
+ group_id: opts.group,
2160
2603
  name: opts.name,
2161
2604
  expires_at: opts.expires,
2162
2605
  password: opts.password,
@@ -2176,7 +2619,13 @@ More results: --cursor ${res.next_cursor}`) : out;
2176
2619
  }
2177
2620
  );
2178
2621
  addWatermarkOptions(
2179
- 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(
2180
2629
  "--allow-list <items>",
2181
2630
  "comma-separated email/domain allow list (e.g. '@acme.com,bob@x.com')"
2182
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(
@@ -2197,6 +2646,16 @@ More results: --cursor ${res.next_cursor}`) : out;
2197
2646
  };
2198
2647
  const list = (v) => v === void 0 ? void 0 : v === "" ? [] : v.split(",").map((s) => s.trim()).filter(Boolean);
2199
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;
2200
2659
  if (opts.name !== void 0) body.name = opts.name;
2201
2660
  if (opts.expires !== void 0)
2202
2661
  body.expires_at = opts.expires === "" ? null : opts.expires;
@@ -2257,6 +2716,64 @@ More results: --cursor ${res.next_cursor}`) : out;
2257
2716
  die(err);
2258
2717
  }
2259
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
+ );
2260
2777
  }
2261
2778
  function truncate5(s, n) {
2262
2779
  const clean = sanitizeForTerminal(s);