papermark 0.7.0 → 0.9.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,30 +2576,69 @@ 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(
2583
+ "--email-authenticated",
2584
+ "require viewers to verify their email via a one-time code",
2585
+ false
2586
+ ).option("--allow-download", "allow viewer to download the file", false).option(
2587
+ "--allow-list <items>",
2588
+ "comma-separated email/domain allow list (e.g. '@acme.com,bob@x.com')"
2589
+ ).option("--deny-list <items>", "comma-separated email/domain deny list").option(
2205
2590
  "--confidential-view",
2206
2591
  "reveal only a narrow band of each page at a time; blurs the rest to prevent full-page screenshots",
2207
2592
  false
2208
- ).option("--watermark", "enable watermark overlay (use with --watermark-*)", false)
2593
+ ).option("--watermark", "enable watermark overlay (use with --watermark-*)", false).option(
2594
+ "--screenshot-protection",
2595
+ "block common screenshot/screen-recording shortcuts",
2596
+ false
2597
+ ).option(
2598
+ "--agreement <id>",
2599
+ "require viewers to accept this agreement (NDA) before viewing"
2600
+ ).option(
2601
+ "--domain <domain>",
2602
+ "verified custom domain to host the link on (requires --slug and a plan that supports custom domains)"
2603
+ ).option(
2604
+ "--slug <slug>",
2605
+ "path segment on the custom domain (requires --domain)"
2606
+ )
2209
2607
  ).action(
2210
2608
  async (opts) => {
2211
2609
  try {
2212
2610
  if (!opts.document && !opts.dataroom) {
2213
2611
  throw new Error("Either --document <id> or --dataroom <id> is required.");
2214
2612
  }
2613
+ if (opts.group && !opts.dataroom) {
2614
+ throw new Error("--group requires --dataroom <id>.");
2615
+ }
2616
+ const list = (v) => v === void 0 ? void 0 : v === "" ? [] : v.split(",").map((s) => s.trim()).filter(Boolean);
2215
2617
  const watermarkConfig = parseWatermarkConfig(opts);
2216
2618
  const enableWatermark = opts.watermark || watermarkConfig !== void 0;
2217
2619
  const link = await api.createLink({
2218
2620
  document_id: opts.document,
2219
2621
  dataroom_id: opts.dataroom,
2622
+ audience_type: opts.group ? "group" : void 0,
2623
+ group_id: opts.group,
2220
2624
  name: opts.name,
2221
2625
  expires_at: opts.expires,
2222
2626
  password: opts.password,
2223
2627
  email_protected: opts.emailProtected,
2628
+ email_authenticated: opts.emailAuthenticated,
2224
2629
  allow_download: opts.allowDownload,
2630
+ allow_list: list(opts.allowList),
2631
+ deny_list: list(opts.denyList),
2225
2632
  enable_confidential_view: opts.confidentialView,
2226
2633
  enable_watermark: enableWatermark || void 0,
2227
- watermark_config: watermarkConfig
2634
+ watermark_config: watermarkConfig,
2635
+ enable_screenshot_protection: opts.screenshotProtection,
2636
+ // A single --agreement <id> both enables the gate and sets the id,
2637
+ // mirroring how --group implies audience_type=group.
2638
+ enable_agreement: opts.agreement ? true : void 0,
2639
+ agreement_id: opts.agreement,
2640
+ domain: opts.domain,
2641
+ slug: opts.slug
2228
2642
  });
2229
2643
  emit(
2230
2644
  link,
@@ -2236,13 +2650,25 @@ More results: --cursor ${res.next_cursor}`) : out;
2236
2650
  }
2237
2651
  );
2238
2652
  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(
2653
+ links.command("update <id>").description("Update an existing share link").option(
2654
+ "--audience <general|group>",
2655
+ "switch the link's audience (group links need --group or an existing group)"
2656
+ ).option(
2657
+ "--group <gid>",
2658
+ "dataroom group id (with --audience group, or alone to swap the group of a group link)"
2659
+ ).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
2660
  "--allow-list <items>",
2241
2661
  "comma-separated email/domain allow list (e.g. '@acme.com,bob@x.com')"
2242
2662
  ).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(
2243
2663
  "--confidential-view <on|off>",
2244
2664
  "reveal only a narrow band of each page at a time; blurs the rest to prevent full-page screenshots"
2245
- )
2665
+ ).option("--enable-agreement <on|off>", "require viewers to accept an agreement (NDA)").option(
2666
+ "--agreement-id <id>",
2667
+ "agreement (NDA) id viewers must accept, or empty string to clear"
2668
+ ).option(
2669
+ "--domain <domain>",
2670
+ "verified custom domain to host the link on (with --slug); pass 'papermark.com' to reset to the default domain"
2671
+ ).option("--slug <slug>", "path segment on the custom domain (with --domain)")
2246
2672
  ).action(
2247
2673
  async (id, opts) => {
2248
2674
  try {
@@ -2257,6 +2683,16 @@ More results: --cursor ${res.next_cursor}`) : out;
2257
2683
  };
2258
2684
  const list = (v) => v === void 0 ? void 0 : v === "" ? [] : v.split(",").map((s) => s.trim()).filter(Boolean);
2259
2685
  const body = {};
2686
+ if (opts.audience !== void 0) {
2687
+ const norm = opts.audience.toLowerCase();
2688
+ if (norm !== "general" && norm !== "group") {
2689
+ throw new Error(
2690
+ `Invalid value for --audience: ${JSON.stringify(opts.audience)} (expected "general" or "group").`
2691
+ );
2692
+ }
2693
+ body.audience_type = norm;
2694
+ }
2695
+ if (opts.group !== void 0) body.group_id = opts.group;
2260
2696
  if (opts.name !== void 0) body.name = opts.name;
2261
2697
  if (opts.expires !== void 0)
2262
2698
  body.expires_at = opts.expires === "" ? null : opts.expires;
@@ -2290,6 +2726,12 @@ More results: --cursor ${res.next_cursor}`) : out;
2290
2726
  if (sp !== void 0) body.enable_screenshot_protection = sp;
2291
2727
  const cv = tri(opts.confidentialView, "--confidential-view");
2292
2728
  if (cv !== void 0) body.enable_confidential_view = cv;
2729
+ const ag = tri(opts.enableAgreement, "--enable-agreement");
2730
+ if (ag !== void 0) body.enable_agreement = ag;
2731
+ if (opts.agreementId !== void 0)
2732
+ body.agreement_id = opts.agreementId === "" ? null : opts.agreementId;
2733
+ if (opts.domain !== void 0) body.domain = opts.domain;
2734
+ if (opts.slug !== void 0) body.slug = opts.slug;
2293
2735
  if (Object.keys(body).length === 0) {
2294
2736
  throw new Error(
2295
2737
  "Pass at least one field to update (e.g. --name, --expires, --password)."
@@ -2317,6 +2759,64 @@ More results: --cursor ${res.next_cursor}`) : out;
2317
2759
  die(err);
2318
2760
  }
