@vercel/board 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +8 -17
  2. package/dist/{a2-HUbQJ0RD.mjs → a2-DngWY2Eb.mjs} +741 -415
  3. package/dist/access-CY5xVMIK.mjs +242 -0
  4. package/dist/{access-CV96SYgr.d.mts → access-DPcrKIH_.d.mts} +6 -1
  5. package/dist/access.d.mts +4 -4
  6. package/dist/access.mjs +5 -4
  7. package/dist/{actors-DcJhFbwJ.mjs → actors-Bm-ojWWT.mjs} +2 -2
  8. package/dist/{auth-JO22iIwr.mjs → auth-CyO2aw5n.mjs} +126 -236
  9. package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
  10. package/dist/{backend-CPC_vyOo.d.mts → backend-Bob-5CM7.d.mts} +93 -42
  11. package/dist/backend-a2/blob-vercel.d.mts +1 -1
  12. package/dist/backend-a2/contract.d.mts +38 -98
  13. package/dist/backend-a2/contract.mjs +1 -1
  14. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  15. package/dist/backend-a2/scheduler-local.mjs +1 -1
  16. package/dist/backend-a2/scheduler-qstash.d.mts +3 -3
  17. package/dist/backend-a2/scheduler-qstash.mjs +8 -8
  18. package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
  19. package/dist/backend-a2/scheduler-vercel.mjs +6 -6
  20. package/dist/backend-a2/store-memory.d.mts +2 -2
  21. package/dist/backend-a2/store-memory.mjs +3 -3
  22. package/dist/backend-a2/store-postgres.d.mts +1 -1
  23. package/dist/backend-a2/store-postgres.mjs +34 -15
  24. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  25. package/dist/backend-a2/store-redis-http.mjs +6 -6
  26. package/dist/backend-a2/store-redis.d.mts +2 -2
  27. package/dist/backend-a2/store-redis.mjs +5 -5
  28. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  29. package/dist/backend-a2/store-sqlite.mjs +3 -3
  30. package/dist/backend-a2.d.mts +143 -242
  31. package/dist/backend-a2.mjs +2 -2
  32. package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-BRrRbjRx.d.mts} +59 -45
  33. package/dist/{backend-http-CzIJMm58.mjs → backend-http-CvUKOn-m.mjs} +11 -10
  34. package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-D8mUzuKQ.mjs} +2 -2
  35. package/dist/backend-http.d.mts +1 -1
  36. package/dist/backend-http.mjs +1 -1
  37. package/dist/backend-http.node.d.mts +1 -1
  38. package/dist/backend-http.node.mjs +1 -1
  39. package/dist/{backend-vercel-DQc_TaHp.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
  40. package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-BaSEUdwg.d.mts} +62 -47
  41. package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-CmY9yZ9m.mjs} +279 -42
  42. package/dist/backend-vercel.d.mts +1 -1
  43. package/dist/backend-vercel.mjs +1 -1
  44. package/dist/backend-vercel.node.d.mts +1 -1
  45. package/dist/backend-vercel.node.mjs +1 -1
  46. package/dist/{blob-DmiQjrYy.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
  47. package/dist/{blob-DdbpJqN1.mjs → blob-WbB2DJKn.mjs} +30 -11
  48. package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-BChYrrA7.d.mts} +32 -28
  49. package/dist/{chat-provider-COtsU5jB.mjs → chat-provider-DkFp2hrh.mjs} +105 -60
  50. package/dist/cli-commands.d.mts +1 -1
  51. package/dist/cli-commands.mjs +1 -1
  52. package/dist/cli-embedded.d.mts +2 -2
  53. package/dist/cli-embedded.mjs +1 -1
  54. package/dist/cli.mjs +4 -4
  55. package/dist/{client-BqAjk0Q4.d.mts → client-COfYrgfV.d.mts} +240 -227
  56. package/dist/{commands-D6gvrOh3.d.mts → commands-DK4W3HCJ.d.mts} +47 -10
  57. package/dist/{commands-C3R1AWgm.mjs → commands-L1JSGz2Z.mjs} +212 -54
  58. package/dist/conformance-DK43HiHf.mjs +1000 -0
  59. package/dist/conformance.d.mts +8 -2
  60. package/dist/conformance.mjs +1 -1
  61. package/dist/{engine-DQxXk7dX.mjs → engine-DurYIRt8.mjs} +29 -210
  62. package/dist/{errors-DDktVeUa.d.mts → errors-2vGs49Db.d.mts} +1 -1
  63. package/dist/{client-D0Px7KzF.mjs → errors-CJXhhzvT.mjs} +132 -27
  64. package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
  65. package/dist/{exports-BG-0Wyn-.mjs → exports-CWIdN0Ub.mjs} +195 -103
  66. package/dist/exports-FBOQWT7O.d.mts +5 -0
  67. package/dist/{folds-BDMSxBUf.mjs → folds-Cu9Q9zyQ.mjs} +3 -22
  68. package/dist/{folds-ZkHM4M-v.d.mts → folds-DLUzfibJ.d.mts} +15 -40
  69. package/dist/{index-D3VvNfh_.d.mts → index-Ho-ayyQL.d.mts} +2 -2
  70. package/dist/index.d.mts +7 -7
  71. package/dist/{index.default-rB8sJ9RX.mjs → index.default-BUcwDaAe.mjs} +1 -1
  72. package/dist/index.mjs +5 -6
  73. package/dist/index.node.d.mts +7 -7
  74. package/dist/index.node.mjs +3 -4
  75. package/dist/inspect/app.js +62 -61
  76. package/dist/inspect/engine.js +3 -3
  77. package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
  78. package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
  79. package/dist/{main-CVBqe3VO.mjs → main-Dz-8komG.mjs} +251 -127
  80. package/dist/{middleware-CXjEeef4.mjs → middleware-BavYSDUa.mjs} +1 -1
  81. package/dist/{node-Zd_C-gxP.mjs → node-BgeGcwX0.mjs} +7 -3
  82. package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
  83. package/dist/{project-CBXlLftU.mjs → project-CQ4VUibA.mjs} +1 -1
  84. package/dist/{provider-DmSTp9lI.d.mts → provider-gynDUoyW.d.mts} +13 -43
  85. package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-DHf85ixI.mjs} +40 -42
  86. package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
  87. package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
  88. package/dist/{reducers-DJzVpXwM.mjs → reducers-DcENwtwZ.mjs} +74 -77
  89. package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
  90. package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
  91. package/dist/{serve-G9RuaDSr.mjs → serve-CTmow-YH.mjs} +7 -7
  92. package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
  93. package/dist/{server-BixV_byP.mjs → server-DG8D6VV0.mjs} +4 -4
  94. package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
  95. package/dist/{serving-B75Y4BF-.mjs → serving-DBFRo6U_.mjs} +1 -1
  96. package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
  97. package/dist/{sql-D4Z_efJX.mjs → sql-CT5xVuGr.mjs} +2 -8
  98. package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
  99. package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
  100. package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
  101. package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
  102. package/dist/store-memory-4IBl0Ztu.mjs +2 -0
  103. package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
  104. package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
  105. package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
  106. package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
  107. package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
  108. package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
  109. package/dist/{sync-kapso.d.mts → sync-chat-sdk.d.mts} +33 -37
  110. package/dist/sync-chat-sdk.mjs +44 -0
  111. package/dist/sync-github.d.mts +5 -18
  112. package/dist/sync-github.mjs +4 -70
  113. package/dist/sync-linear.d.mts +3 -3
  114. package/dist/sync-linear.mjs +6 -5
  115. package/dist/sync-slack.d.mts +7 -7
  116. package/dist/sync-slack.mjs +33 -54
  117. package/dist/sync.d.mts +3 -3
  118. package/dist/sync.mjs +2 -4
  119. package/dist/{tokens-CC3izSNr.mjs → tokens-Dnyd_y1n.mjs} +15 -6
  120. package/dist/{types-No0WdhE5.d.mts → types-CmrFjpml.d.mts} +2 -4
  121. package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-BjwlnGD9.mjs} +124 -115
  122. package/docs/01-board/01-mounting.md +2 -2
  123. package/docs/01-board/03-identity.md +5 -6
  124. package/docs/01-board/04-messages.md +96 -2
  125. package/docs/01-board/05-reading.md +2 -1
  126. package/docs/01-board/06-watchers.md +8 -6
  127. package/docs/01-board/08-middleware.md +6 -3
  128. package/docs/01-board/09-access.md +145 -14
  129. package/docs/02-sync/01-overview.md +19 -26
  130. package/docs/02-sync/02-functions.md +8 -9
  131. package/docs/02-sync/03-slack.md +5 -5
  132. package/docs/02-sync/04-github.md +2 -2
  133. package/docs/02-sync/06-providers.md +91 -62
  134. package/docs/03-guides/01-coding-agents.md +1 -1
  135. package/docs/03-guides/03-ci.md +7 -4
  136. package/docs/04-reference/01-cli.md +109 -51
  137. package/docs/04-reference/02-board.md +76 -28
  138. package/docs/04-reference/03-http.md +76 -38
  139. package/docs/04-reference/04-log.md +8 -11
  140. package/docs/04-reference/05-conformance.md +47 -7
  141. package/docs/index.md +2 -2
  142. package/package.json +7 -10
  143. package/sql/postgres/board.sql +1 -1
  144. package/sql/postgres/schema.sql +1 -1
  145. package/dist/conformance-DlnfuzEB.mjs +0 -556
  146. package/dist/errors-CusN4x2l.mjs +0 -88
  147. package/dist/events-qf5XG5VQ.mjs +0 -75
  148. package/dist/exports-mhAYiU4i.d.mts +0 -5
  149. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
  150. package/dist/store-memory-ZLiZbH3R.mjs +0 -2
  151. package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
  152. package/dist/sync-kapso.mjs +0 -89
  153. package/dist/sync.chat.d.mts +0 -2
  154. package/dist/sync.chat.mjs +0 -2
  155. package/dist/users-yryHKFMo.mjs +0 -42
  156. package/docs/02-sync/05-whatsapp.md +0 -152
