@vercel/board 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +8 -17
  2. package/dist/{a2-HUbQJ0RD.mjs → a2-DngWY2Eb.mjs} +741 -415
  3. package/dist/access-CY5xVMIK.mjs +242 -0
  4. package/dist/{access-CV96SYgr.d.mts → access-DPcrKIH_.d.mts} +6 -1
  5. package/dist/access.d.mts +4 -4
  6. package/dist/access.mjs +5 -4
  7. package/dist/{actors-DcJhFbwJ.mjs → actors-Bm-ojWWT.mjs} +2 -2
  8. package/dist/{auth-JO22iIwr.mjs → auth-CyO2aw5n.mjs} +126 -236
  9. package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
  10. package/dist/{backend-CPC_vyOo.d.mts → backend-Bob-5CM7.d.mts} +93 -42
  11. package/dist/backend-a2/blob-vercel.d.mts +1 -1
  12. package/dist/backend-a2/contract.d.mts +38 -98
  13. package/dist/backend-a2/contract.mjs +1 -1
  14. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  15. package/dist/backend-a2/scheduler-local.mjs +1 -1
  16. package/dist/backend-a2/scheduler-qstash.d.mts +3 -3
  17. package/dist/backend-a2/scheduler-qstash.mjs +8 -8
  18. package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
  19. package/dist/backend-a2/scheduler-vercel.mjs +6 -6
  20. package/dist/backend-a2/store-memory.d.mts +2 -2
  21. package/dist/backend-a2/store-memory.mjs +3 -3
  22. package/dist/backend-a2/store-postgres.d.mts +1 -1
  23. package/dist/backend-a2/store-postgres.mjs +34 -15
  24. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  25. package/dist/backend-a2/store-redis-http.mjs +6 -6
  26. package/dist/backend-a2/store-redis.d.mts +2 -2
  27. package/dist/backend-a2/store-redis.mjs +5 -5
  28. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  29. package/dist/backend-a2/store-sqlite.mjs +3 -3
  30. package/dist/backend-a2.d.mts +143 -242
  31. package/dist/backend-a2.mjs +2 -2
  32. package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-BRrRbjRx.d.mts} +59 -45
  33. package/dist/{backend-http-CzIJMm58.mjs → backend-http-CvUKOn-m.mjs} +11 -10
  34. package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-D8mUzuKQ.mjs} +2 -2
  35. package/dist/backend-http.d.mts +1 -1
  36. package/dist/backend-http.mjs +1 -1
  37. package/dist/backend-http.node.d.mts +1 -1
  38. package/dist/backend-http.node.mjs +1 -1
  39. package/dist/{backend-vercel-DQc_TaHp.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
  40. package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-BaSEUdwg.d.mts} +62 -47
  41. package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-CmY9yZ9m.mjs} +279 -42
  42. package/dist/backend-vercel.d.mts +1 -1
  43. package/dist/backend-vercel.mjs +1 -1
  44. package/dist/backend-vercel.node.d.mts +1 -1
  45. package/dist/backend-vercel.node.mjs +1 -1
  46. package/dist/{blob-DmiQjrYy.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
  47. package/dist/{blob-DdbpJqN1.mjs → blob-WbB2DJKn.mjs} +30 -11
  48. package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-BChYrrA7.d.mts} +32 -28
  49. package/dist/{chat-provider-COtsU5jB.mjs → chat-provider-DkFp2hrh.mjs} +105 -60
  50. package/dist/cli-commands.d.mts +1 -1
  51. package/dist/cli-commands.mjs +1 -1
  52. package/dist/cli-embedded.d.mts +2 -2
  53. package/dist/cli-embedded.mjs +1 -1
  54. package/dist/cli.mjs +4 -4
  55. package/dist/{client-BqAjk0Q4.d.mts → client-COfYrgfV.d.mts} +240 -227
  56. package/dist/{commands-D6gvrOh3.d.mts → commands-DK4W3HCJ.d.mts} +47 -10
  57. package/dist/{commands-C3R1AWgm.mjs → commands-L1JSGz2Z.mjs} +212 -54
  58. package/dist/conformance-DK43HiHf.mjs +1000 -0
  59. package/dist/conformance.d.mts +8 -2
  60. package/dist/conformance.mjs +1 -1
  61. package/dist/{engine-DQxXk7dX.mjs → engine-DurYIRt8.mjs} +29 -210
  62. package/dist/{errors-DDktVeUa.d.mts → errors-2vGs49Db.d.mts} +1 -1
  63. package/dist/{client-D0Px7KzF.mjs → errors-CJXhhzvT.mjs} +132 -27
  64. package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
  65. package/dist/{exports-BG-0Wyn-.mjs → exports-CWIdN0Ub.mjs} +195 -103
  66. package/dist/exports-FBOQWT7O.d.mts +5 -0
  67. package/dist/{folds-BDMSxBUf.mjs → folds-Cu9Q9zyQ.mjs} +3 -22
  68. package/dist/{folds-ZkHM4M-v.d.mts → folds-DLUzfibJ.d.mts} +15 -40
  69. package/dist/{index-D3VvNfh_.d.mts → index-Ho-ayyQL.d.mts} +2 -2
  70. package/dist/index.d.mts +7 -7
  71. package/dist/{index.default-rB8sJ9RX.mjs → index.default-BUcwDaAe.mjs} +1 -1
  72. package/dist/index.mjs +5 -6
  73. package/dist/index.node.d.mts +7 -7
  74. package/dist/index.node.mjs +3 -4
  75. package/dist/inspect/app.js +62 -61
  76. package/dist/inspect/engine.js +3 -3
  77. package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
  78. package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
  79. package/dist/{main-CVBqe3VO.mjs → main-Dz-8komG.mjs} +251 -127
  80. package/dist/{middleware-CXjEeef4.mjs → middleware-BavYSDUa.mjs} +1 -1
  81. package/dist/{node-Zd_C-gxP.mjs → node-BgeGcwX0.mjs} +7 -3
  82. package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
  83. package/dist/{project-CBXlLftU.mjs → project-CQ4VUibA.mjs} +1 -1
  84. package/dist/{provider-DmSTp9lI.d.mts → provider-gynDUoyW.d.mts} +13 -43
  85. package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-DHf85ixI.mjs} +40 -42
  86. package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
  87. package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
  88. package/dist/{reducers-DJzVpXwM.mjs → reducers-DcENwtwZ.mjs} +74 -77
  89. package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
  90. package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
  91. package/dist/{serve-G9RuaDSr.mjs → serve-CTmow-YH.mjs} +7 -7
  92. package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
  93. package/dist/{server-BixV_byP.mjs → server-DG8D6VV0.mjs} +4 -4
  94. package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
  95. package/dist/{serving-B75Y4BF-.mjs → serving-DBFRo6U_.mjs} +1 -1
  96. package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
  97. package/dist/{sql-D4Z_efJX.mjs → sql-CT5xVuGr.mjs} +2 -8
  98. package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
  99. package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
  100. package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
  101. package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
  102. package/dist/store-memory-4IBl0Ztu.mjs +2 -0
  103. package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
  104. package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
  105. package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
  106. package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
  107. package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
  108. package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
  109. package/dist/{sync-kapso.d.mts → sync-chat-sdk.d.mts} +33 -37
  110. package/dist/sync-chat-sdk.mjs +44 -0
  111. package/dist/sync-github.d.mts +5 -18
  112. package/dist/sync-github.mjs +4 -70
  113. package/dist/sync-linear.d.mts +3 -3
  114. package/dist/sync-linear.mjs +6 -5
  115. package/dist/sync-slack.d.mts +7 -7
  116. package/dist/sync-slack.mjs +33 -54
  117. package/dist/sync.d.mts +3 -3
  118. package/dist/sync.mjs +2 -4
  119. package/dist/{tokens-CC3izSNr.mjs → tokens-Dnyd_y1n.mjs} +15 -6
  120. package/dist/{types-No0WdhE5.d.mts → types-CmrFjpml.d.mts} +2 -4
  121. package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-BjwlnGD9.mjs} +124 -115
  122. package/docs/01-board/01-mounting.md +2 -2
  123. package/docs/01-board/03-identity.md +5 -6
  124. package/docs/01-board/04-messages.md +96 -2
  125. package/docs/01-board/05-reading.md +2 -1
  126. package/docs/01-board/06-watchers.md +8 -6
  127. package/docs/01-board/08-middleware.md +6 -3
  128. package/docs/01-board/09-access.md +145 -14
  129. package/docs/02-sync/01-overview.md +19 -26
  130. package/docs/02-sync/02-functions.md +8 -9
  131. package/docs/02-sync/03-slack.md +5 -5
  132. package/docs/02-sync/04-github.md +2 -2
  133. package/docs/02-sync/06-providers.md +91 -62
  134. package/docs/03-guides/01-coding-agents.md +1 -1
  135. package/docs/03-guides/03-ci.md +7 -4
  136. package/docs/04-reference/01-cli.md +109 -51
  137. package/docs/04-reference/02-board.md +76 -28
  138. package/docs/04-reference/03-http.md +76 -38
  139. package/docs/04-reference/04-log.md +8 -11
  140. package/docs/04-reference/05-conformance.md +47 -7
  141. package/docs/index.md +2 -2
  142. package/package.json +7 -10
  143. package/sql/postgres/board.sql +1 -1
  144. package/sql/postgres/schema.sql +1 -1
  145. package/dist/conformance-DlnfuzEB.mjs +0 -556
  146. package/dist/errors-CusN4x2l.mjs +0 -88
  147. package/dist/events-qf5XG5VQ.mjs +0 -75
  148. package/dist/exports-mhAYiU4i.d.mts +0 -5
  149. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
  150. package/dist/store-memory-ZLiZbH3R.mjs +0 -2
  151. package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
  152. package/dist/sync-kapso.mjs +0 -89
  153. package/dist/sync.chat.d.mts +0 -2
  154. package/dist/sync.chat.mjs +0 -2
  155. package/dist/users-yryHKFMo.mjs +0 -42
  156. package/docs/02-sync/05-whatsapp.md +0 -152
