@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,556 +0,0 @@
1
- import { t as BoardError } from "./client-D0Px7KzF.mjs";
2
- //#region src/conformance/index.ts
3
- var Mismatch = class extends Error {};
4
- const show = (value) => JSON.stringify(value) ?? String(value);
5
- function check(condition, message) {
6
- if (!condition) throw new Mismatch(message);
7
- }
8
- function same(actual, expected, what) {
9
- if (actual !== expected) throw new Mismatch(`${what}: expected ${show(expected)}, got ${show(actual)}`);
10
- }
11
- async function rejects(attempt, status, code, what) {
12
- try {
13
- await attempt;
14
- } catch (error) {
15
- if (error instanceof BoardError && error.status === status && error.code === code) return;
16
- throw new Mismatch(`${what}: expected ${status} ${code}, got ${error instanceof BoardError ? `${error.status} ${error.code}` : String(error)}`);
17
- }
18
- throw new Mismatch(`${what}: expected ${status} ${code}, and it succeeded`);
19
- }
20
- /** Where a registration is woken, to put back after a check replaces it: its path (and branch) on a board that wakes by path, or its url. */
21
- function wokenAt(registered) {
22
- if (!registered?.path) return { url: registered?.url ?? null };
23
- return {
24
- path: registered.path,
25
- ...registered.branch ? { branch: registered.branch } : {}
26
- };
27
- }
28
- /** An ID or cursor as the protocol shapes them: a string of 1 to 128 characters. */
29
- function isRef(value, what) {
30
- check(typeof value === "string" && value.length >= 1 && value.length <= 128, `${what} should be a string of 1 to 128 characters, got ${show(value)}`);
31
- }
32
- /**
33
- * The groups a board may keep none of, saying so with a 501: a board on `a2()` without a scheduler
34
- * answers `no_scheduler`, and without a blob store `no_files`.
35
- */
36
- const OPTIONAL = /* @__PURE__ */ new Set(["schedules", "attachments"]);
37
- const needRoot = (ctx) => {
38
- check(ctx.root && ctx.reply, "posting failed, so there's nothing to read");
39
- return {
40
- root: ctx.root,
41
- reply: ctx.reply
42
- };
43
- };
44
- async function post(ctx, text, extra = {}) {
45
- const message = await ctx.board.post(ctx.channel, {
46
- text,
47
- labels: ctx.labels,
48
- noSync: true,
49
- ...extra
50
- });
51
- ctx.posted.push(message.id);
52
- return message;
53
- }
54
- const CHECKS = [
55
- {
56
- group: "core",
57
- name: "posts a message, and reads it back by its ID",
58
- run: async (ctx) => {
59
- const root = await post(ctx, "conformance: a root");
60
- isRef(root.id, "a message's id");
61
- check(typeof root.version === "number", `a message's version should be a number, got ${show(root.version)}`);
62
- const thread = await ctx.board.thread(root.id);
63
- same(thread.message.id, root.id, "the thread read by that id");
64
- same(thread.message.text, "conformance: a root", "its text");
65
- ctx.root = root;
66
- const reply = await ctx.board.reply(root.id, {
67
- text: "conformance: a reply",
68
- noSync: true
69
- });
70
- ctx.posted.push(reply.id);
71
- ctx.reply = reply;
72
- }
73
- },
74
- {
75
- group: "core",
76
- name: "puts a reply in its root's thread",
77
- run: async (ctx) => {
78
- const { root, reply } = needRoot(ctx);
79
- same(reply.re, root.id, "the reply's re");
80
- check((await ctx.board.thread(root.id)).replies.some((m) => m.id === reply.id), `the thread's replies should include ${reply.id}`);
81
- }
82
- },
83
- {
84
- group: "core",
85
- name: "posts once for a client ID, however many times it is sent",
86
- run: async (ctx) => {
87
- const id = `conformance-${ctx.labels.conformance}`;
88
- const first = await post(ctx, "conformance: once", { id });
89
- same((await post(ctx, "conformance: once", { id })).id, first.id, "the second post with the same client id");
90
- same((await ctx.board.posted(id)).id, first.id, "the post found by its client id");
91
- await rejects(ctx.board.posted(`${id}-never`), 404, "post_not_found", "a client id nothing was posted with");
92
- }
93
- },
94
- {
95
- group: "core",
96
- name: "raises the version with each edit, reaction, and label",
97
- run: async (ctx) => {
98
- const { root } = needRoot(ctx);
99
- const version = async () => (await ctx.board.thread(root.id)).message;
100
- let before = await version();
101
- for (const [what, change] of [
102
- ["an edit", () => ctx.board.edit(root.id, "conformance: an edited root")],
103
- ["a reaction", () => ctx.board.react(root.id, "eyes")],
104
- ["a label", () => ctx.board.label(root.id, { set: { state: "open" } })]
105
- ]) {
106
- await change();
107
- const after = await version();
108
- check(after.version > before.version, `${what} should raise the version past ${before.version}, got ${after.version}`);
109
- before = after;
110
- }
111
- same(before.text, "conformance: an edited root", "the edited text");
112
- check(before.editedAt !== null, "an edited message should have editedAt");
113
- check(before.reactions.eyes?.includes(ctx.me.actor), `reactions.eyes should include ${ctx.me.actor}, got ${show(before.reactions)}`);
114
- same(before.labels.state, "open", "the label set");
115
- await ctx.board.label(root.id, { unset: ["state"] });
116
- same((await version()).labels.state, void 0, "the label unset");
117
- }
118
- },
119
- {
120
- group: "core",
121
- name: "matches a label on the message or its thread's root",
122
- run: async (ctx) => {
123
- const { reply } = needRoot(ctx);
124
- const { messages } = await ctx.board.messages({
125
- channel: ctx.channel,
126
- label: ctx.labels
127
- });
128
- check(messages.some((m) => m.id === reply.id), "a reply should match its root's label");
129
- }
130
- },
131
- {
132
- group: "core",
133
- name: "reads a channel after a cursor or a message ID, oldest first",
134
- run: async (ctx) => {
135
- const { root, reply } = needRoot(ctx);
136
- const page = await ctx.board.messages({
137
- channel: ctx.channel,
138
- label: ctx.labels,
139
- after: ctx.start
140
- });
141
- isRef(page.cursor, "a read's cursor");
142
- const ids = page.messages.map((m) => m.id);
143
- check(ids.indexOf(root.id) !== -1 && ids.indexOf(root.id) < ids.indexOf(reply.id), `the root should come before its reply, got ${show(ids)}`);
144
- const later = await ctx.board.messages({
145
- channel: ctx.channel,
146
- label: ctx.labels,
147
- after: root.id
148
- });
149
- check(!later.messages.some((m) => m.id === root.id), "reading after a message should leave it out");
150
- check(later.messages.some((m) => m.id === reply.id), "reading after a message should include what came after it");
151
- }
152
- },
153
- {
154
- group: "core",
155
- name: "lists threads, each with its reply count and latest reply",
156
- run: async (ctx) => {
157
- const { root, reply } = needRoot(ctx);
158
- const { threads } = await ctx.board.threads({
159
- channel: ctx.channel,
160
- label: ctx.labels
161
- });
162
- const found = threads.find((t) => t.message.id === root.id);
163
- check(found, `the threads should include ${root.id}`);
164
- same(found.replies, 1, "its reply count");
165
- same(found.last?.id, reply.id, "its latest reply");
166
- }
167
- },
168
- {
169
- group: "core",
170
- name: "reads events after a cursor, each with a cursor of its own",
171
- run: async (ctx) => {
172
- const { root } = needRoot(ctx);
173
- const page = await ctx.board.poll({
174
- channel: ctx.channel,
175
- after: ctx.start
176
- });
177
- isRef(page.cursor, "the events' cursor");
178
- const posted = page.events.find((event) => event.type === "message.posted" && event.message?.id === root.id);
179
- check(posted, `the events should include ${root.id} being posted`);
180
- isRef(posted.cursor, "an event's cursor");
181
- check(!(await ctx.board.poll({
182
- channel: ctx.channel,
183
- after: page.cursor
184
- })).events.some((event) => event.message?.id === root.id), "reading on from the cursor should not repeat what it read");
185
- }
186
- },
187
- {
188
- group: "core",
189
- name: "holds a long-poll open until the next event",
190
- run: async (ctx) => {
191
- const { cursor } = await ctx.board.poll({
192
- channel: ctx.channel,
193
- label: ctx.labels
194
- });
195
- const waiting = ctx.board.poll({
196
- channel: ctx.channel,
197
- label: ctx.labels,
198
- after: cursor,
199
- wait: 10
200
- });
201
- await new Promise((resolve) => setTimeout(resolve, 200));
202
- const later = await post(ctx, "conformance: while it waits");
203
- check((await waiting).events.some((event) => event.message?.id === later.id), "the long-poll should return the post made while it waited");
204
- }
205
- },
206
- {
207
- group: "core",
208
- name: "deletes a message, leaving a tombstone",
209
- run: async (ctx) => {
210
- const message = await post(ctx, "conformance: to delete");
211
- await ctx.board.delete(message.id);
212
- const { message: after } = await ctx.board.thread(message.id);
213
- check(after.deleted && after.text === "", `a deleted message should be deleted with no text, got ${show({
214
- deleted: after.deleted,
215
- text: after.text
216
- })}`);
217
- }
218
- },
219
- {
220
- group: "core",
221
- name: "lists the channel and you in it",
222
- run: async (ctx) => {
223
- const found = (await ctx.board.channels()).find((c) => c.path === ctx.channel);
224
- check(found, `the channels should include ${ctx.channel}`);
225
- check(typeof found.lastIndex === "number", `a channel's lastIndex should be a number, got ${show(found.lastIndex)}`);
226
- check((await ctx.board.actors()).some((a) => a.id === ctx.me.actor), `the actors should include ${ctx.me.actor}`);
227
- }
228
- },
229
- {
230
- group: "core",
231
- name: "answers 404 for an ID that names nothing",
232
- run: async (ctx) => {
233
- await rejects(ctx.board.thread(`no-such-message-${ctx.labels.conformance}`), 404, "message_not_found", "reading a message that was never posted");
234
- }
235
- },
236
- {
237
- group: "core",
238
- name: "answers 400 for a request that does not validate",
239
- run: async (ctx) => {
240
- await rejects(ctx.board.post("Not A Channel", {
241
- text: "conformance",
242
- noSync: true
243
- }), 400, "invalid_request", "posting to a malformed channel");
244
- }
245
- },
246
- {
247
- group: "schedules",
248
- name: "schedules a reply, lists it, and cancels it",
249
- run: async (ctx) => {
250
- const { root } = needRoot(ctx);
251
- const schedule = await ctx.board.schedule({ re: root.id }, {
252
- text: "conformance: later",
253
- noSync: true,
254
- in: "1d"
255
- });
256
- isRef(schedule.id, "a schedule's id");
257
- same(schedule.re, root.id, "the schedule's re");
258
- same(schedule.state, "pending", "the schedule's state");
259
- check((await ctx.board.schedules({ re: root.id })).some((s) => s.id === schedule.id), `the schedules should include ${schedule.id}`);
260
- const { schedule: canceled } = await ctx.board.cancelSchedule(schedule.id);
261
- same(canceled.state, "canceled", "the canceled schedule");
262
- }
263
- },
264
- {
265
- group: "attachments",
266
- name: "takes a file, posts it, and gives it back",
267
- run: async (ctx) => {
268
- const bytes = new TextEncoder().encode(`conformance ${ctx.labels.conformance}\n`);
269
- const attachment = await ctx.board.upload({
270
- data: bytes,
271
- name: "conformance.txt",
272
- type: "text/plain"
273
- });
274
- check((await post(ctx, "conformance: a file", { attachments: [attachment.id] })).attachments.some((a) => a.id === attachment.id), "the post should carry the file");
275
- const response = await ctx.board.download(attachment.id);
276
- same(new TextDecoder().decode(await response.arrayBuffer()), new TextDecoder().decode(bytes), "the file's bytes");
277
- }
278
- },
279
- {
280
- group: "watchers",
281
- name: "matches a registered tail, hands the match to a pull, and takes its ack",
282
- run: async (ctx) => {
283
- const registered = (await ctx.board.watchers()).find((r) => r.owner === ctx.me.principal);
284
- if (registered && (Object.keys(registered.watch).length > 0 || (registered.sync?.length ?? 0) > 0)) return "this token has watchers or a sync registered here, and checking would replace them";
285
- const where = wokenAt(registered);
286
- await ctx.board.register({
287
- url: null,
288
- watch: { conformance: { tail: `${ctx.channel} --label conformance=${ctx.labels.conformance}` } }
289
- });
290
- try {
291
- const message = await post(ctx, "conformance: for a watcher");
292
- const deadline = Date.now() + 1e4;
293
- let match;
294
- while (!match && Date.now() < deadline) match = (await ctx.board.matches({ wait: 5 })).find((m) => m.event.message?.id === message.id);
295
- check(match, "a pull should hand out the match for the post");
296
- same(match.watcher, "conformance", "the match's watcher");
297
- await ctx.board.ack(match.id, { lease: match.lease });
298
- await rejects(ctx.board.ack(match.id, { lease: match.lease }), 404, "no_match", "acking a match twice");
299
- } finally {
300
- await ctx.board.register({
301
- ...where,
302
- watch: {}
303
- });
304
- }
305
- }
306
- },
307
- {
308
- group: "watchers",
309
- name: "wakes by path only where its discovery says so, and only a Vercel project",
310
- run: async (ctx) => {
311
- const registered = (await ctx.board.watchers()).find((r) => r.owner === ctx.me.principal);
312
- if (registered && (Object.keys(registered.watch).length > 0 || (registered.sync?.length ?? 0) > 0)) return "this token has watchers or a sync registered here, and checking would replace them";
313
- const byPath = (await ctx.board.discovery()).wake_by_path;
314
- const environment = /^vercel:project:[^:]+:(.+)$/.exec(ctx.me.principal)?.[1] ?? null;
315
- if (byPath && environment !== null && environment !== "production" && environment !== "preview") return `a path wakes production or a preview, and this token is ${environment}'s`;
316
- const where = wokenAt(registered);
317
- const branch = environment === "preview" ? { branch: "conformance" } : {};
318
- const attempt = () => ctx.board.register({
319
- path: "/conformance",
320
- ...branch,
321
- watch: {}
322
- });
323
- try {
324
- if (!byPath) await rejects(attempt(), 501, "no_wake_by_path", "a path, on a board that wakes only urls");
325
- else if (environment === null) await rejects(attempt(), 403, "not_a_project", "a path, from a token that isn't a Vercel project's");
326
- else {
327
- const view = await attempt();
328
- same(view.path, "/conformance", "the registration's path");
329
- same(view.url, null, "the registration's url");
330
- same(view.branch ?? null, environment === "preview" ? "conformance" : null, "the registration's branch");
331
- }
332
- } finally {
333
- await ctx.board.register({
334
- ...where,
335
- watch: {}
336
- });
337
- }
338
- }
339
- },
340
- {
341
- group: "sync",
342
- name: "keeps sync's records: an import as a platform author, its lookups by platform ID, a receipt, and where it's synced",
343
- run: async (ctx) => {
344
- const key = "conformance";
345
- const thread = `${key}:${ctx.labels.conformance}:1`;
346
- const author = {
347
- id: `${key}:user:${ctx.labels.conformance}`,
348
- handle: "conformance",
349
- name: "Conformance",
350
- avatar: null,
351
- isBot: false
352
- };
353
- const origin = {
354
- thread,
355
- message: "1"
356
- };
357
- const registered = (await ctx.board.watchers()).find((registration) => registration.owner === ctx.me.principal);
358
- if (registered && (Object.keys(registered.watch).length > 0 || (registered.sync?.length ?? 0) > 0)) return "this token has watchers or a sync registered here, which registering would replace";
359
- const where = wokenAt(registered);
360
- try {
361
- await ctx.board.register({
362
- url: null,
363
- watch: {},
364
- sync: [{
365
- key,
366
- platform: key
367
- }]
368
- });
369
- } catch (error) {
370
- if (error instanceof BoardError && error.status === 403) return "this token can't register a sync here";
371
- throw error;
372
- }
373
- let imported;
374
- try {
375
- imported = await ctx.board.syncImport({
376
- op: "post",
377
- author,
378
- from: key,
379
- channel: ctx.channel,
380
- text: "conformance: from a platform",
381
- labels: ctx.labels,
382
- noSync: true,
383
- origin: {
384
- channel: `${key}:${ctx.labels.conformance}`,
385
- thread,
386
- subthread: null,
387
- message: "1",
388
- context: null
389
- },
390
- id: `conformance-sync-${ctx.labels.conformance}`
391
- });
392
- } catch (error) {
393
- await ctx.board.register({
394
- ...where,
395
- watch: {}
396
- }).catch(() => {});
397
- if (error instanceof BoardError && error.status === 403) return `this token can't sync in ${ctx.channel}`;
398
- throw error;
399
- }
400
- const id = imported.message;
401
- isRef(id, "an import's message");
402
- try {
403
- const message = (await ctx.board.thread(id)).message;
404
- same(message.by.actor, author.id, "the import's author");
405
- same(message.from, key, "the import's from");
406
- same(message.origin?.thread, thread, "the import's origin");
407
- const found = await ctx.board.syncFind({
408
- thread,
409
- message: "1"
410
- });
411
- same(found.message, id, "the message found by its platform IDs");
412
- check(found.thread !== null, "the thread found by its platform thread");
413
- same((await ctx.board.syncThread(found.thread)).root, id, "the synced thread's root");
414
- same((await ctx.board.syncImport({
415
- op: "post",
416
- author,
417
- from: key,
418
- channel: ctx.channel,
419
- text: "conformance: from a platform",
420
- noSync: true,
421
- id: `conformance-sync-${ctx.labels.conformance}`
422
- })).message, id, "the same import, sent again");
423
- await rejects(ctx.board.syncImport({
424
- op: "post",
425
- author,
426
- from: key,
427
- channel: ctx.channel,
428
- text: "conformance: a second root",
429
- noSync: true,
430
- origin: {
431
- thread,
432
- subthread: null,
433
- message: "3",
434
- context: null
435
- }
436
- }), 409, "already_synced", "an import from a platform thread another board thread syncs with");
437
- const external = {
438
- thread: `${key}:${ctx.labels.conformance}:2`,
439
- message: "2"
440
- };
441
- await ctx.board.syncReceipt({
442
- kind: "thread",
443
- message: id,
444
- target: external.thread,
445
- targetKind: "thread",
446
- cause: "request"
447
- });
448
- await ctx.board.syncReceipt({
449
- kind: "message",
450
- message: id,
451
- target: external.thread,
452
- external
453
- });
454
- same(JSON.stringify((await ctx.board.syncCopies([id]))[id]?.[external.thread]), JSON.stringify(external), "the copy its receipt recorded");
455
- const { targets } = await ctx.board.sync(id);
456
- check(targets.some((row) => row.target === external.thread && row.message === external.message), `where the thread is synced should show the copy its receipt recorded, got ${show(targets)}`);
457
- await rejects(ctx.board.linked(`https://conformance.invalid/${ctx.labels.conformance}`), 404, "message_not_found", "a link no sync here knows");
458
- } finally {
459
- await ctx.board.syncImport({
460
- op: "delete",
461
- author,
462
- from: key,
463
- origin
464
- }).catch(() => {});
465
- await ctx.board.syncImport({
466
- op: "delete",
467
- author,
468
- from: key,
469
- origin: {
470
- thread,
471
- message: "3"
472
- }
473
- }).catch(() => {});
474
- await ctx.board.register({
475
- ...where,
476
- watch: {}
477
- }).catch(() => {});
478
- }
479
- }
480
- }
481
- ];
482
- /** Runs the checks against `board`, in order, and reports each. It never throws for what the board answers. */
483
- async function conformance(board, options = {}) {
484
- const checks = [];
485
- const discovery = await board.discovery().catch((error) => error);
486
- const wrong = discovery instanceof Error ? discovery.message : typeof discovery.audience !== "string" || typeof discovery.url !== "string" || !Array.isArray(discovery.sync) ? `discovery should have url, audience, and sync, got ${show(discovery)}` : null;
487
- if (wrong !== null) {
488
- checks.push({
489
- group: "core",
490
- name: "says in its discovery where it is",
491
- status: "failed",
492
- detail: wrong
493
- });
494
- return {
495
- url: board.url,
496
- checks,
497
- ok: false
498
- };
499
- }
500
- const url = discovery.url;
501
- checks.push({
502
- group: "core",
503
- name: "says in its discovery where it is",
504
- status: "passed"
505
- });
506
- const me = await board.me();
507
- const channel = options.channel ?? "conformance";
508
- const ctx = {
509
- board,
510
- channel,
511
- labels: { conformance: crypto.randomUUID().slice(0, 8) },
512
- start: (await board.messages({
513
- channel,
514
- limit: 1
515
- })).cursor,
516
- me,
517
- posted: []
518
- };
519
- try {
520
- for (const { group, name, run } of CHECKS) try {
521
- const skipped = await run(ctx);
522
- checks.push(skipped ? {
523
- group,
524
- name,
525
- status: "skipped",
526
- detail: skipped
527
- } : {
528
- group,
529
- name,
530
- status: "passed"
531
- });
532
- } catch (error) {
533
- if (OPTIONAL.has(group) && error instanceof BoardError && error.status === 501) checks.push({
534
- group,
535
- name,
536
- status: "skipped",
537
- detail: `not kept here: ${error.message}`
538
- });
539
- else checks.push({
540
- group,
541
- name,
542
- status: "failed",
543
- detail: error instanceof Error ? error.message : String(error)
544
- });
545
- }
546
- } finally {
547
- for (const id of [...ctx.posted].reverse()) await board.delete(id).catch(() => {});
548
- }
549
- return {
550
- url,
551
- checks,
552
- ok: checks.every((c) => c.status !== "failed")
553
- };
554
- }
555
- //#endregion
556
- export { conformance as t };
@@ -1,88 +0,0 @@
1
- import { i as handleErrors } from "./client-D0Px7KzF.mjs";
2
- import { ZodError } from "zod";
3
- //#region src/server/errors.ts
4
- var HttpError = class extends Error {
5
- status;
6
- code;
7
- /** What fixes it, when there's one (`BoardError`'s `hint`). */
8
- hint;
9
- /** `handle` for a handle's own error on its way through: everything else is the board's. */
10
- origin;
11
- constructor(status, code, message, options = {}) {
12
- super(message);
13
- this.status = status;
14
- this.code = code;
15
- this.hint = options.hint;
16
- this.origin = options.origin ?? "board";
17
- }
18
- };
19
- /** A request a host's own verifier rejected with `response`, which the board answers with as it is. */
20
- var RejectedError = class extends HttpError {
21
- response;
22
- constructor(response) {
23
- super(response.status, "rejected", `rejected with ${response.status}`);
24
- this.response = response;
25
- }
26
- };
27
- const badRequest = (message, code = "invalid_request") => new HttpError(400, code, message);
28
- const unauthorized = (message) => new HttpError(401, "unauthorized", message);
29
- const forbidden = (message) => new HttpError(403, "forbidden", message);
30
- const notFound = (message, code = "not_found") => new HttpError(404, code, message);
31
- const duplicateId = () => new HttpError(409, "duplicate_id", "that id was already used for a different write");
32
- const json = (body, status = 200, headers) => new Response(JSON.stringify(body), {
33
- status,
34
- headers: {
35
- "content-type": "application/json",
36
- ...headers
37
- }
38
- });
39
- /**
40
- * The issue a request is told about. A union that matched no option says only "Invalid input", so
41
- * it's the first issue of the option that came closest, the one with the fewest, at its own path.
42
- */
43
- function reported(issue) {
44
- if (issue.code !== "invalid_union" || issue.errors.length === 0) return issue;
45
- const first = issue.errors.reduce((best, option) => option.length < best.length ? option : best)[0];
46
- return first ? reported({
47
- ...first,
48
- path: [...issue.path, ...first.path]
49
- }) : issue;
50
- }
51
- /** Every failure becomes `{ error: { code, message, hint? } }` with a matching status. */
52
- function toErrorResponse(error) {
53
- if (error instanceof RejectedError) return error.response;
54
- if (error instanceof HttpError) {
55
- const response = json({ error: {
56
- code: error.code,
57
- message: error.message,
58
- ...error.hint !== void 0 ? { hint: error.hint } : {}
59
- } }, error.status);
60
- if (error.origin === "handle") handleErrors.add(response);
61
- return response;
62
- }
63
- if (error instanceof ZodError) {
64
- const issue = error.issues[0] && reported(error.issues[0]);
65
- const path = issue?.path.length ? `${issue.path.join(".")}: ` : "";
66
- return json({ error: {
67
- code: "invalid_request",
68
- message: `${path}${issue?.message ?? "invalid request"}`
69
- } }, 400);
70
- }
71
- const code = error?.code;
72
- if (code === "INVALID_PAYLOAD") return json({ error: {
73
- code: "invalid_request",
74
- message: error.message
75
- } }, 400);
76
- if (code === "PARTIAL_DUPLICATE_BATCH" || code === "DUPLICATE_EVENT_ID") return toErrorResponse(duplicateId());
77
- if (code === "STORE_UNAVAILABLE" || code === "STORE_NOT_CONFIGURED") return json({ error: {
78
- code: "unavailable",
79
- message: error.message
80
- } }, 503);
81
- console.error("[board]", error);
82
- return json({ error: {
83
- code: "internal",
84
- message: "internal error"
85
- } }, 500);
86
- }
87
- //#endregion
88
- export { forbidden as a, toErrorResponse as c, duplicateId as i, unauthorized as l, RejectedError as n, json as o, badRequest as r, notFound as s, HttpError as t };