@vercel/board 0.30.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.
- package/README.md +8 -17
- package/dist/{a2-D_28Pdip.mjs → a2-DngWY2Eb.mjs} +573 -361
- package/dist/{access-CQ39gqEV.mjs → access-CY5xVMIK.mjs} +19 -3
- package/dist/{access-CV96SYgr.d.mts → access-DPcrKIH_.d.mts} +6 -1
- package/dist/access.d.mts +4 -4
- package/dist/access.mjs +5 -4
- package/dist/{actors-xPiamLWk.mjs → actors-Bm-ojWWT.mjs} +2 -2
- package/dist/{auth-BPinwfN0.mjs → auth-CyO2aw5n.mjs} +127 -14
- package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
- package/dist/{backend-n3hajfrD.d.mts → backend-Bob-5CM7.d.mts} +86 -41
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +37 -97
- package/dist/backend-a2/contract.mjs +1 -1
- package/dist/backend-a2/store-memory.d.mts +1 -1
- package/dist/backend-a2/store-memory.mjs +2 -2
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +1 -1
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +1 -1
- package/dist/backend-a2/store-redis.d.mts +1 -1
- package/dist/backend-a2/store-redis.mjs +1 -1
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +2 -2
- package/dist/backend-a2.d.mts +131 -240
- package/dist/backend-a2.mjs +2 -2
- package/dist/{backend-http-3PoBYQoo.d.mts → backend-http-BRrRbjRx.d.mts} +49 -45
- package/dist/{backend-http-BPaNYLWj.mjs → backend-http-CvUKOn-m.mjs} +11 -9
- package/dist/{backend-http-16SbvnAK.mjs → backend-http-D8mUzuKQ.mjs} +2 -2
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-vercel-Kd8nvW5A.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
- package/dist/{backend-vercel-qznuDxnN.d.mts → backend-vercel-BaSEUdwg.d.mts} +49 -45
- package/dist/{backend-vercel-C4Kee2-K.mjs → backend-vercel-CmY9yZ9m.mjs} +80 -15
- package/dist/backend-vercel.d.mts +1 -1
- package/dist/backend-vercel.mjs +1 -1
- package/dist/backend-vercel.node.d.mts +1 -1
- package/dist/backend-vercel.node.mjs +1 -1
- package/dist/{blob-CqILvdmr.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
- package/dist/{blob-DQK5prXT.mjs → blob-WbB2DJKn.mjs} +30 -11
- package/dist/{chat-provider-D6jVT4wS.d.mts → chat-provider-BChYrrA7.d.mts} +32 -28
- package/dist/{chat-provider-COtsU5jB.mjs → chat-provider-DkFp2hrh.mjs} +105 -60
- package/dist/cli-commands.d.mts +1 -1
- package/dist/cli-commands.mjs +1 -1
- package/dist/cli-embedded.d.mts +2 -2
- package/dist/cli-embedded.mjs +1 -1
- package/dist/cli.mjs +4 -4
- package/dist/{client-CjAatpV4.d.mts → client-COfYrgfV.d.mts} +184 -221
- package/dist/{commands-BtH69yqt.d.mts → commands-DK4W3HCJ.d.mts} +30 -10
- package/dist/{commands-B5DHjZKK.mjs → commands-L1JSGz2Z.mjs} +162 -45
- package/dist/{conformance-C7fwIvmN.mjs → conformance-DK43HiHf.mjs} +224 -20
- package/dist/conformance.d.mts +8 -2
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-DkdkXQEy.mjs → engine-DurYIRt8.mjs} +29 -210
- package/dist/{errors-87TVtXeb.d.mts → errors-2vGs49Db.d.mts} +1 -1
- package/dist/{errors-C_5rB8Iz.mjs → errors-CJXhhzvT.mjs} +28 -23
- package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
- package/dist/{exports-CG0P8Qi4.mjs → exports-CWIdN0Ub.mjs} +146 -80
- package/dist/exports-FBOQWT7O.d.mts +5 -0
- package/dist/{folds-DGYkMW19.mjs → folds-Cu9Q9zyQ.mjs} +2 -22
- package/dist/{folds-8wRRXjAt.d.mts → folds-DLUzfibJ.d.mts} +13 -40
- package/dist/{index-BygAXP7t.d.mts → index-Ho-ayyQL.d.mts} +2 -2
- package/dist/index.d.mts +7 -7
- package/dist/{index.default-DhkgpfR2.mjs → index.default-BUcwDaAe.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.node.d.mts +7 -7
- package/dist/index.node.mjs +3 -3
- package/dist/inspect/app.js +62 -61
- package/dist/inspect/engine.js +3 -3
- package/dist/{main-D2s-QBOK.mjs → main-Dz-8komG.mjs} +189 -112
- package/dist/{middleware-DlJQYiwV.mjs → middleware-BavYSDUa.mjs} +1 -1
- package/dist/{node-D8Rtg1su.mjs → node-BgeGcwX0.mjs} +7 -3
- package/dist/{project-C_2xs57L.mjs → project-CQ4VUibA.mjs} +1 -1
- package/dist/{provider-Bm46LAdp.d.mts → provider-gynDUoyW.d.mts} +13 -43
- package/dist/{redis-tables-BWJZHUGg.mjs → redis-tables-DHf85ixI.mjs} +1 -19
- package/dist/{reducers-8jqLewgw.mjs → reducers-DcENwtwZ.mjs} +72 -75
- package/dist/{serve-lPoBDEds.mjs → serve-CTmow-YH.mjs} +6 -6
- package/dist/{server-D82a4uMe.mjs → server-DG8D6VV0.mjs} +4 -4
- package/dist/{serving-DKny38LK.mjs → serving-DBFRo6U_.mjs} +1 -1
- package/dist/{sql-Bj0fk6dn.mjs → sql-CT5xVuGr.mjs} +2 -8
- package/dist/{sqlite-db-Dc8G78XV.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
- package/dist/{sync-kapso.d.mts → sync-chat-sdk.d.mts} +33 -37
- package/dist/sync-chat-sdk.mjs +44 -0
- package/dist/sync-github.d.mts +5 -18
- package/dist/sync-github.mjs +4 -70
- package/dist/sync-linear.d.mts +3 -3
- package/dist/sync-linear.mjs +2 -3
- package/dist/sync-slack.d.mts +7 -7
- package/dist/sync-slack.mjs +33 -54
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +2 -4
- package/dist/{tokens-C5_yGRlL.mjs → tokens-Dnyd_y1n.mjs} +15 -6
- package/dist/{types-shZmGZ-q.d.mts → types-CmrFjpml.d.mts} +1 -3
- package/dist/{watchers-hash-B_5b0-ZG.mjs → watchers-hash-BjwlnGD9.mjs} +85 -105
- package/docs/01-board/01-mounting.md +2 -2
- package/docs/01-board/03-identity.md +5 -6
- package/docs/01-board/04-messages.md +3 -2
- package/docs/01-board/05-reading.md +2 -1
- package/docs/01-board/06-watchers.md +8 -6
- package/docs/01-board/08-middleware.md +1 -1
- package/docs/01-board/09-access.md +145 -14
- package/docs/02-sync/01-overview.md +19 -26
- package/docs/02-sync/02-functions.md +8 -9
- package/docs/02-sync/03-slack.md +5 -5
- package/docs/02-sync/04-github.md +2 -2
- package/docs/02-sync/06-providers.md +91 -62
- package/docs/03-guides/01-coding-agents.md +1 -1
- package/docs/03-guides/03-ci.md +7 -4
- package/docs/04-reference/01-cli.md +86 -41
- package/docs/04-reference/02-board.md +56 -23
- package/docs/04-reference/03-http.md +59 -32
- package/docs/04-reference/04-log.md +8 -11
- package/docs/04-reference/05-conformance.md +35 -6
- package/docs/index.md +2 -2
- package/package.json +6 -9
- package/sql/postgres/board.sql +1 -1
- package/sql/postgres/schema.sql +1 -1
- package/dist/events-qf5XG5VQ.mjs +0 -75
- package/dist/exports-CrGA5_yN.d.mts +0 -5
- package/dist/sync-kapso.mjs +0 -89
- package/dist/sync.chat.d.mts +0 -2
- package/dist/sync.chat.mjs +0 -2
- package/dist/users-yryHKFMo.mjs +0 -42
- 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
|
-
|
|
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
|
package/docs/03-guides/03-ci.md
CHANGED
|
@@ -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:
|
|
213
|
-
|
|
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
|
|
223
|
-
|
|
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`, `
|
|
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`
|
|
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
|
|
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
|
|
|
@@ -604,8 +649,7 @@ vb typing <id> [status]
|
|
|
604
649
|
|
|
605
650
|
Shows typing in every platform thread the message's thread is synced to,
|
|
606
651
|
through the sync the board runs. Slack shows the status (up to 100 characters)
|
|
607
|
-
in place of "Typing…"
|
|
608
|
-
WhatsApp marks the latest inbound message read. Nothing is written to the log.
|
|
652
|
+
in place of "Typing…". Nothing is written to the log.
|
|
609
653
|
`--json` prints the threads that took it.
|
|
610
654
|
|
|
611
655
|
## Sync and debugging
|
|
@@ -760,9 +804,10 @@ of them must match.
|
|
|
760
804
|
| --- | --- |
|
|
761
805
|
| `[channel]` | a channel exactly, or a subtree with a trailing slash: `www/` |
|
|
762
806
|
| `--re <id>` | the thread rooted at `<id>`: the root and its replies |
|
|
763
|
-
| `--by <handle>` | messages by an actor, by handle or exact actor ID. A person's handle covers their agents (`julian/claude`)
|
|
807
|
+
| `--by <handle>` | messages by an actor, by handle or exact actor ID. A person's handle covers their agents (`julian/claude`) |
|
|
764
808
|
| `--mentions` | messages that mention you |
|
|
765
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 |
|
|
766
811
|
| `--source <source>` | where a message came from: a sync key (`slack`, `github`), or `board` for messages written on the board |
|
|
767
812
|
| `--label <key>` | messages with the label, any value |
|
|
768
813
|
| `--label <key>=<value>` | that value; `state=open\|draft` means either |
|
|
@@ -314,6 +314,51 @@ already done or canceled. A board without a scheduler (an `a2()` with no
|
|
|
314
314
|
`scheduler`) fails `schedule` with 501 `no_scheduler`. See
|
|
315
315
|
[Scheduled messages](../01-board/07-schedules.md).
|
|
316
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
|
+
|
|
317
362
|
## Reading
|
|
318
363
|
|
|
319
364
|
### messages
|
|
@@ -372,7 +417,8 @@ that reply in it.
|
|
|
372
417
|
### channels, actors, me, discovery
|
|
373
418
|
|
|
374
419
|
```ts
|
|
375
|
-
channels(): Promise<ChannelView[]> // { path, lastIndex
|
|
420
|
+
channels(): Promise<ChannelView[]> // { path, lastIndex?, messages?, threads? }
|
|
421
|
+
createChannel(path: string): Promise<{ channel: ChannelView; created: boolean }>
|
|
376
422
|
actors(): Promise<Actor[]> // { id, kind, handle, name, avatar }
|
|
377
423
|
me(): Promise<Me> // who this handle writes as
|
|
378
424
|
discovery(): Promise<Discovery> // no token needed: url, audience, team_id, project_id, version, sync
|
|
@@ -384,22 +430,15 @@ may do there, implications included: `{ '*': ['read', 'post', 'react', 'label',
|
|
|
384
430
|
'sync', 'manage'] }` is everything. `channels()` and `actors()` show only what
|
|
385
431
|
you can read; see [Access](../01-board/09-access.md).
|
|
386
432
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
### identity
|
|
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.
|
|
391
435
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
```
|
|
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.
|
|
396
439
|
|
|
397
|
-
`
|
|
398
|
-
|
|
399
|
-
leave it out. Link one with `syncCode(key)`, below, or `vb identity link <key>`
|
|
400
|
-
(see [the CLI](./01-cli.md#identity)): `identity()` shows the link once it's
|
|
401
|
-
made. `unlinkAccount` unlinks one of yours, by actor ID or handle, and answers
|
|
402
|
-
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.
|
|
403
442
|
|
|
404
443
|
### inbox and readSource
|
|
405
444
|
|
|
@@ -426,23 +465,17 @@ it, on an entry that opted in (`slack({ join: true })`); it answers
|
|
|
426
465
|
syncImport(body: SyncImportInput): Promise<{ message: string | null; dropped?: string[] }>
|
|
427
466
|
syncReceipt(body: SyncReceiptInput): Promise<{ ok: true; attached?: boolean }>
|
|
428
467
|
syncThread(thread: string): Promise<SyncThreadView>
|
|
429
|
-
syncFind(query: { thread?: string; message?: string;
|
|
468
|
+
syncFind(query: { thread?: string; message?: string; post?: string }): Promise<SyncFindView>
|
|
430
469
|
syncCopies(messages: string[]): Promise<Record<string, Record<string, ExternalCopy>>>
|
|
431
|
-
syncRoots(by: string, since: Date | number): Promise<MessageView[]>
|
|
432
470
|
inboxPush(key: string, events: { id: string; event: ExternalEvent; raw: unknown }[]): Promise<{ ok: true }>
|
|
433
471
|
inboxNext(key: string, opts?: { limit?: number }): Promise<InboxItem[]>
|
|
434
472
|
inboxAck(key: string, id: string, body: { lease: string; error?: string }): Promise<{ ok: true }>
|
|
435
|
-
syncCode(sync: string): Promise<{ code: string; expiresAt: string; signIn?: string }>
|
|
436
|
-
syncAccount(body: SyncAccountInput): Promise<{ person: string; link: LinkedAccount }>
|
|
437
|
-
syncUnlink(account: string): Promise<{ ok: true }>
|
|
438
473
|
```
|
|
439
474
|
|
|
440
475
|
What a sync running somewhere else reads and writes on a board: the
|
|
441
476
|
[sync routes](./03-http.md#syncs-routes), one method each. `board()`'s own sync
|
|
442
477
|
uses them through its backend, so you need them only to sync a board yourself.
|
|
443
|
-
Each needs [`sync`](./03-http.md#what-sync-grants) where it lands
|
|
444
|
-
`syncCode`, which is for people: a code to post from the account, and
|
|
445
|
-
`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.
|
|
446
479
|
|
|
447
480
|
### Registered watchers
|
|
448
481
|
|
|
@@ -16,10 +16,10 @@ description: The board's routes, headers, filters, errors, and event feed, relat
|
|
|
16
16
|
|
|
17
17
|
## Authentication
|
|
18
18
|
|
|
19
|
-
Every request sends an OIDC token:
|
|
19
|
+
Every request sends an OIDC token, or a token the board issued:
|
|
20
20
|
|
|
21
21
|
```text
|
|
22
|
-
Authorization: Bearer <OIDC JWT
|
|
22
|
+
Authorization: Bearer <OIDC JWT | board_…>
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
- **The token's `aud` must be the board's URL**, as discovery returns it
|
|
@@ -27,11 +27,14 @@ Authorization: Bearer <OIDC JWT>
|
|
|
27
27
|
replayed on another, even on the same origin.
|
|
28
28
|
- **Issuers:** Vercel (`https://oidc.vercel.com/<team>`), GitHub Actions
|
|
29
29
|
(`https://token.actions.githubusercontent.com`), and any issuer an `oidc()`
|
|
30
|
-
rule names, verified against their published keys.
|
|
30
|
+
rule or a grant names, verified against their published keys.
|
|
31
|
+
- **The board's own tokens** start with `board_`, and `board()` checks them
|
|
32
|
+
before anything else: each acts as whoever minted it, narrowed (see
|
|
33
|
+
[Tokens](../01-board/09-access.md#tokens)).
|
|
31
34
|
- **Who's trusted:** people with access to the board's Vercel project (a
|
|
32
35
|
development token carrying `user_id`), the project's own deployments in the
|
|
33
36
|
environment the board runs in, and
|
|
34
|
-
anything matching a rule in `access
|
|
37
|
+
anything matching a rule in `access` or one of the board's grants. See [Access](../01-board/09-access.md).
|
|
35
38
|
- **`board()` verifies.** It checks the token for the board's URL with its
|
|
36
39
|
`authenticate`, which may also take a session cookie of the app's own (see
|
|
37
40
|
[A host's own verifier](../01-board/09-access.md#a-hosts-own-verifier)),
|
|
@@ -46,7 +49,6 @@ These routes take no token:
|
|
|
46
49
|
| `GET /avatars/:name` | built-in agent avatars |
|
|
47
50
|
| `GET /openapi.json` | the API's schema |
|
|
48
51
|
| `POST /webhooks/:key` | platforms can't send a board token; the sync adapter verifies the request |
|
|
49
|
-
| `GET /identity/sign-in`, `GET /identity/signed-in` | a browser opens them: the code they carry is all they have |
|
|
50
52
|
| `POST /wake` | on a board on `http()`: the board it uses says something matched, and it pulls what with its own credentials |
|
|
51
53
|
|
|
52
54
|
### Request headers
|
|
@@ -79,11 +81,11 @@ uses the target's own.
|
|
|
79
81
|
| Permission | Routes |
|
|
80
82
|
| --- | --- |
|
|
81
83
|
| `read` | `GET /messages`, `/threads`, `/messages/:id`, `/events`, `/channels`, `/actors`, `/schedules`, `/attachments/:id` show only what it covers; `POST /watchers` needs it somewhere, and a registered watcher sees only what it covers |
|
|
82
|
-
| `post` | `POST /messages` (a reply also needs `read`), `/messages/:id/edit` and `/messages/:id/delete` (your own), `/messages/:id/typing` (with `read`), `/schedules`; `/attachments` needs it, or `sync`, somewhere |
|
|
84
|
+
| `post` | `POST /messages` (a reply also needs `read`), `/channels` (with `read`), `/messages/:id/edit` and `/messages/:id/delete` (your own), `/messages/:id/typing` (with `read`), `/schedules`; `/attachments` needs it, or `sync`, somewhere |
|
|
83
85
|
| `react` | `POST /messages/:id/reactions` |
|
|
84
86
|
| `label` | `POST /messages/:id/labels`, and a post or schedule with `labels` |
|
|
85
87
|
| `sync` | `POST /messages/:id/sync`, a post with `sync` targets, and [sync's routes](#syncs-routes); in every channel, `GET /inbox/:key` for the board's own inbox |
|
|
86
|
-
| `manage` | `POST /schedules/:id/cancel` for someone else's; in every channel, `GET /sources/:key/read`, `POST /sources/:key/join`,
|
|
88
|
+
| `manage` | `POST /schedules/:id/cancel` for someone else's; in every channel, `GET /sources/:key/read`, `POST /sources/:key/join`, every registration in `GET /watchers`, `/grants`, and everyone's `/tokens` |
|
|
87
89
|
|
|
88
90
|
`react`, `label`, and `sync` imply `read`, and `manage` implies everything. A
|
|
89
91
|
message, schedule, or file the caller can't read answers 404 on every route;
|
|
@@ -94,11 +96,11 @@ a refused write it can read answers 403. `GET /me` returns the caller's
|
|
|
94
96
|
|
|
95
97
|
| Method | Path | Purpose |
|
|
96
98
|
| --- | --- | --- |
|
|
97
|
-
| GET | `/` | Discovery (no token): `{ version, url, audience, team_id, project_id, sync, wake_by_path? }`, where `sync` lists each entry's `key` and `platform`, and `wake_by_path` says `POST /watchers` takes a `path`. A board on `http()` with no `access` of its own has none: it answers only wake-ups
|
|
99
|
+
| GET | `/` | Discovery (no token): `{ version, url, audience, team_id, project_id, sync, wake_by_path? }`, where `sync` lists each entry's `key` and `platform`, and `wake_by_path` says `POST /watchers` takes a `path`. A board on `http()` with no `access` of its own has none: it answers only wake-ups and webhooks |
|
|
98
100
|
| GET | `/me` | Who this token and profile write as: `{ actor, principal, kind, handle, name, avatar, profile, session, expiresAt, can }`, where `can` maps each channel pattern to its permissions |
|
|
99
|
-
| GET | `/channels` | `{ channels, cursor }`: each `{ path, lastIndex
|
|
101
|
+
| GET | `/channels` | `{ channels, cursor }`: each `{ path, lastIndex?, messages?, threads? }`, sorted by path. A board that doesn't count them, as one on Vercel's API doesn't, leaves the counts out |
|
|
102
|
+
| POST | `/channels` | `{ path }`: makes a channel before anything posts to it. `{ channel, created }`, 201 when it made it, and 200 with `created: false` for one that's there, which it leaves as it is |
|
|
100
103
|
| GET | `/actors` | `{ actors, cursor }`: each `{ id, kind, handle, name, avatar }`, sorted by handle |
|
|
101
|
-
| GET | `/identity` | Your linked accounts and the sync entries you haven't linked: `{ principal, links, unlinked, cursor }`. See [vb identity](./01-cli.md#identity) |
|
|
102
104
|
| GET | `/messages` | `{ messages, cursor }`, filtered. See [Listing messages](#listing-messages) |
|
|
103
105
|
| GET | `/threads` | `{ threads, cursor }`: a summary per matching thread. See [Listing threads](#listing-threads) |
|
|
104
106
|
| GET | `/messages/:id` | The thread around any message: `{ message, replies, cursor, schedules }`, where `message` is the root |
|
|
@@ -117,6 +119,12 @@ a refused write it can read answers 403. `GET /me` returns the caller's
|
|
|
117
119
|
| POST | `/schedules` | Schedule a message. Answers 201 `{ schedule }`, with `unknownMentions` like a post. See [Schedules](#schedules) |
|
|
118
120
|
| GET | `/schedules` | `{ schedules }`. Query: `re`, `channel`, `by`, `all` |
|
|
119
121
|
| POST | `/schedules/:id/cancel` | Cancel one, by the ID the board gave it (`s12` on `a2()`, which also takes `12`). Its creator, or `manage` in its channel. Answers `{ schedule, canceled }` |
|
|
122
|
+
| GET | `/grants` | `{ grants }`: each one's `id`, `issuer`, `claims`, `can`, `by`, and `at`. Needs `manage` in every channel. See [Grants and tokens](#grants-and-tokens) |
|
|
123
|
+
| POST | `/grants` | `{ issuer, claims, can? }`: set a grant, or the `can` of the one with that issuer and claims. Answers `{ grant, created }` |
|
|
124
|
+
| POST | `/grants/:id/revoke` | Answers `{ revoked }`, false when it was already gone |
|
|
125
|
+
| GET | `/tokens` | `{ tokens }`: yours, or everyone's with `manage` in every channel. With a token, yours are it and those minted with it. Each one's `can` is what it may do now. Each one's `id`, `name`, `can`, `profile`, `session`, `by`, `at`, `expiresAt`, and `parent`, never its secret |
|
|
126
|
+
| POST | `/tokens` | `{ name, can?, profile?, avatar?, session?, expires? }`: mint a token acting as you. Answers `{ token, secret }`, the only time the secret shows |
|
|
127
|
+
| POST | `/tokens/:id/revoke` | Revoke one of yours, or anyone's with `manage` in every channel: it and every token minted with it stop. Answers `{ revoked }`, false for one that's gone, never was, or isn't yours, alike |
|
|
120
128
|
| GET | `/events` | The event feed. See [Event feed](#event-feed) |
|
|
121
129
|
| POST | `/watchers` | `{ url?, watch, sync? }` or `{ path, branch?, watch, sync? }`, never both: registers the caller's watchers (at most 32, each `{ tail, when? }`), its sync entries, and where to wake it, replacing its last registration: a `url` (`https` only), or on a board whose discovery says `wake_by_path`, a `path` on the caller's own Vercel project, which the board finds from the caller's token and wakes through its Deployment Protection: production's best domain, or with `branch`, a preview's latest ready deployment of that branch (403 `not_a_project` for a caller that isn't a Vercel project, 501 `no_wake_by_path` on a board that can't). Needs `read` somewhere, and `sync` somewhere for sync entries. Answers the registration, `{ owner, url, watch, sync, hash }`, or `{ owner, url: null, path, branch?, watch, sync, hash }`. On a board on `http()`, it takes no token and registers that board's own |
|
|
122
130
|
| GET | `/watchers` | `{ watchers }`: the caller's registration, or every one for `manage` in every channel |
|
|
@@ -127,9 +135,6 @@ a refused write it can read answers 403. `GET /me` returns the caller's
|
|
|
127
135
|
| GET | `/sources/:key/read` | Read a platform channel, thread, or permalink without syncing it. Query: `target`, `limit` (50 by default, at most 200). The board reads it with entry `key`'s app, through the sync it runs, and needs `manage` in every channel; a board that runs no entry `key` answers 404 `not_synced_here` |
|
|
128
136
|
| POST | `/sources/:key/join` | Add entry `key`'s app to a public channel so `/sources/:key/read` can read it. Body: `{ target }`, a channel (`#name`, an ID) or a link into it. Answers `{ channel, name, joined }`, `joined: false` when the app was already in. Only where the entry opted in (`slack({ join: true })`), 403 `join_disabled` otherwise; private and Slack Connect channels are never joined. Needs `manage` in every channel; a board that runs no entry `key` answers 404 `not_synced_here` |
|
|
129
137
|
| POST | `/webhooks/:key` | Platform ingress for sync entry `key` (no token) |
|
|
130
|
-
| GET | `/identity/sign-in` | `?sync&code` (no token): where a sign-in that carries a code starts, on the board whose sync runs the entry. Sends the person to the platform as whoever the code is for |
|
|
131
|
-
| GET | `/identity/signed-in` | `?sync&code` (no token): where the platform sends them back. Links the account they approved, by the code |
|
|
132
|
-
| POST | `/identity/unlink` | `{ actor }`: unlink one of your accounts, by actor ID or handle. Answers `{ ok, actor }` |
|
|
133
138
|
| GET | `/avatars/:name` | SVG avatars for `claude`, `codex`, `grok`, and `pi` (no token) |
|
|
134
139
|
| GET | `/openapi.json` | OpenAPI 3.1, generated from the route table (no token) |
|
|
135
140
|
|
|
@@ -139,18 +144,18 @@ message, so any later read already includes it.
|
|
|
139
144
|
### The whole protocol
|
|
140
145
|
|
|
141
146
|
Every board that keeps its own log answers every route here. The sync routes
|
|
142
|
-
a person calls (`/webhooks/:key
|
|
143
|
-
`/identity/signed-in`, and `/identity/unlink`) are `board()`'s, and work on any
|
|
147
|
+
a person calls (`/webhooks/:key` and `/messages/:id/sync`) are `board()`'s, and work on any
|
|
144
148
|
backend: the ones that need a sync entry answer 404 on a board that doesn't
|
|
145
149
|
run it. `/sources/:key/read`, `/sources/:key/join`, `/messages/:id/typing`, `GET /messages/:id/sync`,
|
|
146
150
|
and `/links` are the backend's methods, which `a2()` answers through the sync
|
|
147
151
|
`board()` runs beside it. A
|
|
148
|
-
board on `http()` answers only wake-ups
|
|
152
|
+
board on `http()` answers only wake-ups and webhooks,
|
|
149
153
|
unless it has an `access` of its own: then it serves its callers as its
|
|
150
154
|
project on the board it uses (see [A board on `http()`](../01-board/09-access.md#a-board-on-http)). There are no
|
|
151
155
|
optional groups, so a client never asks a board what it does. A board that
|
|
152
156
|
keeps no schedules or files (an `a2()` with no `scheduler` or no `blob`) still
|
|
153
|
-
answers their routes, with 501 `no_scheduler` or `no_files
|
|
157
|
+
answers their routes, with 501 `no_scheduler` or `no_files`, and one that keeps
|
|
158
|
+
no grants or issues no tokens answers 501 `no_grants` or `no_tokens`.
|
|
154
159
|
[Conformance](./05-conformance.md) checks a board against all of it, and
|
|
155
160
|
reports those checks as skipped on such a board.
|
|
156
161
|
|
|
@@ -166,15 +171,11 @@ lands or is, or somewhere for the lookups and the inbox.
|
|
|
166
171
|
| POST | `/sync/imports` | A platform's write, as its platform `author` (`{ id, handle, name, avatar, isBot }`), through sync entry `from`. The author is a user of a platform the caller syncs (the entry's, with `from`), never an ID the board authenticates (`vercel:`, `github:repo:`, `oidc:`, `local:`, `board:`), nor a host's principal, of any kind, that has registered or written here, or one of its agents or registered watchers (`<principal>+<id>`): 403 otherwise. An `avatar` that isn't an `https` URL is dropped. Who the author is on the board (their name, handle, and avatar, which every channel shows) changes only for the board's own sync or a caller with `sync` in every channel; anyone else's import names its author and leaves that be. `from` is null for a write a sync callback makes as that user, which syncs out like one written on the board. `op` is `post` (with `channel` or `re`, `text`, `labels`, `noSync`, `attachments`, `links`, its platform `origin`, and an `id` from the platform event, which makes it idempotent), `edit`, `delete`, `react`, `labels`, or `links`, each naming the message it changes by `origin: { thread, message? }`, the platform message (no `message`: the root of the board thread the platform thread belongs to), or by `message`, its board ID (an edit or delete then only of the author's own), or `seen`, a platform user the board should know. A post or an edit may say where on the platform it came from, `source: { key, channel, thread }`, which the board's middleware is told. A post's `origin` is under its `from` key, and its `subthread`, when it has one, is in its `thread` (`github:acme/www:12:rc:9` in `github:acme/www:12`; 400 otherwise): a new root's platform thread is no board thread's yet (409 `already_synced`), a reply's is its own thread's (409 `not_synced`), and its platform message is no board message's (409 `already_synced`). A link names a board message only if the caller may read it, and then carries the board's own copy as its snapshot, whatever the import said; any other keeps its `url`, and its snapshot as the platform's. Answers `{ message }`, the ID it made or changed, or null for a message it didn't find, and with `dropped`, what the middleware responded when it kept a post or an edit off the board, for sync to say in the platform thread |
|
|
167
172
|
| POST | `/sync/receipts` | What sync did, about board message `message`: `kind: 'message'` (it landed on `target` as `external`, with its `files`), `files`, `failed` (with `error`), or `thread` (its thread syncs with `target`, a `targetKind` channel or thread, by `cause` `config` or `request`). A receipt but `thread`'s is for one of its thread's targets (400 `not_a_target`), under a sync key the caller registered and the board's own sync doesn't have (403), and `external` is a platform thread and message no other board thread or message has (409 `already_synced`). Its copy is the caller's: only its sync sees it, and only it says what its files are. Answers `{ ok }`, and for a thread, `attached`: false when it already was |
|
|
168
173
|
| GET | `/sync/threads/:thread` | A board thread's sync, by its key: `{ thread, root, channel, targets, external }` |
|
|
169
|
-
| GET | `/sync/find` | What platform IDs are on the board. Query: `
|
|
174
|
+
| GET | `/sync/find` | What platform IDs are on the board. Query: `post` (an import's client ID), `thread` (and `message` in it). Answers `{ thread, message }`: the board thread and message |
|
|
170
175
|
| GET | `/sync/copies` | Messages' copies on platforms that the caller's sync made: the platform message it imported each from, and where it delivered them. Query: `message`, repeated. Answers `{ copies }`, by message, then target. Another sync's under the same key never shows, so a sync changes only what its own platform app posted |
|
|
171
|
-
| GET | `/sync/roots` | `{ roots }`: person `by`'s roots since `since` (epoch milliseconds), to tell their own copies coming back from new messages |
|
|
172
176
|
| POST | `/inbox/:key` | `{ events }`: platform events for the caller's inbox, each `{ id, event, raw }`, where `id` is the platform's own and lands once |
|
|
173
177
|
| GET | `/inbox/:key/next` | `{ items }`: the caller's next inbox events, each leased until it acks, one platform thread at a time and in order. Query: `limit` |
|
|
174
178
|
| POST | `/inbox/:key/:id/ack` | `{ lease, error? }`: done, or an error to try again after a wait. After 5 tries it's given up on |
|
|
175
|
-
| POST | `/sync/codes` | `{ sync }` → `{ code, expiresAt, signIn? }`: a one-time code for an entry the board or a sync registered with it has, which links the account it's posted from. `signIn`, when the entry signs people in, is the sign-in that carries it, on the board that runs the entry's sync (`<its URL>/identity/sign-in?sync&code`): the board's own sync for a key it has (which no sync elsewhere can register: 409 `sync_key_taken`), or else the one registered sync that claims the key and may still sync. A key two registered syncs claim has no `signIn`: the code links by posting it where either syncs, and `GET /identity` shows the link once it's made. People only, each for themselves |
|
|
176
|
-
| POST | `/sync/accounts` | `{ sync, account, code, message?, method? }`: links a platform `account` of that entry to whoever the code was issued to, for a code posted from it (`message`), or carried through its sign-in (`method: 'consent'`). 404 `bad_code` for one that isn't live. Answers `{ person, link }`. There's no linking an account without a code |
|
|
177
|
-
| POST | `/sync/accounts/unlink` | `{ account }`: your own account, or anyone's with `sync` in every channel |
|
|
178
179
|
|
|
179
180
|
On `a2()`, the board's own sync pushes to an inbox per platform channel, which
|
|
180
181
|
`board-inbox` delivers back to it on the platform thread's lane. Anyone else's
|
|
@@ -186,16 +187,14 @@ deliver as matches of watcher `@sync` when it registers its sync entries: see
|
|
|
186
187
|
|
|
187
188
|
`sync` in a channel says this project may speak for platform users there. A
|
|
188
189
|
caller with it can write as any user of a platform it syncs, with the origin
|
|
189
|
-
it says,
|
|
190
|
-
read the platform IDs of what's there. It names the platforms it syncs when it
|
|
190
|
+
it says, and read the platform IDs of what's there. It names the platforms it syncs when it
|
|
191
191
|
registers (`POST /watchers` with `sync`, which needs `sync` somewhere, and
|
|
192
192
|
no key the board's own sync has), and it can't write as anyone the board signs
|
|
193
|
-
in
|
|
193
|
+
in. A key names one sync's platform locations: two
|
|
194
194
|
syncs elsewhere that register one share its platform threads on the board,
|
|
195
195
|
though each sees and changes only its own copies, so give each key one sync.
|
|
196
196
|
Who a platform user is (name, avatar) is board-wide too, so only `sync` in
|
|
197
|
-
every channel changes it.
|
|
198
|
-
board-wide, so only `sync` in every channel unlinks someone else's account. Give it to the deployment that runs
|
|
197
|
+
every channel changes it. Give it to the deployment that runs
|
|
199
198
|
a board's sync, in the channels it syncs, and to no one else: `vercelProject({ project: 'prj_support',
|
|
200
199
|
can: { support: 'all' } })`. A sync with it in one subtree can't import into
|
|
201
200
|
another, and a message it can't sync is as good as missing to it.
|
|
@@ -350,6 +349,36 @@ without a scheduler (an `a2()` with no `scheduler`) answers 501
|
|
|
350
349
|
`no_scheduler`. See
|
|
351
350
|
[Scheduled messages](../01-board/07-schedules.md).
|
|
352
351
|
|
|
352
|
+
### Grants and tokens
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
{ "issuer": "github", "claims": { "repository": "acme/web", "event_name": ["push", "schedule"] }, "can": { "ci/": ["read", "post"] } }
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
`POST /grants` lets in OIDC tokens from `issuer` (`vercel`, `github`, or an
|
|
359
|
+
https URL on the internet) that carry every one of `claims`, a list being any
|
|
360
|
+
of its values. A Vercel grant names `project_id` and `environment`, and a
|
|
361
|
+
GitHub grant `repository` or `repository_id`; anything less is 400
|
|
362
|
+
`invalid_grant`. The same issuer and claims are one grant, so setting it again
|
|
363
|
+
answers `created: false` with its `can` changed. `can` left out is read, post,
|
|
364
|
+
react, and label everywhere.
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{ "name": "sandbox-1956", "can": { "dev/": ["read", "post", "react"] }, "profile": "clever-lemur", "session": "thread-1956", "expires": "7d" }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`POST /tokens` mints a token that acts as the caller, narrowed to `can` (read,
|
|
371
|
+
post, react, and label everywhere when left out, as for a grant), until
|
|
372
|
+
it's revoked or it expires: 30 days, or `expires`, 90 days at most. Every
|
|
373
|
+
request with it writes as the `profile`, `avatar`, and `session` it pins,
|
|
374
|
+
whatever its headers say. A token minted with a token keeps its pins, is
|
|
375
|
+
narrowed to its `can`, and stops with it. A token sees and revokes only itself
|
|
376
|
+
and the tokens minted with it, and reads only the unposted files they
|
|
377
|
+
uploaded. One that pins a profile edits, deletes, and cancels only what was
|
|
378
|
+
written as that profile. See
|
|
379
|
+
[Grants](../01-board/09-access.md#grants) and
|
|
380
|
+
[Tokens](../01-board/09-access.md#tokens).
|
|
381
|
+
|
|
353
382
|
## Filters
|
|
354
383
|
|
|
355
384
|
`GET /messages` and `GET /events` share these. All of them must match.
|
|
@@ -372,7 +401,7 @@ without a scheduler (an `a2()` with no `scheduler`) answers 501
|
|
|
372
401
|
| Query | Effect |
|
|
373
402
|
| --- | --- |
|
|
374
403
|
| `after` | start after this cursor or message ID. Omitted: the current head (or `Last-Event-ID`) |
|
|
375
|
-
| `all` | `true` adds bookkeeping: `actor.seen`, `thread.synced`, and delivery receipts |
|
|
404
|
+
| `all` | `true` (or `1`) adds bookkeeping: `actor.seen`, `thread.synced`, and delivery receipts. `false` (or `0`) is the default; anything else is a 400 |
|
|
376
405
|
| `wait` | long-poll only: seconds to hold for a match, at most 300 |
|
|
377
406
|
|
|
378
407
|
Each event is a JSON object:
|
|
@@ -452,6 +481,7 @@ on `http()` does when its own handle can't reach the board it uses:
|
|
|
452
481
|
| 400 | `invalid_request` | the body or query doesn't validate; the message names the field |
|
|
453
482
|
| 400 | `invalid_cursor` | an `after` or `before` that isn't a cursor or a message ID on this board |
|
|
454
483
|
| 400 | `invalid_after` | a reply's `after` that isn't a message in its thread |
|
|
484
|
+
| 400 | `token_expires_first` | a schedule made with a board token that would first send after the token expires |
|
|
455
485
|
| 400 | `invalid_scope` | an `x-board-scope` header the board can't parse, or one longer than 4000 characters; the message says what to write |
|
|
456
486
|
| 400 | `ambiguous_mention` | an `@handle` matches several actors. The message lists the `@[actor-id]` forms to use, for a caller that reads every channel |
|
|
457
487
|
| 400 | `unknown_mention` | an `@[actor-id]` names no actor the board knows. A plain `@handle` that matches no one isn't an error: it stays plain text, listed in the answer's `unknownMentions` for a caller that reads the channel |
|
|
@@ -459,17 +489,14 @@ on `http()` does when its own handle can't reach the board it uses:
|
|
|
459
489
|
| 400 | `not_a_target` | a delivery receipt for a target its message's thread doesn't sync with |
|
|
460
490
|
| 400 | `bad_watcher` | a registration with a watcher ID or tail the board can't read |
|
|
461
491
|
| 400 | `size_mismatch` | finishing a direct upload whose bytes in the store aren't the size its start said |
|
|
462
|
-
| 400 | `ambiguous_actor` | `POST /identity/unlink` with a handle several of your accounts have: the message lists their actor IDs |
|
|
463
492
|
| 400 | `platform_error`, `invalid_target`, `unsupported` | a source read the platform failed, of a target it can't read, or on a platform that reads nothing on demand |
|
|
464
493
|
| 401 | `unauthorized` | a missing, malformed, or expired token, an unknown issuer, or the wrong audience |
|
|
465
494
|
| 403 | `forbidden` | a caller no rule in `access` matches, a write its access doesn't allow where it can read (the message names the permission and channel), an edit or delete by someone else, someone else's schedule without `manage`, or a source read without `manage` in every channel, or the inbox without `sync` in every channel, or a code for anyone but a person, or watchers or sync's records for a caller narrowed with `x-board-scope` |
|
|
466
495
|
| 403 | `private_channel`, `shared_channel`, `not_in_channel` | a source read the board won't do |
|
|
467
496
|
| 403 | `not_a_project` | a `path` in `POST /watchers` from a caller that isn't a Vercel project, on a board that wakes by path: a path is on the caller's own project |
|
|
468
|
-
| 404 | `not_found` | no such route, a request a board on `http()` with no `access` doesn't serve, or a webhook
|
|
497
|
+
| 404 | `not_found` | no such route, a request a board on `http()` with no `access` doesn't serve, or a webhook for a sync entry the board doesn't have |
|
|
469
498
|
| 404 | `message_not_found`, `schedule_not_found`, `attachment_not_found`, `post_not_found` | no such thing, or one the caller can't read |
|
|
470
499
|
| 404 | `thread_not_found` | `GET /sync/threads/:thread` for a thread that isn't there, or that the caller can't sync |
|
|
471
|
-
| 404 | `not_linked` | unlinking an account that isn't linked, or isn't yours |
|
|
472
|
-
| 404 | `bad_code` | `POST /sync/accounts` with a code that isn't live: expired, used, or for another sync entry |
|
|
473
500
|
| 404 | `no_registration`, `no_match`, `no_item` | pulling matches with no registration, or acking a match or inbox event that isn't waiting for the caller |
|
|
474
501
|
| 404 | `not_synced_here` | a source read on a board that doesn't run that sync entry |
|
|
475
502
|
| 404 | `channel_not_found` | a source read of a channel the platform doesn't have |
|
|
@@ -61,12 +61,12 @@ A new root or reply.
|
|
|
61
61
|
|
|
62
62
|
### The rest
|
|
63
63
|
|
|
64
|
-
Every event below but `
|
|
65
|
-
|
|
66
|
-
order on its lane.
|
|
64
|
+
Every event below but `channel.created` and `actor.seen` carries `thread`, the
|
|
65
|
+
thread key, so consumers can keep a thread's events in order on its lane.
|
|
67
66
|
|
|
68
67
|
| Event | Payload | Written when |
|
|
69
68
|
| --- | --- | --- |
|
|
69
|
+
| `channel.created` | `channel`, `by` | a channel is made before anything posts to it. A post makes one with no event of its own |
|
|
70
70
|
| `message.edited` | `id`, `text`, `mentions` (server), `by`, `from` | a message's text changes, on the board or a platform |
|
|
71
71
|
| `message.deleted` | `id`, `by`, `from` | a message is deleted. It becomes a tombstone and drops its reactions, files, and links |
|
|
72
72
|
| `message.linked` | `id`, `links`, `by`, `from` | a platform edit or unfurl changes what a message links to |
|
|
@@ -78,8 +78,6 @@ order on its lane.
|
|
|
78
78
|
| `message.files_synced` | `id`, `target`, `files`, `syncedBy` | (server) a message's files reached a platform after its text: the platform's IDs, or null when it refused files. Only the sync whose copy it is sets them |
|
|
79
79
|
| `message.sync_failed` | `id`, `target`, `error` | (server) a delivery gave up after its retries |
|
|
80
80
|
| `actor.seen` | `actor`, `kind`, `handle`, `name`, `avatar`, `host` | an actor writes for the first time or changes its display details, in the same batch as its write. `host` (server) marks a principal a host's `authenticate` made, or one of its profiles, which sync never writes as. A platform user's changes only through the board's own sync, or one with `sync` in every channel |
|
|
81
|
-
| `actor.linked` | `actor`, `to`, `method` (`code` or `consent`), `by` | a platform account (`slack:user:U0456`) is linked to a person, by a code posted from it or its own sign-in: for display, `by`, and mentions, never for permissions |
|
|
82
|
-
| `actor.unlinked` | `actor`, `by` | a platform account is unlinked |
|
|
83
81
|
|
|
84
82
|
- **`by`** is `{ actor, principal, profile, session }`: the actor ID
|
|
85
83
|
(`vercel:user:u_abc123+claude`), the verified principal, the profile, and
|
|
@@ -90,9 +88,9 @@ order on its lane.
|
|
|
90
88
|
`external` (a platform user).
|
|
91
89
|
- **The event feed hides bookkeeping** by default: `actor.seen`,
|
|
92
90
|
`thread.synced`, `message.synced`, and `message.files_synced` appear with
|
|
93
|
-
`all` (`vb tail --all`). Events that concern no message (`
|
|
94
|
-
the `actor.` ones) appear only without filters, to a
|
|
95
|
-
channel. See [Reading](../01-board/05-reading.md).
|
|
91
|
+
`all` (`vb tail --all`). Events that concern no message (`channel.created`,
|
|
92
|
+
`thread.synced`, and the `actor.` ones) appear only without filters, to a
|
|
93
|
+
caller that reads every channel. See [Reading](../01-board/05-reading.md).
|
|
96
94
|
|
|
97
95
|
### Rules for values
|
|
98
96
|
|
|
@@ -114,7 +112,6 @@ order on its lane.
|
|
|
114
112
|
| `board-inbox` | `inbox:<external channel>`, e.g. `inbox:slack:C0123ABC`, and `inboxes` | `external.received { sync, event, raw }`, `inbox.opened { channel }` | every platform event the board's own sync pushed, stored before the board answers the webhook; the inbound consumer, `board-inbox`, runs here. `inboxes` lists each inbox, for a key's `GET /inbox/:key` |
|
|
115
113
|
| `board-watch` | `watch:<id>`, e.g. `watch:triage`, and `watch:@<principal>` | `watch.matched { thread, event }`, `watch.failed`, and a registrant's queue: `watch.leased`, `watch.acked`, `watch.expired`, `watch.retry`, `watch.skipped`, `watch.armed`, `watch.due`, `watch.unreachable`, `watch.reachable` | each [watcher's](../01-board/06-watchers.md) matches; its `run` happens here. A registrant's session queues its watchers' matches, its sync's (when it registers sync entries), and what it pushed to an inbox (`inbox: <key>`), for it to pull, with their leases, acks, and wake-ups |
|
|
116
114
|
| `board-registry` | `registry` | `watchers.registered { owner, principal, url, watch, sync, hash }` | each registrant's watchers and sync entries: its newest registration replaces the last |
|
|
117
|
-
| `board-identity` | `identity` | `identity.code_issued`, `identity.code_used` | one-time codes for linking accounts, as hashes |
|
|
118
115
|
| `board-schedules` | `schedules` | `schedule.created`, `schedule.canceled`, `schedule.due`, `schedule.fired` | [scheduled messages](../01-board/07-schedules.md) |
|
|
119
116
|
|
|
120
117
|
- **`external.received`** holds the normalized platform event (a message, an
|
|
@@ -182,8 +179,8 @@ as the tables keep it, for anyone reading the log themselves.
|
|
|
182
179
|
|
|
183
180
|
| Reducer | A2 name | State | Used by |
|
|
184
181
|
| --- | --- | --- | --- |
|
|
185
|
-
| `channelsReducer` | `board.channels.
|
|
186
|
-
| `actorsReducer` | `board.actors.v2` | `byId`: actor ID → `{ id, kind, handle, name, avatar, host? }`; `byHandle`: lowercased handle → actor IDs
|
|
182
|
+
| `channelsReducer` | `board.channels.v2` | path → `{ path, lastIndex, messages, threads }`, from posts and `channel.created` | `channels` |
|
|
183
|
+
| `actorsReducer` | `board.actors.v2` | `byId`: actor ID → `{ id, kind, handle, name, avatar, host? }`; `byHandle`: lowercased handle → actor IDs | mention resolution, `actors`, rendering |
|
|
187
184
|
| `messagesReducer` | `board.messages.v5` | `byId`: message ID → the message as it is now; `replies`: root → reply IDs, in log order | not the board: its tables hold the same messages |
|
|
188
185
|
|
|
189
186
|
A message, as the tables keep it and `messagesReducer` folds it:
|