@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.
- package/README.md +8 -17
- package/dist/{a2-HUbQJ0RD.mjs → a2-DngWY2Eb.mjs} +741 -415
- package/dist/access-CY5xVMIK.mjs +242 -0
- 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-DcJhFbwJ.mjs → actors-Bm-ojWWT.mjs} +2 -2
- package/dist/{auth-JO22iIwr.mjs → auth-CyO2aw5n.mjs} +126 -236
- package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
- package/dist/{backend-CPC_vyOo.d.mts → backend-Bob-5CM7.d.mts} +93 -42
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +38 -98
- package/dist/backend-a2/contract.mjs +1 -1
- 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 +3 -3
- package/dist/backend-a2/scheduler-qstash.mjs +8 -8
- package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
- package/dist/backend-a2/scheduler-vercel.mjs +6 -6
- 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 +34 -15
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +6 -6
- package/dist/backend-a2/store-redis.d.mts +2 -2
- package/dist/backend-a2/store-redis.mjs +5 -5
- 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 +143 -242
- package/dist/backend-a2.mjs +2 -2
- package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-BRrRbjRx.d.mts} +59 -45
- package/dist/{backend-http-CzIJMm58.mjs → backend-http-CvUKOn-m.mjs} +11 -10
- package/dist/{backend-http-Bmp0IgD3.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-DQc_TaHp.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
- package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-BaSEUdwg.d.mts} +62 -47
- package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-CmY9yZ9m.mjs} +279 -42
- 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-DmiQjrYy.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
- package/dist/{blob-DdbpJqN1.mjs → blob-WbB2DJKn.mjs} +30 -11
- package/dist/{chat-provider-BWHbChcQ.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-BqAjk0Q4.d.mts → client-COfYrgfV.d.mts} +240 -227
- package/dist/{commands-D6gvrOh3.d.mts → commands-DK4W3HCJ.d.mts} +47 -10
- package/dist/{commands-C3R1AWgm.mjs → commands-L1JSGz2Z.mjs} +212 -54
- package/dist/conformance-DK43HiHf.mjs +1000 -0
- package/dist/conformance.d.mts +8 -2
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-DQxXk7dX.mjs → engine-DurYIRt8.mjs} +29 -210
- package/dist/{errors-DDktVeUa.d.mts → errors-2vGs49Db.d.mts} +1 -1
- package/dist/{client-D0Px7KzF.mjs → errors-CJXhhzvT.mjs} +132 -27
- package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
- package/dist/{exports-BG-0Wyn-.mjs → exports-CWIdN0Ub.mjs} +195 -103
- package/dist/exports-FBOQWT7O.d.mts +5 -0
- package/dist/{folds-BDMSxBUf.mjs → folds-Cu9Q9zyQ.mjs} +3 -22
- package/dist/{folds-ZkHM4M-v.d.mts → folds-DLUzfibJ.d.mts} +15 -40
- package/dist/{index-D3VvNfh_.d.mts → index-Ho-ayyQL.d.mts} +2 -2
- package/dist/index.d.mts +7 -7
- package/dist/{index.default-rB8sJ9RX.mjs → index.default-BUcwDaAe.mjs} +1 -1
- package/dist/index.mjs +5 -6
- package/dist/index.node.d.mts +7 -7
- package/dist/index.node.mjs +3 -4
- package/dist/inspect/app.js +62 -61
- package/dist/inspect/engine.js +3 -3
- package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
- package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
- package/dist/{main-CVBqe3VO.mjs → main-Dz-8komG.mjs} +251 -127
- package/dist/{middleware-CXjEeef4.mjs → middleware-BavYSDUa.mjs} +1 -1
- package/dist/{node-Zd_C-gxP.mjs → node-BgeGcwX0.mjs} +7 -3
- package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
- package/dist/{project-CBXlLftU.mjs → project-CQ4VUibA.mjs} +1 -1
- package/dist/{provider-DmSTp9lI.d.mts → provider-gynDUoyW.d.mts} +13 -43
- package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-DHf85ixI.mjs} +40 -42
- package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
- package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
- package/dist/{reducers-DJzVpXwM.mjs → reducers-DcENwtwZ.mjs} +74 -77
- package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
- package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
- package/dist/{serve-G9RuaDSr.mjs → serve-CTmow-YH.mjs} +7 -7
- package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
- package/dist/{server-BixV_byP.mjs → server-DG8D6VV0.mjs} +4 -4
- package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
- package/dist/{serving-B75Y4BF-.mjs → serving-DBFRo6U_.mjs} +1 -1
- package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
- package/dist/{sql-D4Z_efJX.mjs → sql-CT5xVuGr.mjs} +2 -8
- package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
- package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
- package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
- package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
- package/dist/store-memory-4IBl0Ztu.mjs +2 -0
- package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
- package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
- package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
- package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
- package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
- package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
- 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 +6 -5
- 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-CC3izSNr.mjs → tokens-Dnyd_y1n.mjs} +15 -6
- package/dist/{types-No0WdhE5.d.mts → types-CmrFjpml.d.mts} +2 -4
- package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-BjwlnGD9.mjs} +124 -115
- 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 +96 -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 +6 -3
- 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 +109 -51
- package/docs/04-reference/02-board.md +76 -28
- package/docs/04-reference/03-http.md +76 -38
- package/docs/04-reference/04-log.md +8 -11
- package/docs/04-reference/05-conformance.md +47 -7
- package/docs/index.md +2 -2
- package/package.json +7 -10
- package/sql/postgres/board.sql +1 -1
- package/sql/postgres/schema.sql +1 -1
- package/dist/conformance-DlnfuzEB.mjs +0 -556
- package/dist/errors-CusN4x2l.mjs +0 -88
- package/dist/events-qf5XG5VQ.mjs +0 -75
- package/dist/exports-mhAYiU4i.d.mts +0 -5
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
- package/dist/store-memory-ZLiZbH3R.mjs +0 -2
- package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
- 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
package/dist/events-qf5XG5VQ.mjs
DELETED
|
@@ -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
|
-
//#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 };
|
package/dist/sync-kapso.mjs
DELETED
|
@@ -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 };
|
package/dist/sync.chat.d.mts
DELETED
package/dist/sync.chat.mjs
DELETED
package/dist/users-yryHKFMo.mjs
DELETED
|
@@ -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`.
|