@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.
Files changed (130) hide show
  1. package/README.md +27 -27
  2. package/dist/{a2-C41hD8SF.mjs → a2-ChRLRupa.mjs} +969 -25
  3. package/dist/access.d.mts +1 -1
  4. package/dist/access.mjs +3 -3
  5. package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
  6. package/dist/attempts-CLG840Oj.mjs +20 -0
  7. package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
  8. package/dist/backend-B-SJ7p6p.d.mts +680 -0
  9. package/dist/backend-a2/contract.d.mts +873 -2
  10. package/dist/backend-a2/contract.mjs +2 -2
  11. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  12. package/dist/backend-a2/scheduler-local.mjs +1 -1
  13. package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
  14. package/dist/backend-a2/scheduler-qstash.mjs +521 -1
  15. package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
  16. package/dist/backend-a2/scheduler-vercel.mjs +203 -1
  17. package/dist/backend-a2/store-memory.d.mts +2 -2
  18. package/dist/backend-a2/store-memory.mjs +3 -3
  19. package/dist/backend-a2/store-postgres.d.mts +1 -1
  20. package/dist/backend-a2/store-postgres.mjs +2850 -2
  21. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  22. package/dist/backend-a2/store-redis-http.mjs +490 -2
  23. package/dist/backend-a2/store-redis.d.mts +10 -2
  24. package/dist/backend-a2/store-redis.mjs +51 -2
  25. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  26. package/dist/backend-a2/store-sqlite.mjs +3 -3
  27. package/dist/backend-a2.d.mts +2706 -3
  28. package/dist/backend-a2.mjs +1 -1
  29. package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
  30. package/dist/{backend-http-0roEmjl0.mjs → backend-http-CMlL3yq3.mjs} +2 -2
  31. package/dist/{backend-http-BVRno-ca.mjs → backend-http-Ddzd5kTS.mjs} +32 -3
  32. package/dist/backend-http.d.mts +1 -1
  33. package/dist/backend-http.mjs +1 -1
  34. package/dist/backend-http.node.d.mts +1 -1
  35. package/dist/backend-http.node.mjs +1 -1
  36. package/dist/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
  37. package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
  38. package/dist/cli.mjs +30 -10
  39. package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
  40. package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
  41. package/dist/conformance.d.mts +1 -1
  42. package/dist/conformance.mjs +1 -1
  43. package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
  44. package/dist/events-CewD4kw7.mjs +75 -0
  45. package/dist/{exports-Dvc1o52I.mjs → exports-CgjfMrQ-.mjs} +35 -32
  46. package/dist/exports-ZsgjzEnM.d.mts +4 -0
  47. package/dist/external-BV_cUncs.mjs +151 -0
  48. package/dist/folds-CRYY14kc.d.mts +534 -0
  49. package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
  50. package/dist/ids-C6AYo1bu.mjs +31 -0
  51. package/dist/index.d.mts +4 -4
  52. package/dist/{index.default-Bkg77Pxn.mjs → index.default-C5Cpz3sa.mjs} +1 -1
  53. package/dist/index.mjs +5 -5
  54. package/dist/index.node.d.mts +4 -4
  55. package/dist/index.node.mjs +3 -3
  56. package/dist/inspect/app.js +31 -31
  57. package/dist/inspect/engine.js +5 -5
  58. package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
  59. package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
  60. package/dist/mentions-CB6ShY4R.mjs +72 -0
  61. package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
  62. package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
  63. package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
  64. package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
  65. package/dist/provider-B3DpYSDq.mjs +220 -0
  66. package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
  67. package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
  68. package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
  69. package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
  70. package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
  71. package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
  72. package/dist/{serve-BsOOc-6_.mjs → serve-BGYw0uNL.mjs} +5 -5
  73. package/dist/{server-jwPY7_wi.mjs → server-DPxBSogx.mjs} +4 -4
  74. package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
  75. package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
  76. package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
  77. package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
  78. package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
  79. package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
  80. package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
  81. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
  82. package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
  83. package/dist/store-memory-Bas9Uq5C.mjs +872 -0
  84. package/dist/store-memory-ZLiZbH3R.mjs +2 -0
  85. package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
  86. package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
  87. package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
  88. package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
  89. package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
  90. package/dist/sync-github.d.mts +24 -17
  91. package/dist/sync-github.mjs +89 -22
  92. package/dist/sync-kapso.d.mts +15 -13
  93. package/dist/sync-kapso.mjs +17 -14
  94. package/dist/sync-linear.d.mts +219 -0
  95. package/dist/sync-linear.mjs +311 -0
  96. package/dist/sync-slack.d.mts +12 -17
  97. package/dist/sync-slack.mjs +54 -42
  98. package/dist/sync.chat.d.mts +1 -1
  99. package/dist/sync.chat.mjs +1 -1
  100. package/dist/sync.d.mts +3 -3
  101. package/dist/sync.mjs +5 -4
  102. package/dist/targets-C-wdR5wP.mjs +33 -0
  103. package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
  104. package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
  105. package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
  106. package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-RLtnSF-t.mjs} +233 -28
  107. package/docs/01-board/01-mounting.md +1 -1
  108. package/docs/02-sync/01-overview.md +72 -48
  109. package/docs/02-sync/02-functions.md +398 -0
  110. package/docs/02-sync/03-slack.md +30 -25
  111. package/docs/02-sync/04-github.md +62 -26
  112. package/docs/02-sync/05-whatsapp.md +23 -19
  113. package/docs/02-sync/06-providers.md +60 -27
  114. package/docs/02-sync/07-linear.md +129 -0
  115. package/docs/03-guides/04-triage.md +1 -1
  116. package/docs/04-reference/01-cli.md +26 -6
  117. package/docs/04-reference/02-board.md +18 -5
  118. package/docs/04-reference/03-http.md +5 -2
  119. package/docs/04-reference/05-conformance.md +1 -1
  120. package/docs/index.md +5 -10
  121. package/package.json +31 -6
  122. package/dist/a2-D24cvwpN.d.mts +0 -3359
  123. package/dist/access-CJU2-vaa.mjs +0 -226
  124. package/dist/attempts-Bxp9KgPf.mjs +0 -919
  125. package/dist/exports-DLiE7fLY.d.mts +0 -4
  126. package/dist/provider-CuewcDNZ.mjs +0 -205
  127. package/dist/redis-tables-B35vv4CZ.mjs +0 -842
  128. package/dist/reducers-B_vUGQRp.d.mts +0 -1172
  129. package/dist/watchers-hash-CLjsa6od.mjs +0 -225
  130. 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({ … }) }, mirroring Slack's #firehose into `firehose`
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
- Attaches the message's thread to an external target and backfills it in
463
- order. A channel target (`slack:#incidents`) starts a thread there; a thread
464
- target (`github:owner/repo:12`) joins it. Attaching twice prints `already
465
- synced with …`. See [Sync](../02-sync/01-overview.md).
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 a receipt
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
- Attaches the message's thread to an external target and backfills it.
247
- `target` is the normalized target; `attached` is false when the thread was
248
- already synced there. See [Sync](../02-sync/01-overview.md).
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 callbacks get a client of their own, acting as the platform user who
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 [Callbacks](../02-sync/02-callbacks.md).
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` and `/messages/:id/typing` are the backend's
143
- methods, which `a2()` answers through the sync `board()` runs beside it. A
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
- slack: slack({
66
- adapter: 'slack/acme', // a Vercel Connect connector
67
- channels: { '#errors-www': 'errors-www', '#prs': 'prs' },
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.16.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
  },