2319
2761
  });
2762
+ const linkPermissions = links.command("permissions").description(
2763
+ "Per-item file permissions for dataroom links (ignored on group-audience links \u2014 the group's permissions win)"
2764
+ );
2765
+ linkPermissions.command("get <id>").description("List a dataroom link's per-item permission overrides").option("--json", "output raw JSON").action(async (id, opts) => {
2766
+ if (opts.json) setRuntime({ json: true });
2767
+ try {
2768
+ const res = await api.getLinkPermissions(id);
2769
+ emit(res, () => {
2770
+ if (res.data.length === 0) {
2771
+ return chalk9.dim(
2772
+ "No per-link overrides \u2014 viewers see the full dataroom."
2773
+ );
2774
+ }
2775
+ const rows = res.data.map((p) => ({
2776
+ item_id: p.item_id,
2777
+ type: p.item_type === "dataroom_document" ? "document" : "folder",
2778
+ view: p.can_view ? "yes" : "",
2779
+ download: p.can_download ? "yes" : ""
2780
+ }));
2781
+ return table(rows, ["item_id", "type", "view", "download"]);
2782
+ });
2783
+ } catch (err) {
2784
+ die(err);
2785
+ }
2786
+ });
2787
+ linkPermissions.command("set <id>").description(
2788
+ "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`."
2789
+ ).option(
2790
+ "--file <path>",
2791
+ "JSON array of { item_id, item_type, can_view, can_download }"
2792
+ ).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(
2793
+ async (id, opts) => {
2794
+ try {
2795
+ if (opts.clear && (opts.file || opts.item)) {
2796
+ throw new Error("Pass either --clear or permission input, not both.");
2797
+ }
2798
+ const permissions = opts.clear ? [] : parsePermissionEntries(opts);
2799
+ const res = await api.setLinkPermissions(id, { permissions });
2800
+ emit(res, () => {
2801
+ if (res.data.length === 0) {
2802
+ return chalk9.green("\u2713") + " All overrides cleared \u2014 every item on this link is now hidden.";
2803
+ }
2804
+ return chalk9.green("\u2713") + ` ${res.data.length} permission row(s) now effective:
2805
+ ` + table(
2806
+ res.data.map((p) => ({
2807
+ item_id: p.item_id,
2808
+ type: p.item_type === "dataroom_document" ? "document" : "folder",
2809
+ view: p.can_view ? "yes" : "",
2810
+ download: p.can_download ? "yes" : ""
2811
+ })),
2812
+ ["item_id", "type", "view", "download"]
2813
+ );
2814
+ });
2815
+ } catch (err) {
2816
+ die(err);
2817
+ }
2818
+ }
2819
+ );
2320
2820
  }
2321
2821
  function truncate5(s, n) {
2322
2822
  const clean = sanitizeForTerminal(s);