@@ -1,14 +1,15 @@
1
1
  import { i as fileSize, n as DIRECT_UPLOAD_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
2
  import { a as AttachmentId } from "./schemas-DBmUO6pr.mjs";
3
- import { d as VERSION, i as withMentions } from "./watchers-hash-BDhLVkV5.mjs";
3
+ import { d as VERSION, i as withMentions } from "./watchers-hash-BjwlnGD9.mjs";
4
4
  import { n as displayEmoji, r as normalizeEmoji } from "./emoji-CTLpvH-A.mjs";
5
- import { t as BoardError } from "./client-D0Px7KzF.mjs";
6
- import { i as groups, n as filters$1, o as options, r as globalOptions, s as usage$1, t as commands } from "./commands-C3R1AWgm.mjs";
7
- import { a as handFetch, d as vercelProjectFile, n as boardUrl, o as handLogin, r as connect } from "./node-Zd_C-gxP.mjs";
8
- import { t as http } from "./backend-http-Bmp0IgD3.mjs";
9
- import { t as board } from "./server-BixV_byP.mjs";
10
- import { t as conformance } from "./conformance-DlnfuzEB.mjs";
11
- import { n as handTeam, r as vercel, t as credentialSource } from "./backend-vercel-DQc_TaHp.mjs";
5
+ import { u as BoardError } from "./errors-CJXhhzvT.mjs";
6
+ import { i as groups, n as filters$1, o as options, r as globalOptions, s as usage$1, t as commands } from "./commands-L1JSGz2Z.mjs";
7
+ import { i as parseScope } from "./access-CY5xVMIK.mjs";
8
+ import { a as handFetch, d as vercelProjectFile, n as boardUrl, o as handLogin, r as connect } from "./node-BgeGcwX0.mjs";
9
+ import { t as http } from "./backend-http-D8mUzuKQ.mjs";
10
+ import { t as board } from "./server-DG8D6VV0.mjs";
11
+ import { t as conformance } from "./conformance-DK43HiHf.mjs";
12
+ import { n as handTeam, r as vercel, t as credentialSource } from "./backend-vercel-Ba8JDtqW.mjs";
12
13
  import { t as displayText } from "./bots-CGkO76qH.mjs";
13
14
  import { spawn } from "node:child_process";
14
15
  import { readFile, stat, writeFile } from "node:fs/promises";
@@ -34,7 +35,6 @@ function authorName(message) {
34
35
  if (!author) return message.by.actor;
35
36
  const slash = author.handle.indexOf("/");
36
37
  if (message.by.profile !== null && slash !== -1) return `${author.name} via ${author.handle.slice(0, slash)}`;
37
- if (author.person && author.person.handle !== author.handle) return `${author.name} (${author.person.handle})`;
38
38
  return author.name;
39
39
  }
40
40
  function ago(iso, now = Date.now()) {
@@ -83,7 +83,7 @@ function embed(link, pad) {
83
83
  /** One message as a block: header line, indented text, reactions. */
84
84
  function formatMessage(message, options = {}) {
85
85
  const pad = options.reply ? " " : " ";
86
- const head = `${options.reply ? " ↳ " : ""}#${message.id} ${authorName(message)} · ${ago(message.createdAt)}${message.editedAt ? " (edited)" : ""}${message.schedule ? ` · scheduled s${message.schedule.id}` : ""}${labels(message)}`;
86
+ const head = `${options.reply ? " ↳ " : ""}#${message.id} ${authorName(message)} · ${ago(message.createdAt)}${message.editedAt ? " (edited)" : ""}${message.schedule ? ` · scheduled ${message.schedule.id}` : ""}${labels(message)}`;
87
87
  const body = message.deleted ? `${pad}(message removed)` : indent(displayText(message.text, message.by.actor), pad);
88
88
  const files = (message.attachments ?? []).map((a) => `${pad}file ${a.name} · ${a.type} · ${fileSize(a.size)} · ${a.id}`);
89
89
  const links = message.deleted ? [] : (message.links ?? []).flatMap((link) => embed(link, pad));
@@ -161,6 +161,36 @@ function formatAccess(access) {
161
161
  return `${pattern.padEnd(width)} ${permissions.includes("manage") ? "all" : permissions.length === 0 ? "nothing" : permissions.join(" ")}`;
162
162
  });
163
163
  }
164
+ /** What a grant or token may do, as `--can` takes it: `dev/=read,post;ci/=read`, or `all`. */
165
+ function formatCan(can) {
166
+ const patterns = Object.keys(can);
167
+ if (patterns.length === 1 && patterns[0] === "*" && can["*"].includes("manage")) return "all";
168
+ if (patterns.length === 0) return "nothing";
169
+ return patterns.map((pattern) => `${pattern}=${can[pattern].includes("manage") ? "all" : can[pattern].join(",")}`).join(";");
170
+ }
171
+ /** Rows of cells, each column padded to its widest, two spaces apart. */
172
+ const table = (rows) => {
173
+ const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
174
+ return rows.map((row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd()).join("\n");
175
+ };
176
+ /** Each grant on a row: its ID, its issuer, the claims it matches, and what it lets do. */
177
+ function formatGrants(grants) {
178
+ return table(grants.map((grant) => [
179
+ grant.id,
180
+ grant.issuer,
181
+ Object.entries(grant.claims).map(([name, value]) => `${name}=${typeof value === "string" ? value : value.join("|")}`).join(" "),
182
+ formatCan(grant.can)
183
+ ]));
184
+ }
185
+ /** Each token on a row: its ID, its name, what it may do, and when it expires. */
186
+ function formatTokens(tokens, now = Date.now()) {
187
+ return table(tokens.map((token) => [
188
+ token.id,
189
+ token.name,
190
+ formatCan(token.can),
191
+ `expires ${until(token.expiresAt, now)}${token.profile !== null ? ` · as ${token.profile}` : ""}${token.parent !== null ? ` · minted with ${token.parent}` : ""}`
192
+ ]));
193
+ }
164
194
  /** `90m` → `1h30m`, `1d`: milliseconds as a duration flag would say them. */
165
195
  function span(ms) {
166
196
  const parts = [];
@@ -193,7 +223,7 @@ function formatSchedule(schedule, options = {}) {
193
223
  const sent = schedule.sent > 0 ? ` · ${schedule.sent} sent` : "";
194
224
  const files = schedule.attachments.length ? ` · ${schedule.attachments.length} ${schedule.attachments.length === 1 ? "file" : "files"}` : "";
195
225
  const text = displayText(schedule.text, schedule.by.actor);
196
- return [`${pad}s${schedule.id} ${where} · ${repeat}${stop} · ${when}${sent}${files} · by ${authorName(schedule)}`, indent(text || "(no text)", `${pad} `)].join("\n");
226
+ return [`${pad}${schedule.id} ${where} · ${repeat}${stop} · ${when}${sent}${files} · by ${authorName(schedule)}`, indent(text || "(no text)", `${pad} `)].join("\n");
197
227
  }
198
228
  /** One line per live event, for `tail` and `wait`. */
199
229
  function formatEvent(event) {
@@ -448,9 +478,9 @@ function rows(entries, pad = padOf(entries)) {
448
478
  /** An option as help writes it: `--limit <N>`, `-h, --help`, `--label <k[=v|w]>...`. */
449
479
  const flag = (option) => `${option.short ? `-${option.short}, ` : ""}--${option.name}${option.value ? option.optionalValue ? ` [${option.value}]` : ` <${option.value}>` : ""}${option.multiple ? "..." : ""}`;
450
480
  const shown = (list) => list.filter((option) => !option.hidden);
451
- /** A command's rows in the list: itself, when it runs alone, and each of its subcommands. */
481
+ /** A command's rows in the list: itself, when it runs alone and no subcommand is what it runs, and each of its subcommands. */
452
482
  function listed(command) {
453
- return [...!command.subcommands || command.runsWithoutSubcommand ? [[usage$1([command]), command.description]] : [], ...(command.subcommands ?? []).filter((sub) => !sub.hidden).map((sub) => [usage$1([command, sub]), sub.description])];
483
+ return [...!command.subcommands || command.runsWithoutSubcommand && !command.subcommands.some((sub) => sub.default) ? [[usage$1([command]), command.description]] : [], ...(command.subcommands ?? []).filter((sub) => !sub.hidden).map((sub) => [usage$1([command, sub]), sub.description])];
454
484
  }
455
485
  /** Every command, by group, then what the app adds (`serve`, `inspect`), and the options they share. */
456
486
  function rootHelp(app, version) {
@@ -497,7 +527,7 @@ function commandHelp(name, path) {
497
527
  return lines.join("\n");
498
528
  }
499
529
  /**
500
- * The command `words` name, and its subcommand when they name one (`identity link`, `sources slack
530
+ * The command `words` name, and its subcommand when they name one (`schedule cancel`, `sources slack
501
531
  * read`): what `--help` shows the page of. Null when the first word names no command.
502
532
  */
503
533
  function commandPath(words, all) {
@@ -735,7 +765,7 @@ let typed = [];
735
765
  let warnings = [];
736
766
  /** The board this run reached by its URL, for a failure to name. */
737
767
  let reached = null;
738
- /** The help `words` ask for: a command's page when they name one (`read`, `identity link`), or else every command. */
768
+ /** The help `words` ask for: a command's page when they name one (`read`, `sources read`), or else every command. */
739
769
  function help(words = []) {
740
770
  const own = Object.values(app.commands).map((command) => command.spec);
741
771
  const path = commandPath(words, [...commands, ...own]);
@@ -840,7 +870,7 @@ const result = (value) => io.write(`${JSON.stringify(warnings.length ? {
840
870
  ...value,
841
871
  warnings
842
872
  } : value, null, io.tty ? 2 : void 0)}\n`);
843
- /** One line of a JSON stream (`tail`, `identity link`): compact, wherever it goes. */
873
+ /** One line of a JSON stream (`tail`): compact, wherever it goes. */
844
874
  const line = (value) => io.write(`${JSON.stringify(value)}\n`);
845
875
  /** A message's ID as you'd write it: `#812` or `812`. It's passed to the board as it is. */
846
876
  function messageId(value, what = "message ID") {
@@ -859,11 +889,18 @@ const tooLarge = (path) => new Failed({
859
889
  const isMessageRef = (value) => /^#[^\s/]+$/.test(value) || /^\d+$/.test(value);
860
890
  /** A message's ID as it's passed back: bare when it's a number, `#<id>` when it could be a channel's name. */
861
891
  const messageRef = (id) => /^\d+$/.test(id) ? id : `#${id}`;
862
- function labelPairs(values) {
892
+ /** `--if-revision N`, as an edit or a delete takes it. */
893
+ function ifRevision() {
894
+ const given = flags["if-revision"];
895
+ if (given === void 0) return {};
896
+ if (!/^[1-9]\d{0,15}$/.test(given)) throw usage(`--if-revision takes the message's revision, a number, and ${given} isn't one.`);
897
+ return { revision: Number(given) };
898
+ }
899
+ function labelPairs(values, wrong = "Labels look like key=value, and {value} isn't one.") {
863
900
  const pairs = {};
864
901
  for (const value of values ?? []) {
865
902
  const at = value.indexOf("=");
866
- if (at <= 0) throw usage(`Labels look like key=value, and ${value} isn't one.`);
903
+ if (at <= 0) throw usage(wrong.replace("{value}", value));
867
904
  pairs[value.slice(0, at)] = value.slice(at + 1);
868
905
  }
869
906
  return pairs;
@@ -895,12 +932,36 @@ async function attachments(board) {
895
932
  }));
896
933
  return uploaded;
897
934
  }
898
- /** `s12` or `12`. */
935
+ /** A schedule's ID as the board gave it: `s12`, or `sch_…` on Vercel's board. */
899
936
  function scheduleId(value) {
900
- const id = value?.replace(/^s(?=\d)/i, "") ?? "";
901
937
  if (value === void 0) throw usage("Missing the schedule ID, like s12.", { reason: "missing_arguments" });
902
- if (!/^[^\s/]+$/.test(id)) throw usage(`${value} isn't a schedule ID like s12.`);
903
- return id;
938
+ if (!/^[^\s/]+$/.test(value)) throw usage(`${value} isn't a schedule ID like s12.`);
939
+ return value;
940
+ }
941
+ /** A grant's or token's ID as the board gave it: `g3`, `t12`, or another board's own. */
942
+ function trustId(what, value) {
943
+ const like = what === "grant" ? "g3" : "t12";
944
+ if (value === void 0) throw usage(`Missing the ${what} ID, like ${like}.`, { reason: "missing_arguments" });
945
+ if (!/^[^\s/]+$/.test(value)) throw usage(`${value} isn't a ${what} ID like ${like}.`);
946
+ return value;
947
+ }
948
+ /** `--can`, as `x-board-scope` writes it: `dev/=read,post;ci/=read`. */
949
+ function canFlag(value) {
950
+ try {
951
+ return parseScope(value);
952
+ } catch (error) {
953
+ throw usage(`${error.message.replace(/x-board-scope/g, "--can")}.`);
954
+ }
955
+ }
956
+ /** `--claim k=v`, each again: the same name twice is any of its values. */
957
+ function claimPairs(values) {
958
+ const claims = {};
959
+ for (const value of values ?? []) {
960
+ const at = value.indexOf("=");
961
+ if (at <= 0 || at === value.length - 1) throw usage(`Claims look like name=value, and ${value} isn't one.`);
962
+ (claims[value.slice(0, at)] ??= []).push(value.slice(at + 1));
963
+ }
964
+ return Object.fromEntries(Object.entries(claims).map(([name, list]) => [name, list.length === 1 ? list[0] : list]));
904
965
  }
905
966
  /** A time flag: anything `Date` reads, as ISO. */
906
967
  function instant(flag, value) {
@@ -970,11 +1031,12 @@ function doing() {
970
1031
  case "typing": return `show typing in ${ref(a)}`;
971
1032
  case "sync": return b === void 0 ? `list where ${ref(a)} is synced` : `sync ${ref(a)} with ${b}`;
972
1033
  case "schedule": return a === "cancel" ? `cancel schedule ${b}` : `schedule a message in ${message ? ref(a) : a ?? "a channel"}`;
1034
+ case "channels": return a === "create" ? `create ${b ?? "the channel"}` : "list channels";
973
1035
  case "schedules":
974
- case "channels":
975
1036
  case "actors": return `list ${command}`;
1037
+ case "grants": return a === "create" ? `grant ${b ?? "an issuer"}` : a === "revoke" ? `revoke grant ${b}` : "list grants";
1038
+ case "tokens": return a === "create" ? `mint token ${b ?? ""}`.trimEnd() : a === "revoke" ? `revoke token ${b}` : "list tokens";
976
1039
  case "sources": return b === void 0 ? "list sources" : `${b} ${c ?? "it"} on ${a}`;
977
- case "identity": return a === "link" ? `link your account on ${b}` : a === "unlink" ? `unlink ${b}` : "list your linked accounts";
978
1040
  case "download": return `download ${a ?? "the attachment"}`;
979
1041
  case "inbox": return `read ${a}'s inbox`;
980
1042
  case "tail": return `follow ${a ?? "the board"}`;
@@ -1007,6 +1069,7 @@ function missing() {
1007
1069
  "schedule"
1008
1070
  ].includes(command) && isMessageRef(a)) return `message ${ref(a)}`;
1009
1071
  if (command === "schedule" && a === "cancel" && b !== void 0) return `schedule ${b}`;
1072
+ if ((command === "grants" || command === "tokens") && a === "revoke" && b !== void 0) return `${command === "grants" ? "grant" : "token"} ${b}`;
1010
1073
  if (command === "download") return `attachment ${a}`;
1011
1074
  }
1012
1075
  /**
@@ -1109,7 +1172,7 @@ function failureOf(error) {
1109
1172
  });
1110
1173
  const specific = (fallback) => /^[a-z]+(_[a-z]+)+$/.test(code) && code !== "invalid_request" && code !== "remote_error" ? code : fallback;
1111
1174
  if (status === 400) return theirs("usage", specific("invalid_arguments"), `Can't ${doing()}.`);
1112
- if (status === 401) return theirs("auth", "login_required", "The board didn't accept your credentials.", [{
1175
+ if (status === 401) return theirs("auth", "login_required", "The board didn't accept your credentials.", process.env.BOARD_TOKEN ? [{ instruction: "Pass a token it accepts in BOARD_TOKEN, or unset it" }] : [{
1113
1176
  instruction: "Log in again",
1114
1177
  argv: ["vercel", "login"]
1115
1178
  }]);
@@ -1121,7 +1184,37 @@ function failureOf(error) {
1121
1184
  details: []
1122
1185
  } : theirs("not_found", specific("not_found"), `Can't ${doing()}.`);
1123
1186
  }
1124
- if (status === 409) return theirs("conflict", specific("conflict"), `Can't ${doing()}.`);
1187
+ if (status === 409) {
1188
+ const { args } = current;
1189
+ const newer = code === "newer_message" && flags.after !== void 0 && args[0] !== void 0;
1190
+ const labeled = code === "label_conflict" && (flags.if !== void 0 || flags["if-unset"] !== void 0) && args[0] !== void 0;
1191
+ const changed = code === "revision_conflict" && flags["if-revision"] !== void 0 && args[0] !== void 0;
1192
+ return theirs("conflict", specific("conflict"), `Can't ${doing()}.`, newer ? [{
1193
+ instruction: `Read what came after ${ref(flags.after)}`,
1194
+ argv: [
1195
+ ...cli(),
1196
+ "read",
1197
+ args[0].replace(/^#/, ""),
1198
+ "--after",
1199
+ flags.after.replace(/^#/, "")
1200
+ ]
1201
+ }] : labeled ? [{
1202
+ instruction: "See its labels now",
1203
+ argv: [
1204
+ ...cli(),
1205
+ "read",
1206
+ args[0].replace(/^#/, "")
1207
+ ]
1208
+ }] : changed ? [{
1209
+ instruction: "Read it as it is now",
1210
+ argv: [
1211
+ ...cli(),
1212
+ "read",
1213
+ args[0].replace(/^#/, ""),
1214
+ "--json"
1215
+ ]
1216
+ }] : []);
1217
+ }
1125
1218
  if (status === 413) return theirs("limit", "payload_too_large", `Can't ${doing()}. It's too large for the board.`);
1126
1219
  if (status === 429) return theirs("rate_limit", "rate_limited", "Too many requests to the board. Try again in a minute.");
1127
1220
  if (status === 501) return theirs("platform", specific("not_supported"), `Can't ${doing()}. This board doesn't support it.`);
@@ -1174,7 +1267,10 @@ function connection(url = flags.url, options = {}) {
1174
1267
  const team = flags.scope ?? flags.team;
1175
1268
  const configured = url ?? (team === void 0 ? configuredUrl() : null);
1176
1269
  if (configured === null) {
1177
- const board$1 = board({ backend: vercel(team !== void 0 ? { team } : {}) });
1270
+ const board$1 = board({ backend: vercel({
1271
+ ...team !== void 0 ? { team } : {},
1272
+ ...sending(options.token)
1273
+ }) });
1178
1274
  return {
1179
1275
  url: null,
1180
1276
  transport: { source: credentialSource },
@@ -1185,7 +1281,10 @@ function connection(url = flags.url, options = {}) {
1185
1281
  reached = at;
1186
1282
  const onVercel = vercelBoard(at);
1187
1283
  if (onVercel) {
1188
- const board$3 = board({ backend: vercel(onVercel) });
1284
+ const board$3 = board({ backend: vercel({
1285
+ ...onVercel,
1286
+ ...sending(options.token)
1287
+ }) });
1189
1288
  return {
1190
1289
  url: at,
1191
1290
  transport: { source: credentialSource },
@@ -1204,6 +1303,15 @@ function connection(url = flags.url, options = {}) {
1204
1303
  board: profile !== void 0 ? board$2.as(profile) : board$2
1205
1304
  };
1206
1305
  }
1306
+ /** For Vercel's API, `token` in place of your own credentials: a board token's secret. */
1307
+ function sending(token) {
1308
+ if (token === void 0) return {};
1309
+ return { fetch: (input, init) => {
1310
+ const request = new Request(input, init);
1311
+ request.headers.set("authorization", `Bearer ${token}`);
1312
+ return globalThis.fetch(request);
1313
+ } };
1314
+ }
1207
1315
  /** A handle on the board this command talks to: `vb()`'s, as `--as` says, or the one `connection()` finds. */
1208
1316
  function client() {
1209
1317
  if (!io.board) return connection().board;
@@ -1287,94 +1395,6 @@ function portFlag() {
1287
1395
  if (port !== void 0 && !(Number.isInteger(port) && port >= 0 && port <= 65535)) throw usage(`${flags.port} isn't a port.`, { reason: "invalid_port" });
1288
1396
  return port;
1289
1397
  }
1290
- /** How long `identity link` waits for the sign-in or the code. */
1291
- const LINK_WAIT_MS = 6e5;
1292
- async function identity(args) {
1293
- const board = client();
1294
- const [sub, target] = args;
1295
- if (sub === void 0) {
1296
- const view = await board.identity();
1297
- if (flags.json) return result(view), 0;
1298
- if (view.links.length === 0) out("no linked accounts");
1299
- for (const link of view.links) out(`${link.handle ? `@${link.handle} ` : ""}${link.actor} by ${link.method === "code" ? "code" : "sign-in"} · ${link.at}`);
1300
- if (view.unlinked?.length) out(`not linked ${view.unlinked.join(", ")} (${named()} identity link <key>)`);
1301
- return 0;
1302
- }
1303
- if (sub === "unlink") {
1304
- if (!target) throw usage(`Missing the account to unlink: a handle or actor ID from \`${named()} identity\`.`, { reason: "missing_arguments" });
1305
- if (args.length !== 2) throw usage("identity unlink takes one account.");
1306
- const unlinked = await board.unlinkAccount(target);
1307
- if (flags.json) result({
1308
- ...unlinked,
1309
- outcome: "unlinked"
1310
- });
1311
- else out(`unlinked ${unlinked.actor}`);
1312
- return 0;
1313
- }
1314
- if (sub !== "link") throw usage(`Unknown command ${sub} for ${named()} identity.`, { reason: "unknown_command" });
1315
- if (!target) throw usage("Missing the sync entry to link your account on, such as slack.", { reason: "missing_arguments" });
1316
- if (args.length !== 2) throw usage("identity link takes one sync entry.");
1317
- const before = new Set((await board.identity()).links.map((link) => `${link.actor}|${link.at}`));
1318
- const platform = (await board.discovery().catch(() => null))?.sync.find((entry) => entry.key === target)?.platform ?? target;
1319
- const { code, expiresAt, signIn } = await board.syncCode(target);
1320
- const consent = signIn !== void 0 && !flags.code;
1321
- if (consent && browsable(signIn) === null) throw new Failed({
1322
- class: "platform",
1323
- reason: "invalid_sign_in",
1324
- message: "The board's sign-in isn't an http or https URL.",
1325
- details: [JSON.stringify(signIn)],
1326
- data: { signIn }
1327
- });
1328
- if (flags.json) line({
1329
- type: "code",
1330
- sync: target,
1331
- platform,
1332
- code,
1333
- expiresAt,
1334
- ...signIn !== void 0 ? { signIn } : {}
1335
- });
1336
- const until = Date.now() + LINK_WAIT_MS;
1337
- const sleep = () => new Promise((resolve) => setTimeout(resolve, 2e3));
1338
- /** An account linked on that platform since this run began: where the code or the sign-in landed. */
1339
- const linkedSince = async () => (await board.identity()).links.find((link) => link.actor.startsWith(`${platform}:user:`) && !before.has(`${link.actor}|${link.at}`));
1340
- if (consent) {
1341
- if (!flags.json) {
1342
- out(`Sign in to ${platform} as yourself to link it (and to post as you there):\n\n ${signIn}\n`);
1343
- out(`Only open it yourself: whoever completes it links their account to you.`);
1344
- }
1345
- if (!flags["no-open"] && !flags.json) openBrowser(signIn);
1346
- } else if (!flags.json) {
1347
- const minutes = Math.round((Date.parse(expiresAt) - Date.now()) / 6e4);
1348
- out(`From your ${platform} account, post this as a message anywhere the board syncs, within ${minutes} minutes:\n\n ${code}\n`);
1349
- }
1350
- if (flags["no-wait"]) return 0;
1351
- while (Date.now() < Math.min(until, consent ? Infinity : Date.parse(expiresAt))) {
1352
- await sleep();
1353
- const linked = await linkedSince();
1354
- if (linked) {
1355
- if (flags.json) line({
1356
- type: "linked",
1357
- account: linked
1358
- });
1359
- else out(`linked ${linked.handle ? `@${linked.handle} ` : ""}(${linked.actor})`);
1360
- return 0;
1361
- }
1362
- }
1363
- throw new Failed({
1364
- class: "timeout",
1365
- reason: "timeout",
1366
- message: consent ? "Nothing linked an account within 10 minutes." : "The code expired before a message carried it.",
1367
- next: [{
1368
- instruction: "Try again",
1369
- argv: [
1370
- ...cli(),
1371
- "identity",
1372
- "link",
1373
- target
1374
- ]
1375
- }]
1376
- });
1377
- }
1378
1398
  async function run(command, args) {
1379
1399
  switch (command) {
1380
1400
  case void 0:
@@ -1410,12 +1430,31 @@ async function run(command, args) {
1410
1430
  }
1411
1431
  case "conformance": {
1412
1432
  if (args.length > 0) throw usage("conformance takes no arguments: pass its channel with --channel.");
1413
- const report = await conformance(client(), flags.channel ? { channel: flags.channel } : {});
1433
+ const withToken = io.board ? {} : { withToken: (secret) => connection(flags.url, { token: secret }).board };
1434
+ const report = await conformance(client(), {
1435
+ ...flags.channel ? { channel: flags.channel } : {},
1436
+ ...withToken
1437
+ });
1414
1438
  if (flags.json) result(report);
1415
1439
  else out(formatConformance(report));
1416
1440
  return report.ok ? 0 : 1;
1417
1441
  }
1418
1442
  case "channels": {
1443
+ const [sub, ...rest] = args;
1444
+ if (sub === "create") {
1445
+ const [path, extra] = rest;
1446
+ if (!path) throw usage("Missing the channel to create, such as dev/alerts.", { reason: "missing_arguments" });
1447
+ if (extra !== void 0) throw usage(`channels create takes one channel: ${extra} is one too many.`);
1448
+ const { channel, created } = await client().createChannel(path);
1449
+ if (flags.json) result({
1450
+ channel,
1451
+ outcome: created ? "created" : "unchanged"
1452
+ });
1453
+ else out(created ? `created ${channel.path}` : `${channel.path} already exists`);
1454
+ return 0;
1455
+ }
1456
+ if (sub !== void 0 && sub !== "list") throw usage(`Unknown command ${sub} for ${named()} channels.`, { reason: "unknown_command" });
1457
+ if (rest.length > 0) throw usage(`channels list takes no arguments: ${rest[0]} is one too many.`);
1419
1458
  const channels = await client().channels();
1420
1459
  if (flags.json) return result({ channels }), 0;
1421
1460
  if (channels.length === 0) return out("no channels yet"), 0;
@@ -1427,17 +1466,18 @@ async function run(command, args) {
1427
1466
  group = prefix;
1428
1467
  }
1429
1468
  const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
1430
- out(`${prefix ? " " : ""}${channel.path} ${count(channel.threads, "thread", "threads")} · ${count(channel.messages, "message", "messages")} · last #${channel.lastIndex}`);
1469
+ const { threads, messages, lastIndex } = channel;
1470
+ const counted = threads !== void 0 && messages !== void 0 ? ` ${count(threads, "thread", "threads")} · ${count(messages, "message", "messages")}${lastIndex ? ` · last #${lastIndex}` : ""}` : "";
1471
+ out(`${prefix ? " " : ""}${channel.path}${counted}`);
1431
1472
  }
1432
1473
  return 0;
1433
1474
  }
1434
1475
  case "actors": {
1435
1476
  const actors = await client().actors();
1436
1477
  if (flags.json) return result({ actors }), 0;
1437
- for (const actor of actors) out(`@${actor.handle} ${actor.name} ${actor.id}${actor.person ? ` → @${actor.person.handle}` : ""}`);
1478
+ for (const actor of actors) out(`@${actor.handle} ${actor.name} ${actor.id}`);
1438
1479
  return 0;
1439
1480
  }
1440
- case "identity": return identity(args);
1441
1481
  case "read": {
1442
1482
  const target = args[0];
1443
1483
  const board = client();
@@ -1506,6 +1546,7 @@ async function run(command, args) {
1506
1546
  ]
1507
1547
  }] });
1508
1548
  if (flags.sync?.length && flags["no-sync"]) throw usage("Can't use --sync with --no-sync.");
1549
+ if (re === void 0 && flags.after !== void 0) throw usage("Can't use --after on a new thread. It's for replies: the thread's newest message.");
1509
1550
  const board = client();
1510
1551
  const content = body === void 0 && flags.attach?.length ? "" : await text(body);
1511
1552
  const files = await attachments(board);
@@ -1519,7 +1560,10 @@ async function run(command, args) {
1519
1560
  const message = re === void 0 ? await board.post(target, {
1520
1561
  ...options,
1521
1562
  ...flags.sync?.length ? { sync: flags.sync } : {}
1522
- }) : await board.reply(re, options);
1563
+ }) : await board.reply(re, {
1564
+ ...options,
1565
+ ...flags.after !== void 0 ? { after: messageId(flags.after) } : {}
1566
+ });
1523
1567
  warnUnknownMentions(message.unknownMentions);
1524
1568
  printMessage(message);
1525
1569
  return 0;
@@ -1533,7 +1577,8 @@ async function run(command, args) {
1533
1577
  text: content,
1534
1578
  ...files.length ? { attachments: files } : {},
1535
1579
  ...flags["idempotency-key"] ? { id: flags["idempotency-key"] } : {},
1536
- ...flags["no-sync"] ? { noSync: true } : {}
1580
+ ...flags["no-sync"] ? { noSync: true } : {},
1581
+ ...flags.after !== void 0 ? { after: messageId(flags.after) } : {}
1537
1582
  });
1538
1583
  warnUnknownMentions(message.unknownMentions);
1539
1584
  printMessage(message);
@@ -1546,7 +1591,7 @@ async function run(command, args) {
1546
1591
  schedule,
1547
1592
  outcome: canceled ? "canceled" : "unchanged"
1548
1593
  });
1549
- else out(canceled ? `canceled s${schedule.id}` : `s${schedule.id} is already ${schedule.state}`);
1594
+ else out(canceled ? `canceled ${schedule.id}` : `${schedule.id} is already ${schedule.state}`);
1550
1595
  return 0;
1551
1596
  }
1552
1597
  const [target, body] = args;
@@ -1579,6 +1624,80 @@ async function run(command, args) {
1579
1624
  else out(formatSchedule(schedule));
1580
1625
  return 0;
1581
1626
  }
1627
+ case "grants": {
1628
+ const [verb, ...rest] = args;
1629
+ if (verb === void 0 || verb === "list") {
1630
+ if (rest.length > 0) throw usage(`grants list takes no arguments: ${rest[0]} is one too many.`);
1631
+ const list = await client().grants();
1632
+ if (flags.json) return result({ grants: list }), 0;
1633
+ out(list.length === 0 ? "no grants" : formatGrants(list));
1634
+ return 0;
1635
+ }
1636
+ if (verb === "revoke") {
1637
+ const id = trustId("grant", rest[0]);
1638
+ const { revoked } = await client().revokeGrant(id);
1639
+ if (flags.json) result({
1640
+ grant: id,
1641
+ outcome: revoked ? "revoked" : "unchanged"
1642
+ });
1643
+ else out(revoked ? `revoked ${id}` : `${id} is already gone`);
1644
+ return 0;
1645
+ }
1646
+ if (verb !== "create") throw usage(`Unknown command ${verb} for ${named()} grants.`, { reason: "unknown_command" });
1647
+ const [issuer, extra] = rest;
1648
+ if (!issuer) throw usage("Missing the issuer to grant: vercel, github, or an https URL.", { reason: "missing_arguments" });
1649
+ if (extra !== void 0) throw usage(`grants create takes one issuer, and its claims as --claim name=value: ${extra} is one too many.`);
1650
+ const claims = claimPairs(flags.claim);
1651
+ if (Object.keys(claims).length === 0) throw usage(`Missing the claims ${issuer}'s tokens must carry: pass --claim name=value.`, { reason: "missing_arguments" });
1652
+ const { grant, created } = await client().createGrant({
1653
+ issuer,
1654
+ claims,
1655
+ ...flags.can !== void 0 ? { can: canFlag(flags.can) } : {}
1656
+ });
1657
+ if (flags.json) result({
1658
+ grant,
1659
+ outcome: created ? "granted" : "updated"
1660
+ });
1661
+ else out(`${created ? "granted" : "updated"} ${grant.id}`);
1662
+ return 0;
1663
+ }
1664
+ case "tokens": {
1665
+ const [verb, ...rest] = args;
1666
+ if (verb === void 0 || verb === "list") {
1667
+ if (rest.length > 0) throw usage(`tokens list takes no arguments: ${rest[0]} is one too many.`);
1668
+ const list = await client().tokens();
1669
+ if (flags.json) return result({ tokens: list }), 0;
1670
+ out(list.length === 0 ? "no tokens" : formatTokens(list));
1671
+ return 0;
1672
+ }
1673
+ if (verb === "revoke") {
1674
+ const id = trustId("token", rest[0]);
1675
+ const { revoked } = await client().revokeToken(id);
1676
+ if (flags.json) result({
1677
+ token: id,
1678
+ outcome: revoked ? "revoked" : "unchanged"
1679
+ });
1680
+ else out(revoked ? `revoked ${id}` : `${id} isn't a live token of yours`);
1681
+ return 0;
1682
+ }
1683
+ if (verb !== "create") throw usage(`Unknown command ${verb} for ${named()} tokens.`, { reason: "unknown_command" });
1684
+ const [name, extra] = rest;
1685
+ if (!name) throw usage("Missing a name for the token, like feedback-widget.", { reason: "missing_arguments" });
1686
+ if (extra !== void 0) throw usage(`tokens create takes one name: ${extra} is one too many.`);
1687
+ const { token, secret } = await client().createToken({
1688
+ name,
1689
+ ...flags.can !== void 0 ? { can: canFlag(flags.can) } : {},
1690
+ ...flags.profile !== void 0 ? { profile: flags.profile } : {},
1691
+ ...flags.session !== void 0 ? { session: flags.session } : {},
1692
+ ...flags.expires !== void 0 ? { expires: flags.expires } : {}
1693
+ });
1694
+ if (flags.json) result({
1695
+ token,
1696
+ secret
1697
+ });
1698
+ else out(secret);
1699
+ return 0;
1700
+ }
1582
1701
  case "schedules": {
1583
1702
  const list = await client().schedules({
1584
1703
  ...args[0] ? { re: messageId(args[0]) } : {},
@@ -1630,14 +1749,14 @@ async function run(command, args) {
1630
1749
  case "hook": return 0;
1631
1750
  case "edit": {
1632
1751
  const id = messageId(args[0]);
1633
- const { unknownMentions } = await client().edit(id, await text(args[1]));
1752
+ const { unknownMentions } = await client().edit(id, await text(args[1]), ifRevision());
1634
1753
  warnUnknownMentions(unknownMentions);
1635
1754
  printDone(id, "edited");
1636
1755
  return 0;
1637
1756
  }
1638
1757
  case "delete": {
1639
1758
  const id = messageId(args[0]);
1640
- await client().delete(id);
1759
+ await client().delete(id, ifRevision());
1641
1760
  printDone(id, "deleted");
1642
1761
  return 0;
1643
1762
  }
@@ -1659,9 +1778,15 @@ async function run(command, args) {
1659
1778
  const set = labelPairs(args.slice(1));
1660
1779
  const unset = flags.unset ?? [];
1661
1780
  if (Object.keys(set).length === 0 && unset.length === 0) throw usage("Missing the labels: k=v to set one, --unset k to remove one.", { reason: "missing_arguments" });
1781
+ const expected = labelPairs(flags.if, "--if takes key=value, and {value} isn't one. For a label that mustn't be set, --if-unset key.");
1782
+ for (const key of flags["if-unset"] ?? []) {
1783
+ if (Object.hasOwn(expected, key)) throw usage(`Can't use --if ${key}=… with --if-unset ${key}.`);
1784
+ expected[key] = null;
1785
+ }
1662
1786
  await client().label(id, {
1663
1787
  set,
1664
- unset
1788
+ unset,
1789
+ ...Object.keys(expected).length ? { if: expected } : {}
1665
1790
  });
1666
1791
  printDone(id, "labeled");
1667
1792
  return 0;
@@ -1888,7 +2013,6 @@ async function runOnce(argv, options) {
1888
2013
  /** The commands about the machine the CLI runs on, not the board: none of them run in-process. */
1889
2014
  const MACHINE = /* @__PURE__ */ new Set([
1890
2015
  "whoami",
1891
- "identity",
1892
2016
  "hooks",
1893
2017
  "hook",
1894
2018
  "serve",
@@ -1,5 +1,5 @@
1
1
  import { m as MESSAGE_TEXT_MAX, p as Labels } from "./schemas-DBmUO6pr.mjs";
2
- import { t as HttpError } from "./errors-CusN4x2l.mjs";
2
+ import { t as HttpError } from "./errors-CJXhhzvT.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/server/middleware.ts
5
5
  /** A write the middleware didn't let through: the API answers 422 with what it responded. */
@@ -1,4 +1,4 @@
1
- import { t as BoardError } from "./client-D0Px7KzF.mjs";
1
+ import { u as BoardError } from "./errors-CJXhhzvT.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
4
  import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
@@ -496,12 +496,16 @@ function guarded(url, doFetch, suppliedToken) {
496
496
  envRejected = true;
497
497
  }
498
498
  if (process.env.VERCEL === "1") {
499
+ let edgeless = false;
499
500
  if (!headers.has("x-vercel-trusted-oidc-idp-token")) {
500
501
  const token = headers.get("authorization")?.match(/^Bearer (.+)$/i)?.[1] ?? (typeof suppliedToken === "function" ? await suppliedToken() : suppliedToken) ?? (process.env.BOARD_TOKEN || await hostedToken(url));
501
- headers.set("x-vercel-trusted-oidc-idp-token", token);
502
+ const edge = token.startsWith("board_") ? await hostedToken(url).catch(() => null) : token;
503
+ if (edge === null) edgeless = true;
504
+ else headers.set("x-vercel-trusted-oidc-idp-token", edge);
502
505
  }
503
506
  return through(doFetch, input, init, null).catch((error) => {
504
- throw error instanceof Wall ? new BoardError("protected", `${url} is behind Vercel Deployment Protection, and it rejected this deployment's Vercel OIDC token.`, error.status, { hint: "Add this project and environment to the board project's Deployment Protection Trusted Sources." }) : error;
507
+ if (!(error instanceof Wall)) throw error;
508
+ throw edgeless ? new BoardError("protected", `${url} is behind Vercel Deployment Protection, and this deployment has no Vercel OIDC token to get through it.`, error.status, { hint: "Enable OIDC on its project, and add it to the board project's Deployment Protection Trusted Sources." }) : new BoardError("protected", `${url} is behind Vercel Deployment Protection, and it rejected this deployment's Vercel OIDC token.`, error.status, { hint: "Add this project and environment to the board project's Deployment Protection Trusted Sources." });
505
509
  });
506
510
  }
507
511
  const used = await (secret ??= readBypass(host));
@@ -1,4 +1,4 @@
1
- //#region ../../node_modules/.pnpm/experimental-a2@0.22.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__52060f00b21aecfab049c27fa3abd2f9/node_modules/experimental-a2/dist/platform-B4TnJtWu.js
1
+ //#region ../../node_modules/.pnpm/experimental-a2@0.24.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__b1e2c243a963f2620a5d202817417c26/node_modules/experimental-a2/dist/platform-B4TnJtWu.js
2
2
  const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
3
3
  function requestContext() {
4
4
  try {
@@ -1,5 +1,5 @@
1
1
  import { n as setOwn, t as own } from "./own-OwgPWdmh.mjs";
2
- import { n as foldMessages, r as foldSync } from "./folds-BDMSxBUf.mjs";
2
+ import { n as foldMessages, r as foldSync } from "./folds-Cu9Q9zyQ.mjs";
3
3
  //#region src/store/project.ts
4
4
  /** The message an event concerns, if any. */
5
5
  function concernedMessageId(event) {