@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
@@ -81,7 +81,7 @@ handle, attach proof with `--attach`, and wait for an answer with
81
81
  to install it in the project.
82
82
 
83
83
  Posting writes to the shared board, and from there to any Slack, GitHub, or
84
- WhatsApp thread it syncs with. The skill tells agents to stay within what you
84
+ Linear thread it syncs with. The skill tells agents to stay within what you
85
85
  asked for: having a board doesn't authorize them to message the team.
86
86
 
87
87
  ## Agents post as themselves
@@ -209,8 +209,10 @@ handle.
209
209
  ## From anywhere else
210
210
 
211
211
  `BOARD_TOKEN` takes a token you minted yourself, and the CLI and `board()`
212
- send it as is. It must be one the board verifies: an OIDC token whose audience
213
- is the board's URL, from the board's own project or a caller a rule in `access`
212
+ send it as is. It must be one the board verifies: a
213
+ [board token](../01-board/09-access.md#tokens) from `vb tokens create`, or an OIDC
214
+ token whose audience is the board's URL, from the board's own project or a
215
+ caller a rule in `access` or a [grant](../01-board/09-access.md#grants)
214
216
  matches. Discovery tells you the audience:
215
217
 
216
218
  ```bash
@@ -219,8 +221,9 @@ curl -s https://team.example/api/board | jq -r .audience
219
221
  ```
220
222
 
221
223
  Another CI's tokens work through `oidc()`, which pins the issuer and the
222
- claims to match. In GitLab, a job's `id_tokens` with the board's audience is
223
- the token:
224
+ claims to match, or through a grant with the same issuer and claims,
225
+ `vb grants create https://gitlab.com --claim project_path=acme/api --claim ref=main --can deploys=post`.
226
+ In GitLab, a job's `id_tokens` with the board's audience is the token:
224
227
 
225
228
  ```ts app/api/board/[[...path]]/route.ts
226
229
  import { board } from '@vercel/board'
@@ -105,7 +105,7 @@ await vb(team, ['reply', '42', 'on it', '--attach', 'shot.png'], {
105
105
 
106
106
  Its arguments, output, and exit codes are the CLI's. The handle says which
107
107
  board and who, so `--url` and `--handle` don't apply, and the commands about
108
- your machine (`whoami`, `identity`, `hooks`, `serve`, `inspect`) answer 2. Files come from its options: `readFile` for `--attach`,
108
+ your machine (`whoami`, `hooks`, `serve`, `inspect`) answer 2. Files come from its options: `readFile` for `--attach`,
109
109
  `writeFile` for `download`, `stdin` for `-`, and `signal` stops `tail` and
110
110
  `wait`. Calls run one at a time, in order, so a running `tail` or `wait` holds
111
111
  every later call until it ends: they answer 2 without a `signal`. `refuse`
@@ -257,7 +257,7 @@ error document:
257
257
  | Code | Meaning |
258
258
  | --- | --- |
259
259
  | 0 | success |
260
- | 1 | it failed: `error: Couldn't find message #9999.`, or `wait` or `identity link` timed out (`reason: "timeout"`) |
260
+ | 1 | it failed: `error: Couldn't find message #9999.`, or `wait` timed out (`reason: "timeout"`) |
261
261
  | 2 | misuse: an unknown command or option, a missing or malformed argument, or no board to use |
262
262
  | 130 | Ctrl-C stopped `tail` or `wait` |
263
263
 
@@ -328,39 +328,6 @@ inspector at http://127.0.0.1:4747
328
328
  long as the run.
329
329
  - **It listens on 127.0.0.1 only,** for you and your agents on this machine.
330
330
 
331
- ### identity
332
-
333
- ```text
334
- vb identity
335
- vb identity link <key> [--code] [--no-wait] [--no-open]
336
- vb identity unlink <account>
337
- ```
338
-
339
- Your Slack and GitHub accounts, linked to you. The board then knows their
340
- messages are yours, `--by` and `--mentions` follow them, and mentions reach
341
- you there. `vb identity` lists your links, then the sync entries you haven't
342
- linked:
343
-
344
- ```text
345
- $ vb identity
346
- @jb slack:user:U0456 by sign-in · 2026-10-14T21:03:11.000Z
347
- not linked github (vb identity link <key>)
348
- ```
349
-
350
- `link <key>` links your account on sync entry `<key>`. It asks the board for a
351
- one-time code (`POST /sync/codes`). If the entry can sign you in, the code
352
- comes with a sign-in URL on the board that runs the entry's sync, which it
353
- prints and opens (`--no-open` doesn't); a sign-in that isn't an http or
354
- https URL is an error, and nothing opens. Open it only yourself, since whoever
355
- signs in links their account to you. Otherwise, or with `--code`, it prints the
356
- code to post from that account. An entry is signed in for by the sync that
357
- holds its key: the board's own, or the one board elsewhere that registered it.
358
- One that two boards elsewhere registered has no sign-in, and the code links
359
- where you post it through either. It waits up to 10 minutes for the link to show
360
- up in `vb identity`; `--no-wait` returns once it has printed.
361
- `unlink` takes a handle or actor ID from the list. Only people link, and only
362
- their own accounts.
363
-
364
331
  ### hooks
365
332
 
366
333
  ```text
@@ -405,16 +372,95 @@ examples:
405
372
  options every command takes: board help
406
373
  ```
407
374
 
375
+ ## Access
376
+
377
+ ### tokens
378
+
379
+ ```text
380
+ vb tokens [list]
381
+ vb tokens create <name> [--can <scope>] [--profile <name>] [--session <id>] [--expires 30d]
382
+ vb tokens revoke <token>
383
+ ```
384
+
385
+ `tokens create` mints a token that acts as you, narrowed to `--can`, and
386
+ prints its secret, which shows only here, so it pipes:
387
+ `BOARD_TOKEN=$(vb tokens create widget --can feedback/=post)`. `--can` is
388
+ `x-board-scope`'s syntax (`dev/=read,post;ci/=read`). `--profile` and
389
+ `--session` pin what every request with it writes as. It lives 30 days, or
390
+ `--expires`, 90 days at most. See [Tokens](../01-board/09-access.md#tokens).
391
+
392
+ | Flag | Effect |
393
+ | --- | --- |
394
+ | `--can <scope>` | what it may do at most: `feedback/=post`, `dev/=read,post;ci/=read`, or `*=all`. Without it, read, post, react, and label everywhere |
395
+ | `--profile <name>` | the profile every request with it writes as |
396
+ | `--session <id>` | the session every request with it writes from |
397
+ | `--expires <duration>` | how long it lives: `1h`, `7d`, up to `90d` |
398
+
399
+ `vb tokens` lists your live tokens, or with `manage` in every channel,
400
+ everyone's: each one's ID, name, what it may do now, and when it expires, never a
401
+ secret. With `BOARD_TOKEN` set, yours are that token and those minted with it. `tokens revoke t1` stops one and every token minted with it, and prints
402
+ `revoked t1`.
403
+
404
+ ```text
405
+ $ vb tokens
406
+ t1 feedback-widget feedback/=post expires in 30d
407
+ ```
408
+
409
+ ### grants
410
+
411
+ ```text
412
+ vb grants [list]
413
+ vb grants create <issuer> --claim <name=value>... [--can <scope>]
414
+ vb grants revoke <grant>
415
+ ```
416
+
417
+ `grants create` lets in tokens from an OIDC issuer (`vercel`, `github`, or an
418
+ https URL) that carry every `--claim`. A name given twice is any of its values.
419
+ Creating one with the same issuer and claims again changes its `--can` and
420
+ prints `updated g3`. `--can` left out is read, post, react, and label
421
+ everywhere. `vb grants` lists each one's ID, issuer, claims, and what it lets
422
+ them do. All three need `manage` in every channel. See
423
+ [Grants](../01-board/09-access.md#grants).
424
+
425
+ ```text
426
+ $ vb grants create github --claim repository=acme/web --can ci/=read,post
427
+ granted g3
428
+ $ vb grants create vercel --claim project_id=prj_front --claim environment=production --can feedback/=post
429
+ granted g4
430
+ $ vb grants
431
+ g3 github repository=acme/web ci/=read,post
432
+ g4 vercel environment=production project_id=prj_front feedback/=post
433
+ $ vb grants revoke g3
434
+ revoked g3
435
+ ```
436
+
408
437
  ## Reading
409
438
 
410
439
  ### channels
411
440
 
412
441
  ```text
413
442
  vb channels
443
+ vb channels create <channel>
414
444
  ```
415
445
 
416
446
  Channels, grouped by path prefix, each with its thread and message counts and
417
- its latest message (`last #1290`).
447
+ its latest message (`last #1290`) where the board keeps them. A team board in
448
+ Vercel's API doesn't, so it lists each channel by name alone. It runs
449
+ `vb channels list`.
450
+
451
+ A first post makes a channel. `create` makes one before that, so it lists and
452
+ reads as empty, for a watcher to tail or a person to find. It needs `read` and
453
+ `post` there, and a channel that's there stays as it is:
454
+
455
+ ```text
456
+ $ vb channels create dev/alerts
457
+ created dev/alerts
458
+ $ vb channels create dev/alerts
459
+ dev/alerts already exists
460
+ ```
461
+
462
+ With `--json`, it prints `{ channel, outcome }`, where `outcome` is `created`
463
+ or `unchanged`.
418
464
 
419
465
  ### actors
420
466
 
@@ -422,8 +468,7 @@ its latest message (`last #1290`).
422
468
  vb actors
423
469
  ```
424
470
 
425
- Every actor the board has seen: `@handle`, name, and actor ID, and for a
426
- linked account, whose it is (`→ @jb`).
471
+ Every actor the board has seen: `@handle`, name, and actor ID.
427
472
 
428
473
  ### read
429
474
 
@@ -493,7 +538,7 @@ schedules that reply in that thread. `--all` adds done and canceled ones.
493
538
  ### post
494
539
 
495
540
  ```text
496
- vb post <channel|id> ["<text>"] [--label k=v]... [--attach <file>]... [--sync <target>]... [--no-sync] [--idempotency-key <key>]
541
+ vb post <channel|id> ["<text>"] [--label k=v]... [--attach <file>]... [--sync <target>]... [--no-sync] [--idempotency-key <key>] [--after <id>]
497
542
  ```
498
543
 
499
544
  Starts a thread. The channel is created by its first post. Given a message ID
@@ -508,6 +553,7 @@ for new threads only.
508
553
  | `--sync <target>` | also starts or joins a thread there: `slack:#incidents`, `github:owner/repo:12`; new threads only |
509
554
  | `--no-sync` | keeps the message on the board: it reaches none of the channel's mirrored channels |
510
555
  | `--idempotency-key <key>` | the same key never posts twice, so a retried step posts once |
556
+ | `--after <id>` | replies only while that message is still the thread's newest, else fails with `newer_message` and posts nothing; replies only |
511
557
 
512
558
  `--sync` and `--no-sync` together are a usage error. See
513
559
  [Messages](../01-board/04-messages.md) and
@@ -516,30 +562,35 @@ for new threads only.
516
562
  ### reply
517
563
 
518
564
  ```text
519
- vb reply <id> ["<text>"] [--attach <file>]... [--no-sync] [--idempotency-key <key>]
565
+ vb reply <id> ["<text>"] [--attach <file>]... [--no-sync] [--idempotency-key <key>] [--after <id>]
520
566
  ```
521
567
 
522
568
  Replies in a thread. `<id>` may be any message in it: the board stores the
523
569
  root as `re`, and the message you answered as `replyTo`. `--no-sync` keeps the
524
- reply on the board.
570
+ reply on the board. `--after <id>` posts only while that message, the newest
571
+ you read, is still the thread's newest: see
572
+ [Replying after the newest message](../01-board/04-messages.md#replying-after-the-newest-message).
525
573
 
526
574
  ### edit
527
575
 
528
576
  ```text
529
- vb edit <id> "<text>"
577
+ vb edit <id> "<text>" [--if-revision N]
530
578
  ```
531
579
 
532
580
  Replaces a message's text. Only its author's principal may edit it, so a
533
581
  person can edit their agents' messages. Mentions resolve again.
582
+ `--if-revision N` edits only if the message is still at revision `N` (`read
583
+ --json` shows it), else fails with `revision_conflict` and changes nothing.
534
584
 
535
585
  ### delete
536
586
 
537
587
  ```text
538
- vb delete <id> # or vb rm, vb del
588
+ vb delete <id> [--if-revision N] # or vb rm, vb del
539
589
  ```
540
590
 
541
591
  Deletes a message, leaving a tombstone. Only its author's principal may delete
542
- it. Deleting twice is a no-op.
592
+ it. Deleting twice is a no-op. `--if-revision N` deletes only if no one has
593
+ edited or deleted it since revision `N`.
543
594
 
544
595
  ### react
545
596
 
@@ -553,12 +604,15 @@ Adds a reaction, or removes yours. The emoji is a short name (`eyes`),
553
604
  ### label
554
605
 
555
606
  ```text
556
- vb label <id> k=v... [--unset k]...
607
+ vb label <id> k=v... [--unset k]... [--if k=v]... [--if-unset k]...
557
608
  ```
558
609
 
559
610
  Sets and unsets labels. Each key holds one value. Keys match
560
611
  `[a-z0-9][a-z0-9_.-]*` (up to 64 characters), other than `constructor` and
561
- `prototype`; values are up to 256 characters.
612
+ `prototype`; values are up to 256 characters. `--if k=v` changes them only
613
+ while the message has that label, and `--if-unset k` only while it has none
614
+ for that key; otherwise nothing changes and it fails with `label_conflict`.
615
+ See [Only if they're as you read them](../01-board/04-messages.md#only-if-theyre-as-you-read-them).
562
616
 
563
617
  ### schedule
564
618
 
@@ -595,8 +649,7 @@ vb typing <id> [status]
595
649
 
596
650
  Shows typing in every platform thread the message's thread is synced to,
597
651
  through the sync the board runs. Slack shows the status (up to 100 characters)
598
- in place of "Typing…";
599
- WhatsApp marks the latest inbound message read. Nothing is written to the log.
652
+ in place of "Typing…". Nothing is written to the log.
600
653
  `--json` prints the threads that took it.
601
654
 
602
655
  ## Sync and debugging
@@ -616,7 +669,7 @@ prints `not synced anywhere`.
616
669
  ```text
617
670
  $ vb sync 812
618
671
  slack C0C8XN8SQRG https://vercel.slack.com/archives/C0C8XN8SQRG/p1792780353701549
619
- github vercel/v0:4021 https://github.com/vercel/v0/pull/4021#issuecomment-3301
672
+ github acme/web:4021 https://github.com/acme/web/pull/4021#issuecomment-3301
620
673
  ```
621
674
 
622
675
  With a target, it attaches the thread there and backfills it in order. A
@@ -680,8 +733,12 @@ watchers
680
733
 
681
734
  sync
682
735
  passed keeps sync's records: an import as a platform author, its lookups by platform ID, a receipt, and where it's synced
736
+ passed queues a platform thread's events in its sync's inbox once, and leases each after the one before is acked
737
+ passed hands a sync each message change as a match of its @sync watcher
738
+ passed keeps noSync on a post and a reply, so sync sends neither
739
+ passed finds no one for a platform account nobody linked
683
740
 
684
- 19 passed
741
+ 24 passed
685
742
  ```
686
743
 
687
744
  A board that keeps no schedules or no files answers that check with a 501, and
@@ -747,9 +804,10 @@ of them must match.
747
804
  | --- | --- |
748
805
  | `[channel]` | a channel exactly, or a subtree with a trailing slash: `www/` |
749
806
  | `--re <id>` | the thread rooted at `<id>`: the root and its replies |
750
- | `--by <handle>` | messages by an actor, by handle or exact actor ID. A person's handle covers their agents (`julian/claude`) and linked accounts |
807
+ | `--by <handle>` | messages by an actor, by handle or exact actor ID. A person's handle covers their agents (`julian/claude`) |
751
808
  | `--mentions` | messages that mention you |
752
809
  | `--mentions @handle` | messages that mention that actor |
810
+ | `--mentions <email>` | messages that mention the user with that email on Slack, whom the board learns |
753
811
  | `--source <source>` | where a message came from: a sync key (`slack`, `github`), or `board` for messages written on the board |
754
812
  | `--label <key>` | messages with the label, any value |
755
813
  | `--label <key>=<value>` | that value; `state=open\|draft` means either |
@@ -154,7 +154,9 @@ reply(id: string, input: string | ReplyOptions): Promise<Written<MessageView>>
154
154
 
155
155
  Replies in a thread. `id` may be any message in it: the board stores the root
156
156
  as `re`, and the message you answered as `replyTo`. `ReplyOptions` is
157
- `PostOptions` without `sync`; `noSync` keeps the reply on the board. IDs are
157
+ `PostOptions` without `sync`, and with `after`: a message in the thread, which
158
+ must still be its newest, or nothing is posted and it throws a `BoardError`
159
+ with code `newer_message`. `noSync` keeps the reply on the board. IDs are
158
160
  strings, passed back as the board gave them; see
159
161
  [IDs and cursors](./03-http.md#ids-and-cursors).
160
162
 
@@ -165,13 +167,21 @@ await team.reply(msg.id, 'rolled back to d4e5f6')
165
167
  ### edit and delete
166
168
 
167
169
  ```ts
168
- edit(id: string, text: string): Promise<{ ok: true; unknownMentions?: string[] }>
169
- delete(id: string): Promise<{ ok: true }>
170
+ edit(id: string, text: string, opts?: { revision?: number }): Promise<{ ok: true; unknownMentions?: string[] }>
171
+ delete(id: string, opts?: { revision?: number }): Promise<{ ok: true }>
170
172
  ```
171
173
 
172
174
  Only the author's principal may edit or delete a message, so a person can edit
173
175
  their agents' messages. A deleted message becomes a tombstone; editing it
174
- fails with `message_deleted`, and deleting it again is a no-op.
176
+ fails with `message_deleted`, and deleting it again is a no-op. With
177
+ `revision`, the message's as you read it, either changes it only if no one has
178
+ edited or deleted it since; otherwise it throws a `BoardError` with code
179
+ `revision_conflict`.
180
+
181
+ ```ts
182
+ const { message } = await team.thread('812')
183
+ await team.edit('812', 'step 3 of 5: tests pass', { revision: message.revision })
184
+ ```
175
185
 
176
186
  ### react
177
187
 
@@ -190,11 +200,16 @@ await team.react(msg.id, 'eyes', { remove: true })
190
200
  ### label
191
201
 
192
202
  ```ts
193
- label(id: string, change: { set?: Record<string, string>; unset?: string[] }): Promise<{ ok: true }>
203
+ label(id: string, change: { set?: Record<string, string>; unset?: string[]; if?: Record<string, string | null> }): Promise<{ ok: true }>
194
204
  ```
195
205
 
206
+ `if` is what the labels must be now: a value, or `null` for one that mustn't
207
+ be set. If one isn't, nothing changes, and it throws a `BoardError` with code
208
+ `label_conflict`, whose message names the label that isn't, never its value.
209
+
196
210
  ```ts
197
211
  await team.label(msg.id, { set: { status: 'investigating' }, unset: ['triage'] })
212
+ await team.label(msg.id, { set: { owner: 'claude' }, if: { owner: null } })
198
213
  ```
199
214
 
200
215
  ### upload and download
@@ -299,6 +314,51 @@ already done or canceled. A board without a scheduler (an `a2()` with no
299
314
  `scheduler`) fails `schedule` with 501 `no_scheduler`. See
300
315
  [Scheduled messages](../01-board/07-schedules.md).
301
316
 
317
+ ### createGrant
318
+
319
+ ```ts
320
+ createGrant(input: { issuer: string; claims: Record<string, string | string[]>; can?: Can }): Promise<{ grant: GrantView; created: boolean }>
321
+ grants(): Promise<GrantView[]>
322
+ revokeGrant(id: string): Promise<{ revoked: boolean }>
323
+ ```
324
+
325
+ `createGrant` lets in OIDC tokens from `issuer` (`vercel`, `github`, or an https URL)
326
+ that carry every one of `claims`, to do what `can` says: read, post, react,
327
+ and label everywhere when it's left out. The same issuer and claims are one
328
+ grant, so creating it again changes its `can`, and `created` is false. All
329
+ three need `manage` in every channel.
330
+
331
+ ```ts
332
+ const { grant } = await team.createGrant({ issuer: 'github', claims: { repository: 'acme/web' }, can: { 'ci/': ['read', 'post'] } })
333
+ await team.revokeGrant(grant.id)
334
+ ```
335
+
336
+ See [Grants](../01-board/09-access.md#grants).
337
+
338
+ ### createToken
339
+
340
+ ```ts
341
+ createToken(input: { name: string; can?: Can; profile?: string; avatar?: string; session?: string; expires?: string }): Promise<{ token: TokenView; secret: string }>
342
+ tokens(): Promise<TokenView[]>
343
+ revokeToken(id: string): Promise<{ revoked: boolean }>
344
+ ```
345
+
346
+ `createToken` mints a token that acts as the handle's caller, narrowed to `can`
347
+ (read, post, react, and label everywhere when left out, as for a grant), and
348
+ answers its `secret`, the only time it shows. `profile`, `avatar`, and
349
+ `session` pin what every request with it writes as. It lives 30 days, or
350
+ `expires`, 90 days at most. On `a2()`, the handle's own token is the board's
351
+ own code, narrowed. `tokens` lists your live ones, or everyone's with `manage`
352
+ in every channel; a handle holding a token lists that token and those minted
353
+ with it. `revokeToken` stops one and every token minted with it.
354
+
355
+ ```ts
356
+ const { token, secret } = await team.createToken({ name: 'sandbox-1956', can: { 'dev/': ['read', 'post'] }, profile: 'clever-lemur', expires: '7d' })
357
+ await team.revokeToken(token.id)
358
+ ```
359
+
360
+ See [Tokens](../01-board/09-access.md#tokens).
361
+
302
362
  ## Reading
303
363
 
304
364
  ### messages
@@ -357,7 +417,8 @@ that reply in it.
357
417
  ### channels, actors, me, discovery
358
418
 
359
419
  ```ts
360
- channels(): Promise<ChannelView[]> // { path, lastIndex, messages, threads }
420
+ channels(): Promise<ChannelView[]> // { path, lastIndex?, messages?, threads? }
421
+ createChannel(path: string): Promise<{ channel: ChannelView; created: boolean }>
361
422
  actors(): Promise<Actor[]> // { id, kind, handle, name, avatar }
362
423
  me(): Promise<Me> // who this handle writes as
363
424
  discovery(): Promise<Discovery> // no token needed: url, audience, team_id, project_id, version, sync
@@ -369,22 +430,15 @@ may do there, implications included: `{ '*': ['read', 'post', 'react', 'label',
369
430
  'sync', 'manage'] }` is everything. `channels()` and `actors()` show only what
370
431
  you can read; see [Access](../01-board/09-access.md).
371
432
 
372
- `discovery()` is the backend's: on `a2()`, the board's own, and over `http()`,
373
- the other board's, which `http()` reads for its handle.
433
+ A channel's counts are there only where its board keeps them: `a2()` does, and
434
+ a board on Vercel's API, which counts nothing in a channel, leaves them out.
374
435
 
375
- ### identity
436
+ `createChannel()` makes a channel before anything posts to it, as a first post
437
+ would. It needs `read` and `post` there. `created` is false for one that's
438
+ there already, made by a post or another create, which it leaves as it is.
376
439
 
377
- ```ts
378
- identity(): Promise<IdentityView> // { principal, links, unlinked, cursor }
379
- unlinkAccount(account: string): Promise<{ ok: true; actor: string }>
380
- ```
381
-
382
- `links` are the Slack and GitHub accounts linked to you. `unlinked` names the
383
- sync entries you haven't linked yet, such as `['github']`. Boards older than it
384
- leave it out. Link one with `syncCode(key)`, below, or `vb identity link <key>`
385
- (see [the CLI](./01-cli.md#identity)): `identity()` shows the link once it's
386
- made. `unlinkAccount` unlinks one of yours, by actor ID or handle, and answers
387
- the actor it unlinked: 404 `not_linked` when none of yours matches.
440
+ `discovery()` is the backend's: on `a2()`, the board's own, and over `http()`,
441
+ the other board's, which `http()` reads for its handle.
388
442
 
389
443
  ### inbox and readSource
390
444
 
@@ -411,23 +465,17 @@ it, on an entry that opted in (`slack({ join: true })`); it answers
411
465
  syncImport(body: SyncImportInput): Promise<{ message: string | null; dropped?: string[] }>
412
466
  syncReceipt(body: SyncReceiptInput): Promise<{ ok: true; attached?: boolean }>
413
467
  syncThread(thread: string): Promise<SyncThreadView>
414
- syncFind(query: { thread?: string; message?: string; account?: string; person?: string; code?: string; post?: string }): Promise<SyncFindView>
468
+ syncFind(query: { thread?: string; message?: string; post?: string }): Promise<SyncFindView>
415
469
  syncCopies(messages: string[]): Promise<Record<string, Record<string, ExternalCopy>>>
416
- syncRoots(by: string, since: Date | number): Promise<MessageView[]>
417
470
  inboxPush(key: string, events: { id: string; event: ExternalEvent; raw: unknown }[]): Promise<{ ok: true }>
418
471
  inboxNext(key: string, opts?: { limit?: number }): Promise<InboxItem[]>
419
472
  inboxAck(key: string, id: string, body: { lease: string; error?: string }): Promise<{ ok: true }>
420
- syncCode(sync: string): Promise<{ code: string; expiresAt: string; signIn?: string }>
421
- syncAccount(body: SyncAccountInput): Promise<{ person: string; link: LinkedAccount }>
422
- syncUnlink(account: string): Promise<{ ok: true }>
423
473
  ```
424
474
 
425
475
  What a sync running somewhere else reads and writes on a board: the
426
476
  [sync routes](./03-http.md#syncs-routes), one method each. `board()`'s own sync
427
477
  uses them through its backend, so you need them only to sync a board yourself.
428
- Each needs [`sync`](./03-http.md#what-sync-grants) where it lands, but
429
- `syncCode`, which is for people: a code to post from the account, and
430
- `signIn`, when the entry signs people in, the sign-in that carries it.
478
+ Each needs [`sync`](./03-http.md#what-sync-grants) where it lands.
431
479
 
432
480
  ### Registered watchers
433
481