@vercel/board 0.16.0 → 0.18.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 +27 -27
- package/dist/{a2-C41hD8SF.mjs → a2-ChRLRupa.mjs} +969 -25
- package/dist/access.d.mts +1 -1
- package/dist/access.mjs +3 -3
- package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
- package/dist/attempts-CLG840Oj.mjs +20 -0
- package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
- package/dist/backend-B-SJ7p6p.d.mts +680 -0
- package/dist/backend-a2/contract.d.mts +873 -2
- package/dist/backend-a2/contract.mjs +2 -2
- package/dist/backend-a2/scheduler-local.d.mts +1 -1
- package/dist/backend-a2/scheduler-local.mjs +1 -1
- package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
- package/dist/backend-a2/scheduler-qstash.mjs +521 -1
- package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
- package/dist/backend-a2/scheduler-vercel.mjs +203 -1
- package/dist/backend-a2/store-memory.d.mts +2 -2
- package/dist/backend-a2/store-memory.mjs +3 -3
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +2850 -2
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +490 -2
- package/dist/backend-a2/store-redis.d.mts +10 -2
- package/dist/backend-a2/store-redis.mjs +51 -2
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +3 -3
- package/dist/backend-a2.d.mts +2706 -3
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
- package/dist/{backend-http-0roEmjl0.mjs → backend-http-CMlL3yq3.mjs} +2 -2
- package/dist/{backend-http-BVRno-ca.mjs → backend-http-Ddzd5kTS.mjs} +32 -3
- 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/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
- package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
- package/dist/cli.mjs +30 -10
- package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
- package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
- package/dist/events-CewD4kw7.mjs +75 -0
- package/dist/{exports-Dvc1o52I.mjs → exports-CgjfMrQ-.mjs} +35 -32
- package/dist/exports-ZsgjzEnM.d.mts +4 -0
- package/dist/external-BV_cUncs.mjs +151 -0
- package/dist/folds-CRYY14kc.d.mts +534 -0
- package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
- package/dist/ids-C6AYo1bu.mjs +31 -0
- package/dist/index.d.mts +4 -4
- package/dist/{index.default-Bkg77Pxn.mjs → index.default-C5Cpz3sa.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.node.d.mts +4 -4
- package/dist/index.node.mjs +3 -3
- package/dist/inspect/app.js +31 -31
- package/dist/inspect/engine.js +5 -5
- package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
- package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
- package/dist/mentions-CB6ShY4R.mjs +72 -0
- package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
- package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
- package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
- package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
- package/dist/provider-B3DpYSDq.mjs +220 -0
- package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
- package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
- package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
- package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
- package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
- package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
- package/dist/{serve-BsOOc-6_.mjs → serve-BGYw0uNL.mjs} +5 -5
- package/dist/{server-jwPY7_wi.mjs → server-DPxBSogx.mjs} +4 -4
- package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
- package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
- package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
- package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
- package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
- package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
- package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
- package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
- package/dist/store-memory-Bas9Uq5C.mjs +872 -0
- package/dist/store-memory-ZLiZbH3R.mjs +2 -0
- package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
- package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
- package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
- package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
- package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
- package/dist/sync-github.d.mts +24 -17
- package/dist/sync-github.mjs +89 -22
- package/dist/sync-kapso.d.mts +15 -13
- package/dist/sync-kapso.mjs +17 -14
- package/dist/sync-linear.d.mts +219 -0
- package/dist/sync-linear.mjs +311 -0
- package/dist/sync-slack.d.mts +12 -17
- package/dist/sync-slack.mjs +54 -42
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.chat.mjs +1 -1
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +5 -4
- package/dist/targets-C-wdR5wP.mjs +33 -0
- package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
- package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
- package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
- package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-RLtnSF-t.mjs} +233 -28
- package/docs/01-board/01-mounting.md +1 -1
- package/docs/02-sync/01-overview.md +72 -48
- package/docs/02-sync/02-functions.md +398 -0
- package/docs/02-sync/03-slack.md +30 -25
- package/docs/02-sync/04-github.md +62 -26
- package/docs/02-sync/05-whatsapp.md +23 -19
- package/docs/02-sync/06-providers.md +60 -27
- package/docs/02-sync/07-linear.md +129 -0
- package/docs/03-guides/04-triage.md +1 -1
- package/docs/04-reference/01-cli.md +26 -6
- package/docs/04-reference/02-board.md +18 -5
- package/docs/04-reference/03-http.md +5 -2
- package/docs/04-reference/05-conformance.md +1 -1
- package/docs/index.md +5 -10
- package/package.json +31 -6
- package/dist/a2-D24cvwpN.d.mts +0 -3359
- package/dist/access-CJU2-vaa.mjs +0 -226
- package/dist/attempts-Bxp9KgPf.mjs +0 -919
- package/dist/exports-DLiE7fLY.d.mts +0 -4
- package/dist/provider-CuewcDNZ.mjs +0 -205
- package/dist/redis-tables-B35vv4CZ.mjs +0 -842
- package/dist/reducers-B_vUGQRp.d.mts +0 -1172
- package/dist/watchers-hash-CLjsa6od.mjs +0 -225
- package/docs/02-sync/02-callbacks.md +0 -265
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Linear
|
|
3
|
+
description: Turn each Linear issue into a board thread, with its comments as replies and its state as a label.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Configure it
|
|
7
|
+
|
|
8
|
+
```package-install
|
|
9
|
+
npm i chat @chat-adapter/linear @vercel/connect
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`linear(app, sync)` takes a [Vercel Connect](./01-overview.md#set-up-webhooks)
|
|
13
|
+
connector and a [sync function](./02-functions.md). `teams` makes one from a map
|
|
14
|
+
of Linear team keys to board channels. board builds the Chat SDK adapter from the
|
|
15
|
+
connector name:
|
|
16
|
+
|
|
17
|
+
```ts app/api/board/[[...path]]/route.ts
|
|
18
|
+
import { board } from '@vercel/board'
|
|
19
|
+
import { a2 } from '@vercel/board/backend-a2'
|
|
20
|
+
import { vercelQueues } from '@vercel/board/backend-a2/scheduler-vercel'
|
|
21
|
+
import { postgres } from '@vercel/board/backend-a2/store-postgres'
|
|
22
|
+
import { linear, teams } from '@vercel/board/sync-linear'
|
|
23
|
+
|
|
24
|
+
const backend = a2({
|
|
25
|
+
store: postgres({ connectionString: process.env.DATABASE_URL! }),
|
|
26
|
+
scheduler: vercelQueues({ topic: 'board' }),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const team = board({
|
|
30
|
+
backend,
|
|
31
|
+
sync: {
|
|
32
|
+
linear: linear('linear/acme', teams({
|
|
33
|
+
ENG: 'dev', // ENG-123 and the rest of the team's issues, both ways
|
|
34
|
+
SUP: 'support',
|
|
35
|
+
})),
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
export const GET = team.fetch, POST = team.fetch
|
|
39
|
+
export const { scheduler } = backend
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A key is a team's key, the prefix of its issue IDs (`ENG` in `ENG-123`). A value is
|
|
43
|
+
a board channel to mirror, or a [function](./02-functions.md). A team that isn't
|
|
44
|
+
in the map stays off the board, except threads someone syncs by hand.
|
|
45
|
+
|
|
46
|
+
## An issue's life in one thread
|
|
47
|
+
|
|
48
|
+
1. **An issue opens.** The board creates a root in the team's channel,
|
|
49
|
+
`ENG-123: <title>` with its link, labeled `issue=ENG-123` and `state` with its
|
|
50
|
+
workflow state's type.
|
|
51
|
+
2. **Comments** arrive as replies. A reply to a comment stays in that comment's
|
|
52
|
+
thread on Linear, and a board reply to one goes back into it. Edits and
|
|
53
|
+
deletes sync too.
|
|
54
|
+
3. **Moving the issue** to another state updates `state`. A new title edits a
|
|
55
|
+
root that Linear opened to say it.
|
|
56
|
+
4. **A root posted on the board** in a mirrored channel opens an issue in that
|
|
57
|
+
team. Its first line is the title, and the whole message is the description.
|
|
58
|
+
Editing the root edits the description. Deleting it on the board leaves the
|
|
59
|
+
issue open on Linear, and a new title on Linear leaves the root as written.
|
|
60
|
+
|
|
61
|
+
| Thing | ID |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| A team, the channel | `linear:ENG` |
|
|
64
|
+
| An issue | `linear:ENG:<issueId>` |
|
|
65
|
+
| A comment thread, grouped under its issue | `linear:ENG:<issueId>:c:<commentId>` |
|
|
66
|
+
|
|
67
|
+
`<issueId>` is the issue's UUID, which stays the same when the issue is renamed.
|
|
68
|
+
To sync a thread by hand, name the issue by its identifier, its UUID, or its ID:
|
|
69
|
+
`vb sync 812 linear:ENG-123`. A comment thread isn't a sync target: sync its
|
|
70
|
+
issue.
|
|
71
|
+
|
|
72
|
+
An issue that moves to another team starts a new thread in that team's channel,
|
|
73
|
+
with a root fetched from Linear: the team is part of the ID, so what's said on
|
|
74
|
+
the board follows the team it's in now. The old thread stays as it was.
|
|
75
|
+
|
|
76
|
+
## Events
|
|
77
|
+
|
|
78
|
+
| Event | What the mirror does |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `issue.created` | A root labeled `issue=ENG-123` and `state=<type>` |
|
|
81
|
+
| `issue.updated` | A new state sets `state`; a new title edits a root Linear opened. Other changes write nothing |
|
|
82
|
+
| `comment` | A reply. Edits and deletes sync too |
|
|
83
|
+
|
|
84
|
+
`state` is the workflow state's type: `triage`, `backlog`, `unstarted`,
|
|
85
|
+
`started`, `completed`, or `canceled`, the same whatever a team names its states.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# in a terminal:
|
|
89
|
+
vb read dev --label state=started # what's in progress
|
|
90
|
+
vb wait dev --label state=completed --timeout 1h
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A comment on an issue that opened before you mapped its team still lands: the
|
|
94
|
+
board fetches the issue through Linear's API, creates its root, then posts the
|
|
95
|
+
comment as a reply.
|
|
96
|
+
|
|
97
|
+
## Reactions
|
|
98
|
+
|
|
99
|
+
A reaction on the board goes to its comment on Linear, while it's added: Linear
|
|
100
|
+
reacts to comments, not issues, and the adapter can't take one back. Reactions
|
|
101
|
+
on Linear stay there, since Linear's reaction webhooks don't say which issue
|
|
102
|
+
they're on.
|
|
103
|
+
|
|
104
|
+
## Setup
|
|
105
|
+
|
|
106
|
+
1. Create a Linear app (**Settings > API > Applications**), through Vercel
|
|
107
|
+
Connect or by hand, with the `read`, `write`, `comments:create`, and
|
|
108
|
+
`issues:create` scopes, installed as the app (`actor=app`).
|
|
109
|
+
2. Subscribe its webhook to **Issues** and **Comments**, for the teams you map.
|
|
110
|
+
3. Point the webhook at `https://<host>/api/board/webhooks/linear`: Connect's
|
|
111
|
+
trigger path, or the app's webhook URL with its signing secret.
|
|
112
|
+
|
|
113
|
+
Without Connect, pass your own adapter, in its default `comments` mode, with the
|
|
114
|
+
app's own credentials:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// in board({ sync: { … } }):
|
|
118
|
+
// import { createLinearAdapter } from '@chat-adapter/linear'
|
|
119
|
+
linear({
|
|
120
|
+
adapter: createLinearAdapter({
|
|
121
|
+
clientCredentials: { clientId: process.env.LINEAR_CLIENT_ID!, clientSecret: process.env.LINEAR_CLIENT_SECRET! },
|
|
122
|
+
webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,
|
|
123
|
+
}),
|
|
124
|
+
}, teams({ ENG: 'dev' })),
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> [!WARNING]
|
|
128
|
+
> The adapter takes the token's user for itself and skips what they write. With a
|
|
129
|
+
> personal API key, that's you: your own comments never reach the board.
|
|
@@ -114,7 +114,7 @@ const backend = a2({
|
|
|
114
114
|
|
|
115
115
|
const team = board({
|
|
116
116
|
backend,
|
|
117
|
-
// sync: { slack: slack({
|
|
117
|
+
// sync: { slack: slack('slack/acme', channels({ '#firehose': 'firehose' })) },
|
|
118
118
|
watch: {
|
|
119
119
|
triage: {
|
|
120
120
|
tail: 'firehose --from slack',
|
|
@@ -95,7 +95,8 @@ $ vb read 812
|
|
|
95
95
|
| Command | One line per |
|
|
96
96
|
| --- | --- |
|
|
97
97
|
| `read <channel>`, `read [filters]` | thread: `{ message, replies, last }`, the root, its reply count, and its latest reply or `null` |
|
|
98
|
-
| `read <id>` | message: `{ id, channel, text, re, labels, reactions, by, author, createdAt, … }`, where `re` is the root's ID or `null` for a root |
|
|
98
|
+
| `read <id>`, `read <link>` | message: `{ id, channel, text, re, labels, reactions, by, author, createdAt, … }`, where `re` is the root's ID or `null` for a root |
|
|
99
|
+
| `sync <id>` | target: `{ target, key, thread, message, url, undelivered }`, the message's copy there and the platform's link to it |
|
|
99
100
|
| `tail`, `wait` | event: `{ cursor, type, createdAt, payload, message, actor }` |
|
|
100
101
|
| `post`, `reply` | the new message |
|
|
101
102
|
|
|
@@ -288,6 +289,7 @@ linked account, whose it is (`→ @jb`).
|
|
|
288
289
|
```text
|
|
289
290
|
vb read [channel] [filters] [--after <id>] [--limit N]
|
|
290
291
|
vb read <id>
|
|
292
|
+
vb read <link>
|
|
291
293
|
```
|
|
292
294
|
|
|
293
295
|
- **`read <channel>`** prints one summary per thread, like a Slack channel:
|
|
@@ -299,6 +301,11 @@ vb read <id>
|
|
|
299
301
|
- **`read <id>`** prints one whole thread: the root and every reply, plus the
|
|
300
302
|
thread's pending schedules. Any message in the thread works. With filters,
|
|
301
303
|
it prints the root and only the replies that match: `vb read 812 --by ada`.
|
|
304
|
+
- **`read <link>`** reads the thread a platform link names: a Slack permalink,
|
|
305
|
+
a GitHub PR, issue, or comment. The board answers from what its sync
|
|
306
|
+
recorded and never asks the platform, so a link it didn't sync, or one in a
|
|
307
|
+
channel you can't read, is the same error, exit 1:
|
|
308
|
+
`error: no message on this board for that link`.
|
|
302
309
|
- **A channel that doesn't exist** is an error, exit 1:
|
|
303
310
|
`error: no channel <name>: board channels lists them`.
|
|
304
311
|
- `--limit` is 50 by default, at most 500. `--after <id>` reads forward from a
|
|
@@ -456,13 +463,26 @@ WhatsApp marks the latest inbound message read. Nothing is written to the log.
|
|
|
456
463
|
### sync
|
|
457
464
|
|
|
458
465
|
```text
|
|
466
|
+
vb sync <id>
|
|
459
467
|
vb sync <id> <target>
|
|
460
468
|
```
|
|
461
469
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
470
|
+
With no target, it prints where the message's thread is synced: a row a
|
|
471
|
+
target, with its sync key, where it is, and the platform's link to this
|
|
472
|
+
message's copy there, or why it didn't reach it. A thread on no platform
|
|
473
|
+
prints `not synced anywhere`.
|
|
474
|
+
|
|
475
|
+
```text
|
|
476
|
+
$ vb sync 812
|
|
477
|
+
slack C0C8XN8SQRG https://vercel.slack.com/archives/C0C8XN8SQRG/p1792780353701549
|
|
478
|
+
github vercel/v0:4021 https://github.com/vercel/v0/pull/4021#issuecomment-3301
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
With a target, it attaches the thread there and backfills it in order. A
|
|
482
|
+
channel target (`slack:#incidents`) starts a thread there; a thread target
|
|
483
|
+
(`github:owner/repo:12`) joins it. Either way the thread stays synced both
|
|
484
|
+
ways. Attaching twice prints `already synced with …`. See
|
|
485
|
+
[Sync](../02-sync/01-overview.md).
|
|
466
486
|
|
|
467
487
|
### sources
|
|
468
488
|
|
|
@@ -515,7 +535,7 @@ watchers
|
|
|
515
535
|
passed matches a registered tail, hands the match to a pull, and takes its ack
|
|
516
536
|
|
|
517
537
|
sync
|
|
518
|
-
passed keeps sync's records: an import as a platform author, its lookups by platform ID, and
|
|
538
|
+
passed keeps sync's records: an import as a platform author, its lookups by platform ID, a receipt, and where it's synced
|
|
519
539
|
|
|
520
540
|
18 passed
|
|
521
541
|
```
|
|
@@ -240,12 +240,25 @@ This handle's own post with `id`, as posting it answered, or a 404
|
|
|
240
240
|
### sync
|
|
241
241
|
|
|
242
242
|
```ts
|
|
243
|
+
sync(id: string): Promise<SyncedView>
|
|
243
244
|
sync(id: string, target: string): Promise<{ target: string; attached: boolean }>
|
|
245
|
+
linked(url: string): Promise<ThreadView>
|
|
244
246
|
```
|
|
245
247
|
|
|
246
|
-
|
|
247
|
-
`target` is the normalized target; `attached` is false when the
|
|
248
|
-
already synced there.
|
|
248
|
+
With a target, `sync` attaches the message's thread to an external target and
|
|
249
|
+
backfills it. `target` is the normalized target; `attached` is false when the
|
|
250
|
+
thread was already synced there.
|
|
251
|
+
|
|
252
|
+
With no target, it says where the thread is synced: `{ targets }`, each
|
|
253
|
+
`{ target, key, thread, message, url, undelivered }`, with this message's copy
|
|
254
|
+
there by the platform's ID and the platform's link to it. It needs `read` where
|
|
255
|
+
the message is.
|
|
256
|
+
|
|
257
|
+
`linked(url)` is the thread a platform link names (a Slack permalink, a GitHub
|
|
258
|
+
PR, issue, or comment), as `thread(id)` answers it. The board looks it up in
|
|
259
|
+
what its sync recorded and never asks the platform: a link it didn't sync, or
|
|
260
|
+
to a message you can't read, is 404 `message_not_found`. See
|
|
261
|
+
[Sync](../02-sync/01-overview.md).
|
|
249
262
|
|
|
250
263
|
### typing
|
|
251
264
|
|
|
@@ -574,6 +587,6 @@ export async function POST(request: Request) {
|
|
|
574
587
|
calls `http()`'s methods, which call that board, whose middleware sees its
|
|
575
588
|
writes.
|
|
576
589
|
|
|
577
|
-
Sync
|
|
590
|
+
Sync functions get a client of their own, acting as the platform user who
|
|
578
591
|
triggered the event. See [Mounting](../01-board/01-mounting.md),
|
|
579
|
-
[Watchers](../01-board/06-watchers.md), and [
|
|
592
|
+
[Watchers](../01-board/06-watchers.md), and [Sync functions](../02-sync/02-functions.md).
|
|
@@ -109,6 +109,8 @@ a refused write it can read answers 403. `GET /me` returns the caller's
|
|
|
109
109
|
| POST | `/messages/:id/reactions` | `{ emoji, remove? }`. `emoji` is a short name: `eyes`, `thumbs_up` |
|
|
110
110
|
| POST | `/messages/:id/labels` | `{ set?, unset? }`, at least one |
|
|
111
111
|
| POST | `/messages/:id/sync` | `{ target }`: attach the thread to an external target and backfill it. Answers `{ target, attached }` |
|
|
112
|
+
| GET | `/messages/:id/sync` | Where the thread is synced: `{ targets }`, each `{ target, key, thread, message, url, undelivered }`, with this message's copy there and the platform's link to it. Needs `read` where it is |
|
|
113
|
+
| GET | `/links` | The thread a platform link names, as `GET /messages/:id` answers it. Query: `url`. From sync's records alone; a link the board didn't sync, or to a message the caller can't read, is 404 `message_not_found` |
|
|
112
114
|
| POST | `/messages/:id/typing` | `{ status? }`: show typing in the thread's synced platform threads. Answers `{ targets }`. Writes nothing to the log |
|
|
113
115
|
| POST | `/attachments` | Upload one file. Answers 201 `{ attachment: { id, name, type, size } }`. See [Attachments](#attachments) |
|
|
114
116
|
| GET | `/attachments/:id` | The file |
|
|
@@ -139,8 +141,9 @@ Every board that keeps its own log answers every route here. The sync routes
|
|
|
139
141
|
a person calls (`/webhooks/:key`, `/messages/:id/sync`, `/identity/sign-in`,
|
|
140
142
|
`/identity/signed-in`, and `/identity/unlink`) are `board()`'s, and work on any
|
|
141
143
|
backend: the ones that need a sync entry answer 404 on a board that doesn't
|
|
142
|
-
run it. `/sources/:key/read
|
|
143
|
-
methods, which `a2()` answers through the sync
|
|
144
|
+
run it. `/sources/:key/read`, `/messages/:id/typing`, `GET /messages/:id/sync`,
|
|
145
|
+
and `/links` are the backend's methods, which `a2()` answers through the sync
|
|
146
|
+
`board()` runs beside it. A
|
|
144
147
|
board on `http()` answers only wake-ups, webhooks, and its sync's sign-ins,
|
|
145
148
|
unless it has an `access` of its own: then it serves its callers as its
|
|
146
149
|
project on the board it uses (see [A board on `http()`](../01-board/09-access.md#a-board-on-http)). There are no
|
|
@@ -56,7 +56,7 @@ Then the rest, a group each in the report:
|
|
|
56
56
|
| `schedules` | schedules a reply a day out, lists it, and cancels it |
|
|
57
57
|
| `attachments` | uploads a file, posts it, and downloads it |
|
|
58
58
|
| `watchers` | registers a tail, pulls the match for a post, and acks it. Skipped when the token already has watchers or a sync registered there, since registering replaces them |
|
|
59
|
-
| `sync` | imports a message as a platform user of its own (`conformance:user:<id>`), finds it by its platform IDs, imports it again to the same message, refuses a second root from the same platform thread, records a receipt and reads the copy back, and deletes it. Skipped when the token can't register a sync or `sync` in the channel, or already has watchers or a sync registered |
|
|
59
|
+
| `sync` | imports a message as a platform user of its own (`conformance:user:<id>`), finds it by its platform IDs, imports it again to the same message, refuses a second root from the same platform thread, records a receipt and reads the copy back, lists where the thread is synced with it, answers 404 for a link no sync there knows, and deletes it. Skipped when the token can't register a sync or `sync` in the channel, or already has watchers or a sync registered |
|
|
60
60
|
|
|
61
61
|
Every board answers all of them: see
|
|
62
62
|
[the whole protocol](./03-http.md#the-whole-protocol). A board may keep no
|
package/docs/index.md
CHANGED
|
@@ -51,8 +51,8 @@ import { board } from '@vercel/board'
|
|
|
51
51
|
import { a2 } from '@vercel/board/backend-a2'
|
|
52
52
|
import { vercelQueues } from '@vercel/board/backend-a2/scheduler-vercel'
|
|
53
53
|
import { postgres } from '@vercel/board/backend-a2/store-postgres'
|
|
54
|
-
import { slack } from '@vercel/board/sync-slack'
|
|
55
|
-
import { github } from '@vercel/board/sync-github'
|
|
54
|
+
import { channels, slack } from '@vercel/board/sync-slack'
|
|
55
|
+
import { github, repos } from '@vercel/board/sync-github'
|
|
56
56
|
|
|
57
57
|
const backend = a2({
|
|
58
58
|
store: postgres({ connectionString: process.env.DATABASE_URL! }),
|
|
@@ -62,14 +62,9 @@ const backend = a2({
|
|
|
62
62
|
const team = board({
|
|
63
63
|
backend,
|
|
64
64
|
sync: {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}),
|
|
69
|
-
github: github({
|
|
70
|
-
adapter: 'github/acme',
|
|
71
|
-
repos: { 'acme/www': 'prs' },
|
|
72
|
-
}),
|
|
65
|
+
// A Vercel Connect connector, and what each event does.
|
|
66
|
+
slack: slack('slack/acme', channels({ '#errors-www': 'errors-www', '#prs': 'prs' })),
|
|
67
|
+
github: github('github/acme', repos({ 'acme/www': 'prs' })),
|
|
73
68
|
},
|
|
74
69
|
})
|
|
75
70
|
export const GET = team.fetch, POST = team.fetch
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/board",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "A message board you mount in one route: one durable log, verified authors, live tail, and two-way sync.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -100,27 +100,34 @@
|
|
|
100
100
|
"types": "./dist/sync-kapso.d.mts",
|
|
101
101
|
"default": "./dist/sync-kapso.mjs"
|
|
102
102
|
},
|
|
103
|
+
"./sync-linear": {
|
|
104
|
+
"types": "./dist/sync-linear.d.mts",
|
|
105
|
+
"default": "./dist/sync-linear.mjs"
|
|
106
|
+
},
|
|
103
107
|
"./package.json": "./package.json"
|
|
104
108
|
},
|
|
105
109
|
"dependencies": {
|
|
106
|
-
"@chat-adapter/github": "^4.41.1",
|
|
107
|
-
"@chat-adapter/slack": "^4.41.1",
|
|
108
|
-
"@vercel/connect": "^2.3.4",
|
|
109
110
|
"@vercel/oidc": "^3.8.9",
|
|
110
|
-
"chat": "^4.41.1",
|
|
111
|
-
"experimental-a2": "0.22.0",
|
|
112
111
|
"jose": "^6.2.12",
|
|
113
112
|
"zod": "^4.6.5"
|
|
114
113
|
},
|
|
115
114
|
"peerDependencies": {
|
|
115
|
+
"@chat-adapter/github": "^4.41.1",
|
|
116
|
+
"@chat-adapter/linear": "^4.41.1",
|
|
117
|
+
"@chat-adapter/slack": "^4.41.1",
|
|
116
118
|
"@upstash/qstash": "^2.11.0",
|
|
117
119
|
"@vercel/blob": "^2.8.0",
|
|
120
|
+
"@vercel/connect": "^2.3.4",
|
|
118
121
|
"@vercel/queue": "*",
|
|
122
|
+
"chat": "^4.41.1",
|
|
119
123
|
"ioredis": "^5.4.0",
|
|
120
124
|
"pg": "^8.11.0"
|
|
121
125
|
},
|
|
122
126
|
"devDependencies": {
|
|
123
127
|
"@base-ui/react": "^1.8.0",
|
|
128
|
+
"@chat-adapter/github": "^4.41.1",
|
|
129
|
+
"@chat-adapter/linear": "^4.41.1",
|
|
130
|
+
"@chat-adapter/slack": "^4.41.1",
|
|
124
131
|
"@chat-adapter/tests": "^4.41.1",
|
|
125
132
|
"@electric-sql/pglite": "0.3.15",
|
|
126
133
|
"@kapso/chat-adapter": "^0.1.1",
|
|
@@ -129,7 +136,10 @@
|
|
|
129
136
|
"@types/react": "^19.3.0",
|
|
130
137
|
"@types/react-dom": "^19.3.0",
|
|
131
138
|
"@vercel/blob": "^2.8.0",
|
|
139
|
+
"@vercel/connect": "^2.3.4",
|
|
132
140
|
"ai": "^7.0.130",
|
|
141
|
+
"chat": "^4.41.1",
|
|
142
|
+
"experimental-a2": "0.22.0",
|
|
133
143
|
"ioredis": "5.11.1",
|
|
134
144
|
"lucide-react": "^1.49.0",
|
|
135
145
|
"pg": "^8.16.3",
|
|
@@ -143,15 +153,30 @@
|
|
|
143
153
|
"vitest": "^5.0.3"
|
|
144
154
|
},
|
|
145
155
|
"peerDependenciesMeta": {
|
|
156
|
+
"@chat-adapter/github": {
|
|
157
|
+
"optional": true
|
|
158
|
+
},
|
|
159
|
+
"@chat-adapter/linear": {
|
|
160
|
+
"optional": true
|
|
161
|
+
},
|
|
162
|
+
"@chat-adapter/slack": {
|
|
163
|
+
"optional": true
|
|
164
|
+
},
|
|
146
165
|
"@upstash/qstash": {
|
|
147
166
|
"optional": true
|
|
148
167
|
},
|
|
149
168
|
"@vercel/blob": {
|
|
150
169
|
"optional": true
|
|
151
170
|
},
|
|
171
|
+
"@vercel/connect": {
|
|
172
|
+
"optional": true
|
|
173
|
+
},
|
|
152
174
|
"@vercel/queue": {
|
|
153
175
|
"optional": true
|
|
154
176
|
},
|
|
177
|
+
"chat": {
|
|
178
|
+
"optional": true
|
|
179
|
+
},
|
|
155
180
|
"ioredis": {
|
|
156
181
|
"optional": true
|
|
157
182
|
},
|