@@ -1,75 +0,0 @@
1
- import { d as LabelKey, p as Labels } from "./schemas-DBmUO6pr.mjs";
2
- import { n as ExternalEvent, r as ExternalLocation } from "./external-D3p5T57R.mjs";
3
- import { z } from "zod";
4
- //#region src/sync/events.ts
5
- const EventBase = z.object({
6
- id: z.string().min(1),
7
- platform: z.string().min(1),
8
- key: z.string().min(1),
9
- ...ExternalLocation.shape,
10
- raw: z.unknown()
11
- });
12
- const eventTypes = {
13
- message: "message",
14
- edited: "message.edited",
15
- deleted: "message.deleted",
16
- reaction: "reaction.added",
17
- lifecycle: "lifecycle"
18
- };
19
- function envelope(platform, key, event, raw, id) {
20
- return {
21
- ...event,
22
- ...EventBase.parse({
23
- id,
24
- platform,
25
- key,
26
- channel: event.channel,
27
- thread: event.thread,
28
- subthread: "subthread" in event ? event.subthread : null,
29
- raw
30
- }),
31
- type: event.kind === "lifecycle" ? event.type : event.kind === "reaction" && !event.added ? "reaction.removed" : eventTypes[event.kind]
32
- };
33
- }
34
- function eventSchema(platform, messageType) {
35
- const [message, edited, deleted, reaction, lifecycle] = ExternalEvent.options;
36
- const common = {
37
- ...EventBase.shape,
38
- platform: z.literal(platform)
39
- };
40
- return z.discriminatedUnion("kind", [
41
- message.extend({
42
- ...common,
43
- type: z.literal(messageType)
44
- }),
45
- edited.extend({
46
- ...common,
47
- type: z.literal("message.edited")
48
- }),
49
- deleted.extend({
50
- ...common,
51
- type: z.literal("message.deleted")
52
- }),
53
- reaction.extend({
54
- ...common,
55
- type: z.enum(["reaction.added", "reaction.removed"])
56
- }),
57
- lifecycle.extend(common)
58
- ]);
59
- }
60
- const SyncOverrides = z.object({
61
- root: z.custom((value) => typeof value === "function").optional(),
62
- text: z.string().optional(),
63
- labels: Labels.optional(),
64
- unset: z.array(LabelKey).optional(),
65
- noSync: z.boolean().optional()
66
- }).strict();
67
- function immutable(value) {
68
- if (value && typeof value === "object") {
69
- for (const item of Object.values(value)) immutable(item);
70
- Object.freeze(value);
71
- }
72
- return value;
73
- }
74
- //#endregion
75
- export { immutable as a, eventSchema as i, SyncOverrides as n, envelope as r, EventBase as t };
@@ -1,5 +0,0 @@
1
- import "./auth-CU56Q2Xh.mjs";
2
- import "./folds-ZkHM4M-v.mjs";
3
- import "./client-BqAjk0Q4.mjs";
4
- import "./backend-CPC_vyOo.mjs";
5
- import "./errors-DDktVeUa.mjs";
@@ -1,5 +0,0 @@
1
- //#region ../../node_modules/.pnpm/experimental-a2@0.22.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__52060f00b21aecfab049c27fa3abd2f9/node_modules/experimental-a2/dist/store-N8PXxDAS.js
2
- const SYSTEM_CLOCK = { now: () => /* @__PURE__ */ new Date() };
3
- const RANDOM_IDS = () => crypto.randomUUID();
4
- //#endregion
5
- export { SYSTEM_CLOCK as n, RANDOM_IDS as t };
@@ -1,2 +0,0 @@
1
- import { t as memory } from "./store-memory-Bas9Uq5C.mjs";
2
- export { memory };
@@ -1,2 +0,0 @@
1
- import { t as sqlite } from "./store-sqlite-CxxPCcCT.mjs";
2
- export { sqlite };
@@ -1,89 +0,0 @@
1
- import { s as channels } from "./provider-BaVmi4Hi.mjs";
2
- import { i as eventSchema, r as envelope } from "./events-qf5XG5VQ.mjs";
3
- import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-COtsU5jB.mjs";
4
- import { z } from "zod";
5
- //#region src/sync/kapso.ts
6
- const decode = (id) => id.split(":").map((part) => Buffer.from(part, "base64url").toString("utf8")).join(":");
7
- const encode = (id) => id.split(":").map((part) => Buffer.from(part, "utf8").toString("base64url")).join(":");
8
- const WhatsAppEvent = eventSchema("whatsapp", "message");
9
- /**
10
- * Syncs WhatsApp numbers on Kapso with the board: `sync` is what each event
11
- * does, both ways, like `numbers({ '16315558151': 'support' })`. Each Kapso
12
- * conversation is a thread. WhatsApp has no threads in a number, so a board
13
- * thread never starts one: a board root in a mirrored channel stays on the board.
14
- */
15
- function kapso(options, sync) {
16
- return new KapsoProvider(options, sync);
17
- }
18
- /**
19
- * Routes each event by its WhatsApp number. Keys are phone number IDs (`'16315558151'`), not
20
- * the numbers; values are a board channel to `mirror`, or a sync function.
21
- */
22
- const numbers = (map) => channels(map);
23
- var KapsoProvider = class extends ChatSyncProvider {
24
- constructor(options, sync) {
25
- if (typeof options.markRead !== "function") throw new Error("kapso requires a public markRead operation for cross-instance read and typing");
26
- const fileIds = (raw) => (raw?.responses ?? []).flatMap((response) => response.messages?.[0]?.id ?? []);
27
- super({
28
- fileIds,
29
- platform: "whatsapp",
30
- adapter: options.adapter,
31
- canStartThreads: false,
32
- async uploadFiles(thread, files) {
33
- const sent = await options.adapter.postMessage(thread, {
34
- markdown: "",
35
- files
36
- });
37
- return {
38
- message: sent.id,
39
- files: fileIds(sent.raw)
40
- };
41
- },
42
- refusesFiles: (error) => error?.code === "VALIDATION_ERROR",
43
- async resolveChannel(key) {
44
- if (!/^\d+$/.test(key)) throw new Error(`${key} isn't a phone number ID; use the number's ID from Kapso, not the number`);
45
- return key;
46
- },
47
- fromChatId: decode,
48
- toChatId: encode,
49
- singleReaction: true,
50
- async startTyping(thread, latest) {
51
- if (!latest) return options.adapter.startTyping(thread);
52
- const [phoneNumberId] = decode(thread.slice(thread.indexOf(":") + 1)).split(":");
53
- await options.markRead({
54
- phoneNumberId,
55
- messageId: latest,
56
- typingIndicator: { type: "text" }
57
- });
58
- },
59
- groupThread: (thread) => thread,
60
- isRoot: () => false,
61
- context: () => null,
62
- markdown(message) {
63
- const text = markdownOf(message);
64
- const transcript = message.raw?.kapso?.transcript?.text;
65
- return [text, typeof transcript === "string" && transcript.trim() && transcript !== text ? `> ${transcript.trim()}` : null].filter(Boolean).join("\n\n");
66
- },
67
- async resolveTarget(target) {
68
- const parts = target.split(":");
69
- if (!/^\d+$/.test(parts[0]) || parts.length !== 1 && parts.length !== 3) throw new Error(`invalid Kapso target: ${target}`);
70
- return {
71
- target,
72
- channel: parts[0],
73
- kind: parts.length === 1 ? "channel" : "thread"
74
- };
75
- }
76
- }, sync);
77
- }
78
- eventId(key, input) {
79
- if (input.kind !== "reaction") return super.eventId(key, input);
80
- const reaction = input.reaction;
81
- const stamp = z.looseObject({ id: z.string().min(1) }).parse(reaction.raw).id;
82
- return `reaction:${key}:${this.toNative(reaction.threadId)}|${reaction.messageId}|${reaction.rawEmoji}|${reaction.user.userId}|${reaction.added}|${stamp}`;
83
- }
84
- event(context) {
85
- return WhatsAppEvent.parse(envelope(this.platform, context.key, context.event, context.raw, context.id));
86
- }
87
- };
88
- //#endregion
89
- export { KapsoProvider, WhatsAppEvent, kapso, numbers };
@@ -1,2 +0,0 @@
1
- import { i as externalAuthor, n as ChatSyncProvider, r as ChatTransport, t as ChatInput } from "./chat-provider-BWHbChcQ.mjs";
2
- export { ChatInput, ChatSyncProvider, ChatTransport, externalAuthor };
@@ -1,2 +0,0 @@
1
- import { n as externalAuthor, t as ChatSyncProvider } from "./chat-provider-COtsU5jB.mjs";
2
- export { ChatSyncProvider, externalAuthor };
@@ -1,42 +0,0 @@
1
- //#region src/sync/users.ts
2
- const USER_TOKENS = Symbol.for("board.userTokens");
3
- /**
4
- * Gives an adapter people's tokens, for your own OAuth: `slack({ adapter:
5
- * withUserTokens(createSlackAdapter(...), { authorize, token }) }, sync)`. A
6
- * Connect connector name (`slack('slack/acme', sync)`) does this for you.
7
- */
8
- function withUserTokens(adapter, tokens) {
9
- Object.defineProperty(adapter, USER_TOKENS, {
10
- value: tokens,
11
- enumerable: false,
12
- configurable: true
13
- });
14
- return adapter;
15
- }
16
- const userTokensOf = (adapter) => adapter[USER_TOKENS] ?? null;
17
- /** Vercel Connect's user tokens for `connector`: the subject is the board's principal, so Connect binds the account to it. */
18
- function connectUserTokens(connector) {
19
- const subject = (principal) => ({ subject: {
20
- type: "user",
21
- id: principal
22
- } });
23
- return {
24
- async authorize(principal, options = {}) {
25
- const { startAuthorization } = await import("@vercel/connect");
26
- return (options.callback ? await startAuthorization(connector, subject(principal), { callbackUrl: options.callback }) : await startAuthorization(connector, subject(principal))).url;
27
- },
28
- async token(principal) {
29
- const { getToken, NoValidTokenError, UserAuthorizationRequiredError } = await import("@vercel/connect");
30
- try {
31
- return await getToken(connector, subject(principal));
32
- } catch (error) {
33
- if (error instanceof UserAuthorizationRequiredError || error instanceof NoValidTokenError) return null;
34
- const code = error.code;
35
- if (code === "user_authorization_required" || code === "no_token") return null;
36
- throw error;
37
- }
38
- }
39
- };
40
- }
41
- //#endregion
42
- export { userTokensOf as n, withUserTokens as r, connectUserTokens as t };
@@ -1,152 +0,0 @@
1
- ---
2
- title: WhatsApp
3
- description: Sync WhatsApp conversations on a business number with board threads, through Kapso.
4
- ---
5
-
6
- ## Configure it
7
-
8
- ```package-install
9
- npm i chat @kapso/chat-adapter @kapso/whatsapp-cloud-api
10
- ```
11
-
12
- `kapso({ adapter, markRead }, sync)` takes
13
- [Kapso's Chat SDK adapter](https://chat-sdk.dev/adapters/vendor-official/kapso),
14
- a read operation, and a [sync function](./02-functions.md). `numbers` makes one
15
- from a map of WhatsApp phone number IDs to board channels (two-way mirrors) or
16
- functions of your own:
17
-
18
- ```ts app/api/board/[[...path]]/route.ts
19
- import { board } from '@vercel/board'
20
- import { a2 } from '@vercel/board/backend-a2'
21
- import { vercelQueues } from '@vercel/board/backend-a2/scheduler-vercel'
22
- import { postgres } from '@vercel/board/backend-a2/store-postgres'
23
- import { kapso, numbers } from '@vercel/board/sync-kapso'
24
- import { createKapsoAdapter } from '@kapso/chat-adapter'
25
- import { WhatsAppClient } from '@kapso/whatsapp-cloud-api'
26
-
27
- const client = new WhatsAppClient({
28
- baseUrl: 'https://api.kapso.ai/meta/whatsapp',
29
- kapsoApiKey: process.env.KAPSO_API_KEY!,
30
- })
31
-
32
- const backend = a2({
33
- store: postgres({ connectionString: process.env.DATABASE_URL! }),
34
- scheduler: vercelQueues({ topic: 'board' }),
35
- })
36
-
37
- const team = board({
38
- backend,
39
- sync: {
40
- kapso: kapso(
41
- {
42
- adapter: createKapsoAdapter({ client }),
43
- markRead: (input) => client.messages.markRead(input),
44
- },
45
- numbers({ '16315558151': 'support' }),
46
- ),
47
- },
48
- })
49
- export const GET = team.fetch, POST = team.fetch
50
- export const { scheduler } = backend
51
- ```
52
-
53
- Keys are phone number IDs as Kapso shows them, not the phone numbers themselves.
54
- A key that isn't one is skipped with a warning that says so.
55
-
56
- `markRead` is required at setup. The adapter only remembers messages whose webhook
57
- reached its own instance, so typing and read receipts go through this operation
58
- instead, which works from any serverless instance.
59
-
60
- | Thing | ID |
61
- | --- | --- |
62
- | A business number, the channel | `kapso:16315558151` |
63
- | A Kapso conversation, a thread | `kapso:16315558151:<WhatsApp ID>:<conversation ID>` |
64
-
65
- Kapso's adapter encodes each part of its IDs; the board writes them decoded, so
66
- targets and `vb inbox` read as above.
67
-
68
- ## Conversations are threads
69
-
70
- WhatsApp has no threads, so a contact's messages group into Kapso's conversations.
71
- One starts when they write, and ends after a day of quiet or when someone closes it
72
- in Kapso. Writing again after that starts a new board thread.
73
-
74
- - **Authors** are `whatsapp:user:<WhatsApp ID>`, named by their WhatsApp profile
75
- name.
76
- - **The board never starts a conversation.** A number has no threads to start, so
77
- a root posted in a mirrored channel stays on the board.
78
- - **Board messages post from the business number,** with the author's name in bold
79
- (`*julian*: refunded`). To post the text alone, wrap the function in
80
- `render(({ message }) => message.text, …)`; see [render](./02-functions.md#render).
81
-
82
- ## Reply within the window
83
-
84
- WhatsApp takes free-form messages only within 24 hours of the contact's last one.
85
- A board reply after that fails, and is recorded as `message.sync_failed`, which
86
- `vb read` shows under it:
87
-
88
- ```text
89
- ✗ didn't reach kapso:16315558151:51987654321:conv_123: <WhatsApp's error>
90
- ```
91
-
92
- Templates aren't synced; send them with `@kapso/whatsapp-cloud-api`.
93
-
94
- ## Reactions
95
-
96
- WhatsApp keeps one reaction per person per message. A contact's new reaction
97
- replaces their old one on the board, and removing it clears it. The business number
98
- has one too: the board's reactions on a WhatsApp message show there as the latest
99
- one added, and removing any of them clears it.
100
-
101
- ## Media
102
-
103
- An image, video, voice note, or document becomes an attachment on the board,
104
- fetched through Kapso, with its caption as the text. A voice note's transcript is
105
- quoted under it when Kapso has one. A board without a blob store keeps no files:
106
- each is a line in the text instead, as on [Slack](./03-slack.md#files).
107
-
108
- A board message's files follow its text to WhatsApp, each as its own message.
109
- WhatsApp can't delete them, so they stay when the board message is deleted.
110
-
111
- ## Typing
112
-
113
- ```ts
114
- // in the route that mounts the board with its `sync`:
115
- await team.typing('812')
116
- ```
117
-
118
- Shows "typing…" to the contact and marks their latest message read (blue ticks).
119
- WhatsApp shows no status text, so a status is ignored.
120
-
121
- ## Route by function
122
-
123
- A function for a number receives every event for it, both ways. Importing a
124
- message into a channel connects its conversation, and delivering sends board
125
- replies in that thread to the contact:
126
-
127
- ```ts
128
- // in kapso({ … }, numbers({ … })):
129
- '16315558151': async (sync) => {
130
- if (sync.direction === 'out') return sync.deliver()
131
- if (sync.event.kind !== 'message') return
132
- await sync.import('leads', { text: `${sync.event.author.name}: ${sync.event.text}` })
133
- },
134
- ```
135
-
136
- See [sync functions](./02-functions.md) for the event and the overrides.
137
-
138
- ## What doesn't sync
139
-
140
- - Edits and deletes, which WhatsApp can't do for recipients.
141
- - Button and list replies.
142
-
143
- ## Setup
144
-
145
- 1. In Kapso, create an API key and copy the number's phone number ID.
146
- 2. Add a webhook with the URL `https://<host>/api/board/webhooks/kapso`, a secret
147
- key, and the `whatsapp.message.received` event.
148
- 3. Set `KAPSO_API_KEY` and `KAPSO_WEBHOOK_SECRET`, or pass `webhookSecret` to
149
- `createKapsoAdapter`. Without the secret, the adapter rejects every webhook.
150
-
151
- The webhook path ends in the entry's key, so an entry keyed `kapso` listens at
152
- `/webhooks/kapso`.