@vercel/board 0.20.0 → 0.21.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 (31) hide show
  1. package/README.md +0 -25
  2. package/dist/{a2-D__qQpe7.mjs → a2-BgNAyr_f.mjs} +1 -1
  3. package/dist/backend-a2.mjs +1 -1
  4. package/dist/{backend-http-CG5y03UP.mjs → backend-http-CLcXkcAU.mjs} +190 -140
  5. package/dist/backend-http-Dzb4lpMG.mjs +18 -0
  6. package/dist/backend-http.mjs +1 -1
  7. package/dist/backend-http.node.mjs +1 -1
  8. package/dist/backend-vercel-BjUpoLR9.mjs +60 -0
  9. package/dist/backend-vercel-CYOqtLPc.d.mts +656 -0
  10. package/dist/backend-vercel-DjL8tYIF.mjs +1314 -0
  11. package/dist/backend-vercel.d.mts +9 -0
  12. package/dist/backend-vercel.mjs +11 -0
  13. package/dist/backend-vercel.node.d.mts +12 -0
  14. package/dist/backend-vercel.node.mjs +2 -0
  15. package/dist/cli-embedded.d.mts +5 -5
  16. package/dist/cli-embedded.mjs +6 -5
  17. package/dist/cli.mjs +3 -3
  18. package/dist/{exports-DqWJ7nzC.mjs → exports-DIivm0je.mjs} +1 -1
  19. package/dist/{index.default-C3luVnd-.mjs → index.default-DNKzR4md.mjs} +1 -1
  20. package/dist/index.mjs +2 -2
  21. package/dist/index.node.mjs +1 -1
  22. package/dist/inspect/app.js +29 -29
  23. package/dist/{main-wB-VfTCL.mjs → main-BZ7nOIhV.mjs} +75 -508
  24. package/dist/{backend-http-c-VHpUOs.mjs → node-BeKPcokJ.mjs} +15 -17
  25. package/dist/{serve-AnApUWOR.mjs → serve-8zkBGMmb.mjs} +3 -3
  26. package/dist/{server-BqVickb5.mjs → server-Du0rSpcW.mjs} +2 -2
  27. package/dist/{watchers-hash-DYwvkhdX.mjs → watchers-hash-9sxfL1Gd.mjs} +1 -1
  28. package/docs/03-guides/01-coding-agents.md +0 -94
  29. package/docs/04-reference/01-cli.md +26 -23
  30. package/docs/index.md +1 -2
  31. package/package.json +6 -1
@@ -0,0 +1,1314 @@
1
+ import { _ as RemoteWatcherId } from "./schemas-Be08nyfW.mjs";
2
+ import { f as VERSION, k as RegisterBody, n as compileWatcher, o as METHODS, t as canonicalWatchers } from "./watchers-hash-9sxfL1Gd.mjs";
3
+ import { a as sha256, n as derivedId } from "./ids-C6AYo1bu.mjs";
4
+ import { a as forbidden, o as json, s as notFound, t as HttpError } from "./errors-CQ-fwxM3.mjs";
5
+ import { a as wakePath, i as registrant, n as callerProfile, t as PROJECTS_OWN } from "./backend-http-CLcXkcAU.mjs";
6
+ //#region src/server/backend-vercel.ts
7
+ const PROFILE = /^[A-Za-z][A-Za-z0-9-]{0,31}$/;
8
+ const SESSION = /^[A-Za-z0-9_.:-]{1,128}$/;
9
+ const CLIENT_ID = /^[A-Za-z0-9_.:-]{1,128}$/;
10
+ const MESSAGE_ID = /^msg_([0-9a-f-]{36})_([1-9]\d*)$/;
11
+ /** A cursor this backend made: a time, then the position it names, in base 36. */
12
+ const CURSOR = /^([0-9a-z]+)(?:\.([0-9a-z]+))?$/;
13
+ /** The API's own cursor for one channel, which its message list and feed share, and any process resumes. */
14
+ const API_CURSOR = /^c1\./;
15
+ /** The API's longest page of a channel's messages, and longest long-poll, in seconds. */
16
+ const PAGE_MAX = 100;
17
+ const WAIT_MAX_S = 25;
18
+ /** Pages a filtered read of one channel scans for matches before it answers with what it found. */
19
+ const SCAN_PAGES = 10;
20
+ /** What the API keeps none of: a check that gets one of these is told it isn't kept here. */
21
+ const UNKEPT = {
22
+ schedule: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
23
+ schedules: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
24
+ cancelSchedule: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
25
+ upload: ["no_files", "Vercel's boards keep no files yet"],
26
+ attachment: ["no_files", "Vercel's boards keep no files yet"]
27
+ };
28
+ const notSupported = (what) => new HttpError(501, "not_supported", `Vercel's boards don't support ${what} yet`);
29
+ /** What a caller board() verified may do. Null for this deployment's own code, which may do what its credentials may. */
30
+ const accessOf = (caller) => "identity" in caller ? caller.identity.principal.access : null;
31
+ /** A verified caller's grant, which the API can't check: it sees this deployment's credentials. */
32
+ function need(access, permission, channel) {
33
+ if (access && !access.can(permission, channel)) throw forbidden(`this token can't ${permission} in ${channel}`);
34
+ }
35
+ const apiKind = (kind) => kind === "app" ? "workflow" : kind;
36
+ /** A UUIDv7, which the API takes as an operation ID. */
37
+ function operationId() {
38
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
39
+ const now = BigInt(Date.now());
40
+ for (let i = 0; i < 6; i++) bytes[i] = Number(now >> BigInt(8 * (5 - i)) & 255n);
41
+ bytes[6] = bytes[6] & 15 | 112;
42
+ bytes[8] = bytes[8] & 63 | 128;
43
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
44
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
45
+ }
46
+ const iso = (ms) => new Date(ms).toISOString();
47
+ const idAt = (threadId, sequence) => `msg_${threadId.slice(4)}_${sequence}`;
48
+ /** Where a message ID points: its thread and its place in it. Null for one that isn't a message's. */
49
+ function locate(id) {
50
+ const found = MESSAGE_ID.exec(id);
51
+ return found ? {
52
+ threadId: `thr_${found[1]}`,
53
+ sequence: Number(found[2])
54
+ } : null;
55
+ }
56
+ /** A principal as board names it: `vercel:user:<id>`, `vercel:app:<id>`, `vercel:project:<id>:<env>`. */
57
+ function principalId(principal) {
58
+ if (principal.type === "user") return `vercel:user:${principal.userId}`;
59
+ if (principal.type === "app") return `vercel:app:${principal.clientId}`;
60
+ return `vercel:project:${principal.projectId}:${principal.environment}`;
61
+ }
62
+ const authorOf = (actor) => ({
63
+ actor: actor.id,
64
+ principal: principalId(actor.principal),
65
+ profile: actor.profile?.name ?? null,
66
+ session: actor.session ?? null
67
+ });
68
+ /** The API names nobody, so an actor's handle is its profile, or a short form of its principal. */
69
+ function actorView(actor) {
70
+ const { principal } = actor;
71
+ const base = principal.type === "user" ? `user-${principal.userId.slice(0, 6).toLowerCase()}` : principal.type === "app" ? `app-${principal.clientId.slice(0, 6).toLowerCase()}` : principal.environment === "production" ? principal.projectId : `${principal.projectId}@${principal.environment}`;
72
+ const handle = actor.profile?.name ?? base;
73
+ const kind = actor.profile ? "agent" : principal.type === "user" ? "person" : principal.type === "project" ? "deployment" : "workflow";
74
+ return {
75
+ id: actor.id,
76
+ kind,
77
+ handle,
78
+ name: handle,
79
+ avatar: actor.profile?.avatar ?? null
80
+ };
81
+ }
82
+ /**
83
+ * Vercel's board for your team, in Vercel's API: this deployment keeps nothing and reads and writes
84
+ * it as itself, its people as themselves. Messages, threads, reactions, labels, and a channel's
85
+ * changes work; what the API keeps none of yet (schedules, files, watchers, sync) answers 501.
86
+ */
87
+ function createVercel(options, environment) {
88
+ const origin = (options.api ?? "https://api.vercel.com").replace(/\/+$/, "");
89
+ if (!/^https:\/\/[^/]+$|^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) throw new Error(`vercel: ${origin} isn't Vercel's API: pass an https origin`);
90
+ if (!options.fetch && !environment) throw new Error("vercel: pass a fetch that sends credentials: outside Node there's no environment to find them in");
91
+ const doFetch = options.fetch ?? environment.fetch();
92
+ let binding = null;
93
+ /** This board's own code's names, which a caller here never takes as its profile. */
94
+ const own = /* @__PURE__ */ new Set();
95
+ let team = null;
96
+ const teamId = () => team ??= options.team ? Promise.resolve(options.team) : environment?.team() ?? Promise.resolve(null);
97
+ /** One request to the API, as `who`. Its errors are the API's own, with its status and code. */
98
+ async function api(who, method, path, init = {}) {
99
+ const url = new URL(`${origin}/v1/messageboard${path}`);
100
+ const id = await teamId();
101
+ if (id) url.searchParams.set(id.startsWith("team_") ? "teamId" : "slug", id);
102
+ for (const [name, value] of Object.entries(init.query ?? {})) if (value !== void 0) url.searchParams.set(name, value);
103
+ const headers = { "content-type": "application/json" };
104
+ if (who.profile) headers["x-board-profile"] = who.profile.name;
105
+ if (who.profile?.avatar?.startsWith("https://")) headers["x-board-avatar"] = who.profile.avatar;
106
+ if (who.session && SESSION.test(who.session)) headers["x-board-session"] = who.session;
107
+ let response;
108
+ try {
109
+ response = await doFetch(url, {
110
+ method,
111
+ headers,
112
+ redirect: "error",
113
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
114
+ ...init.signal ? { signal: init.signal } : {}
115
+ });
116
+ } catch (error) {
117
+ if (error instanceof HttpError || error instanceof Error && error.name === "AbortError") throw error;
118
+ throw new HttpError(502, "unreachable", `Vercel's API didn't answer: ${error instanceof Error ? error.message : String(error)}`);
119
+ }
120
+ const text = await response.text();
121
+ let body;
122
+ try {
123
+ body = text ? JSON.parse(text) : void 0;
124
+ } catch {}
125
+ if (response.ok) return body;
126
+ const error = body?.error;
127
+ throw new HttpError(response.status, error?.code ?? "remote_error", error?.message ?? `Vercel's API answered ${response.status}`);
128
+ }
129
+ /** Every page of a list, in order. */
130
+ async function all(who, path, key, query = {}, signal) {
131
+ const out = [];
132
+ let cursor;
133
+ do {
134
+ const page = await api(who, "GET", path, {
135
+ query: {
136
+ ...query,
137
+ cursor
138
+ },
139
+ ...signal ? { signal } : {}
140
+ });
141
+ out.push(...page[key]);
142
+ cursor = page.cursor ?? void 0;
143
+ } while (cursor);
144
+ return out;
145
+ }
146
+ let board = null;
147
+ /**
148
+ * The board's ID: the one given, or the team's, which the API names only to those it lets in. A
149
+ * team with none gets one the first time a person uses it, as the API asks of its clients; a
150
+ * project can't make it, and needs a person to grant it the board anyway.
151
+ */
152
+ const boardId = (who) => (board ??= options.board ? Promise.resolve(options.board) : api(who, "GET", "/boards").then(async ({ defaultBoardId }) => {
153
+ if (defaultBoardId) return defaultBoardId;
154
+ if ((await self(who)).actor.principal.type === "project") throw new HttpError(404, "no_board", "Vercel's API names no board this project may use: ask someone on the team to grant it the team's board, or pass `board`");
155
+ const { board: made } = await api(who, "POST", "/boards/default", { body: { operationId: operationId() } });
156
+ return made.id;
157
+ })).catch((error) => {
158
+ board = null;
159
+ throw error;
160
+ });
161
+ const onBoard = async (who, path = "") => `/boards/${encodeURIComponent(await boardId(who))}${path}`;
162
+ let channels = null;
163
+ const loadChannels = async (who, fresh = false) => {
164
+ if (fresh) channels = null;
165
+ channels ??= onBoard(who, "/channels").then((path) => all(who, path, "channels"));
166
+ return channels.catch((error) => {
167
+ channels = null;
168
+ throw error;
169
+ });
170
+ };
171
+ /** The channel by `name`, which the API finds without listing the board's. */
172
+ const named = /* @__PURE__ */ new Map();
173
+ async function channelNamed(who, name) {
174
+ const known = named.get(name);
175
+ if (known) return known;
176
+ const { channels: [found] } = await api(who, "GET", await onBoard(who, "/channels"), { query: { name } });
177
+ if (found) named.set(name, found);
178
+ return found ?? null;
179
+ }
180
+ async function channelById(who, id) {
181
+ const found = (await loadChannels(who)).find((channel) => channel.id === id) ?? (await loadChannels(who, true)).find((channel) => channel.id === id);
182
+ if (!found) throw new HttpError(502, "remote_error", `Vercel's API names a channel it doesn't list: ${id}`);
183
+ return found;
184
+ }
185
+ /** A channel to post in, made the first time something posts to it, as on any board. */
186
+ async function channelToPost(who, name) {
187
+ const found = await channelNamed(who, name);
188
+ if (found) return found;
189
+ try {
190
+ const { channel } = await api(who, "POST", await onBoard(who, "/channels"), { body: {
191
+ operationId: operationId(),
192
+ name
193
+ } });
194
+ channels = null;
195
+ named.set(name, channel);
196
+ return channel;
197
+ } catch (error) {
198
+ if (error instanceof HttpError && error.code === "channel_name_taken") {
199
+ const made = await channelNamed(who, name);
200
+ if (made) return made;
201
+ }
202
+ throw error;
203
+ }
204
+ }
205
+ /** The channels a filter names that `access` reads: one, a channel and those under it (`dev/`), or every one. */
206
+ async function channelsFor(who, filter, access) {
207
+ const readable = (channel) => access === null || access.readable(channel.name);
208
+ if (filter !== void 0 && !filter.endsWith("/")) {
209
+ const found = await channelNamed(who, filter);
210
+ return found && readable(found) ? [found] : [];
211
+ }
212
+ const listed = (await loadChannels(who, true)).filter(readable);
213
+ if (filter === void 0) return listed;
214
+ const path = filter.slice(0, -1);
215
+ return listed.filter((channel) => channel.name === path || channel.name.startsWith(filter));
216
+ }
217
+ async function messageById(who, id, signal) {
218
+ if (!locate(id)) throw notFound(`message not found: ${id}`, "message_not_found");
219
+ try {
220
+ return await api(who, "GET", await onBoard(who, `/messages/${encodeURIComponent(id)}`), signal ? { signal } : {});
221
+ } catch (error) {
222
+ if (error instanceof HttpError && error.code === "thread_not_found") throw notFound(`message not found: ${id}`, "message_not_found");
223
+ throw error;
224
+ }
225
+ }
226
+ /** A message `access` may read, and its channel: one it can't is as missing as one that isn't there. */
227
+ async function readableMessage(who, access, id, signal) {
228
+ const found = await messageById(who, id, signal);
229
+ const channel = await channelById(who, found.thread.channelId);
230
+ if (access && !access.readable(channel.name)) throw notFound(`message not found: ${id}`, "message_not_found");
231
+ return {
232
+ ...found,
233
+ channel
234
+ };
235
+ }
236
+ /** Each thread a read needs, read once: a label matches on its root's, which the API keeps on the thread. */
237
+ function threadReader(who, signal) {
238
+ const read = /* @__PURE__ */ new Map();
239
+ return (id) => {
240
+ let thread = read.get(id);
241
+ if (!thread) read.set(id, thread = onBoard(who, `/threads/${encodeURIComponent(id)}`).then((path) => api(who, "GET", path, signal ? { signal } : {})).then((found) => found.thread));
242
+ return thread;
243
+ };
244
+ }
245
+ const threadMessages = async (who, threadId, signal) => all(who, await onBoard(who, `/threads/${encodeURIComponent(threadId)}/messages`), "messages", {}, signal);
246
+ /** Who reacted with each emoji. The API counts them on the message and lists them apart. */
247
+ async function reactors(who, message) {
248
+ if (!message.reactions || Object.keys(message.reactions).length === 0) return {};
249
+ const named = message.reactors ?? {};
250
+ if (Object.entries(message.reactions).every(([emoji, count]) => (named[emoji]?.length ?? 0) >= count)) return Object.fromEntries(Object.entries(named).map(([emoji, actors]) => [emoji, [...actors]]));
251
+ const path = await onBoard(who, `/threads/${encodeURIComponent(message.threadId)}/messages/${message.sequence}/reactions`);
252
+ const out = {};
253
+ for (const reaction of await all(who, path, "reactions")) (out[reaction.emoji] ??= []).push(reaction.actor.id);
254
+ return out;
255
+ }
256
+ async function view(who, message, channel) {
257
+ const root = message.sequence === 1;
258
+ return {
259
+ id: message.id,
260
+ channel,
261
+ text: message.text,
262
+ re: root ? null : idAt(message.threadId, 1),
263
+ replyTo: message.replyTo !== void 0 && message.replyTo !== 1 ? idAt(message.threadId, message.replyTo) : null,
264
+ thread: message.threadId,
265
+ labels: { ...message.labels },
266
+ reactions: message.deletedAt === void 0 ? await reactors(who, message) : {},
267
+ mentions: [],
268
+ by: authorOf(message.createdBy),
269
+ noSync: false,
270
+ origin: null,
271
+ from: null,
272
+ createdAt: iso(message.createdAt),
273
+ editedAt: message.editedAt === void 0 ? null : iso(message.editedAt),
274
+ deleted: message.deletedAt !== void 0,
275
+ undelivered: {},
276
+ attachments: [],
277
+ links: [],
278
+ version: message.version,
279
+ author: actorView(message.createdBy)
280
+ };
281
+ }
282
+ const positions = /* @__PURE__ */ new Map();
283
+ function cursorOf(position) {
284
+ const key = `${position.t.toString(36)}.${Math.floor(Math.random() * 36 ** 8).toString(36)}`;
285
+ positions.set(key, position);
286
+ if (positions.size > 1e4) positions.delete(positions.keys().next().value);
287
+ return key;
288
+ }
289
+ function positionOf(cursor) {
290
+ const kept = positions.get(cursor);
291
+ if (kept) return kept;
292
+ const found = CURSOR.exec(cursor);
293
+ if (!found) throw new HttpError(400, "invalid_cursor", `after: ${cursor} isn't a cursor here or a message's ID`);
294
+ return {
295
+ t: parseInt(found[1], 36),
296
+ feeds: {}
297
+ };
298
+ }
299
+ /** A time a read goes on from: a cursor's, or a message's own. */
300
+ async function timeOf(who, after) {
301
+ return locate(after) ? (await messageById(who, after)).message.createdAt : positionOf(after).t;
302
+ }
303
+ /** Each channel's feed now, taken before a read, so what's posted while it reads comes after. */
304
+ async function heads(who, listed) {
305
+ const path = await onBoard(who, "/changes");
306
+ const found = await Promise.all(listed.map((channel) => api(who, "GET", path, { query: {
307
+ channelId: channel.id,
308
+ from: "latest"
309
+ } })));
310
+ return Object.fromEntries(listed.map((channel, i) => [channel.id, {
311
+ at: found[i].cursor,
312
+ skip: 0
313
+ }]));
314
+ }
315
+ /** Whether a message matches a read's filters. A label matches on the message or its thread's root. */
316
+ function matches(filters, message, thread, me) {
317
+ if (filters.re !== void 0 && filters.re !== message.id && filters.re !== idAt(thread.id, 1)) return false;
318
+ if (filters.by !== void 0) {
319
+ const { createdBy } = message;
320
+ const names = [
321
+ createdBy.id,
322
+ principalId(createdBy.principal),
323
+ createdBy.profile?.name
324
+ ];
325
+ if (!(filters.by === "me" ? createdBy.id === me : names.includes(filters.by))) return false;
326
+ }
327
+ if (filters.mentions !== void 0) return false;
328
+ if (filters.from !== void 0 && filters.from !== "board") return false;
329
+ if (filters.text !== void 0 && !message.text.toLowerCase().includes(filters.text.toLowerCase())) return false;
330
+ for (const [key, values] of filters.label ?? []) {
331
+ const on = (labels) => labels !== void 0 && Object.hasOwn(labels, key) && (values === null || values.includes(labels[key]));
332
+ if (!on(message.labels) && !on(thread.labels)) return false;
333
+ }
334
+ return true;
335
+ }
336
+ /** The label the API's index can narrow a channel's threads by: a root's `key` or `key=value`. */
337
+ function indexed(filters) {
338
+ const first = filters.label?.find(([, values]) => values === null || values.length === 1);
339
+ return first && (first[1] === null ? first[0] : `${first[0]}=${first[1][0]}`);
340
+ }
341
+ async function threadsOf(who, channel, filters, signal) {
342
+ const label = indexed(filters);
343
+ return all(who, await onBoard(who, "/threads"), "threads", {
344
+ channelId: channel.id,
345
+ label
346
+ }, signal);
347
+ }
348
+ /**
349
+ * A channel's threads, as far as a read needs: the API lists them most recently active first, so
350
+ * it stops at the first not active since `after`, or at the `limit` newest when it's given one
351
+ * (not when the read filters what's listed). One it lists by a label comes oldest first, so it
352
+ * reads every page.
353
+ */
354
+ async function latestThreads(who, channel, label, page, signal) {
355
+ const path = await onBoard(who, "/threads");
356
+ const out = [];
357
+ let cursor;
358
+ do {
359
+ const found = await api(who, "GET", path, {
360
+ query: {
361
+ channelId: channel.id,
362
+ label,
363
+ cursor
364
+ },
365
+ signal
366
+ });
367
+ out.push(...found.threads);
368
+ cursor = found.cursor ?? void 0;
369
+ const last = out.at(-1);
370
+ if (label === void 0 && (page.after !== void 0 ? last !== void 0 && last.lastActivityAt <= page.after : page.limit !== void 0 && out.length >= page.limit)) break;
371
+ } while (cursor);
372
+ return out;
373
+ }
374
+ /** A thread at a glance: its root, its reply count, and its latest reply, read by reference. */
375
+ async function summary(who, thread, channel, signal) {
376
+ const [root, last] = await Promise.all([messageById(who, idAt(thread.id, 1), signal), thread.lastMessage && thread.lastMessage.sequence > 1 ? messageById(who, thread.lastMessage.id, signal) : null]);
377
+ return {
378
+ message: await view(who, root.message, channel.name),
379
+ replies: thread.messageCount - 1,
380
+ last: last && await view(who, last.message, channel.name)
381
+ };
382
+ }
383
+ let me = null;
384
+ /** Who these credentials are, asked once. */
385
+ const self = (who) => (me ??= api(who, "GET", "/me")).catch((error) => {
386
+ me = null;
387
+ throw error;
388
+ });
389
+ /** The actor `who` writes as: the credentials' principal, and its profile, as the API names it. */
390
+ const actorOf = async (who) => `${principalId((await self(who)).actor.principal)}${who.profile ? `+${who.profile.name.toLowerCase()}` : ""}`;
391
+ /** Who a caller acts as there: its profile and session, beside this deployment's credentials. */
392
+ function whoOf(caller) {
393
+ if ("identity" in caller) {
394
+ const avatar = caller.identity.actor.avatar?.startsWith("https://") ? caller.identity.actor.avatar : void 0;
395
+ return {
396
+ profile: {
397
+ name: callerProfile(caller.identity.actor.handle, own),
398
+ ...avatar ? { avatar } : {}
399
+ },
400
+ session: null
401
+ };
402
+ }
403
+ const named = (profile) => {
404
+ if (profile === null) return null;
405
+ const name = typeof profile === "string" ? profile : profile.name;
406
+ if (!PROFILE.test(name)) throw new HttpError(400, "invalid_request", `as: ${name}: profiles start with a letter and use up to 32 letters, numbers, and hyphens`);
407
+ return typeof profile === "string" || !profile.avatar ? { name } : {
408
+ name,
409
+ avatar: profile.avatar
410
+ };
411
+ };
412
+ if (caller.origin.via?.kind === "client") {
413
+ const asked = "profile" in caller ? caller.profile : caller.self ?? void 0;
414
+ const found = environment?.profile() ?? {
415
+ profile: null,
416
+ session: null
417
+ };
418
+ if (asked === void 0) return {
419
+ profile: named(found.profile),
420
+ session: found.session
421
+ };
422
+ return {
423
+ profile: named(asked),
424
+ session: asked === null ? null : found.session
425
+ };
426
+ }
427
+ return {
428
+ profile: named(caller.self ?? "client"),
429
+ session: null
430
+ };
431
+ }
432
+ /** A post's client ID, as the API takes one: its own when it fits, a hash of it when it doesn't. */
433
+ const clientIdOf = async (id) => CLIENT_ID.test(id) ? id : `h.${(await sha256(id)).slice(0, 64)}`;
434
+ /** A gateway caller's post IDs are its own, derived under who it is here, since the API sees this deployment. */
435
+ const postId = async (caller, id) => clientIdOf("identity" in caller ? await derivedId("board-gateway", caller.identity.principal.id, id) : id);
436
+ /** Messages a read covers, newest last: every thread's in the channels it names, or one thread's. */
437
+ async function collect(who, access, filters, signal) {
438
+ let listed;
439
+ let threads;
440
+ if (filters.re !== void 0) {
441
+ const found = await readableMessage(who, access, filters.re, signal).catch((error) => {
442
+ if (error instanceof HttpError && error.status === 404) return null;
443
+ throw error;
444
+ });
445
+ if (!found) return {
446
+ messages: [],
447
+ feeds: {},
448
+ newest: 0
449
+ };
450
+ const { channel } = found;
451
+ if (filters.channel !== void 0 && !(await channelsFor(who, filters.channel, access)).some((c) => c.id === channel.id)) return {
452
+ messages: [],
453
+ feeds: {},
454
+ newest: 0
455
+ };
456
+ listed = [channel];
457
+ threads = [{
458
+ thread: found.thread,
459
+ channel
460
+ }];
461
+ } else {
462
+ listed = await channelsFor(who, filters.channel, access);
463
+ threads = (await Promise.all(listed.map(async (channel) => (await threadsOf(who, channel, filters, signal)).map((thread) => ({
464
+ thread,
465
+ channel
466
+ }))))).flat();
467
+ }
468
+ const feeds = await heads(who, listed);
469
+ const mine = await actorOf(who);
470
+ let newest = 0;
471
+ const messages = [];
472
+ for (const { thread, channel } of threads) for (const message of await threadMessages(who, thread.id, signal)) {
473
+ newest = Math.max(newest, message.createdAt);
474
+ if (matches(filters, message, thread, mine)) messages.push({
475
+ message,
476
+ thread,
477
+ channel
478
+ });
479
+ }
480
+ messages.sort((a, b) => a.message.createdAt - b.message.createdAt || a.thread.id.localeCompare(b.thread.id) || a.message.sequence - b.message.sequence);
481
+ return {
482
+ messages,
483
+ feeds,
484
+ newest
485
+ };
486
+ }
487
+ /** A change in the API's feed as a board event, with the message as it is now. Null for what isn't a message's. */
488
+ async function eventOf(who, change, channel, cursor, filters, me, given) {
489
+ if (!change.messageId || !change.type.startsWith("message.")) return null;
490
+ const found = given ?? await messageById(who, change.messageId).catch((error) => {
491
+ if (error instanceof HttpError && error.status === 404) return null;
492
+ throw error;
493
+ });
494
+ if (!found || !matches(filters, found.message, found.thread, me)) return null;
495
+ const message = await view(who, found.message, channel);
496
+ const by = authorOf(change.actor);
497
+ const base = {
498
+ id: message.id,
499
+ thread: message.thread
500
+ };
501
+ let type;
502
+ let payload;
503
+ switch (change.type) {
504
+ case "message.posted":
505
+ type = "message.posted";
506
+ payload = {
507
+ channel,
508
+ text: message.text,
509
+ re: message.re,
510
+ replyTo: message.replyTo,
511
+ thread: message.thread,
512
+ labels: message.labels,
513
+ noSync: false,
514
+ mentions: [],
515
+ by,
516
+ origin: null,
517
+ from: null,
518
+ attachments: [],
519
+ links: []
520
+ };
521
+ break;
522
+ case "message.edited":
523
+ type = "message.edited";
524
+ payload = {
525
+ ...base,
526
+ text: message.text,
527
+ mentions: [],
528
+ by,
529
+ from: null
530
+ };
531
+ break;
532
+ case "message.deleted":
533
+ type = "message.deleted";
534
+ payload = {
535
+ ...base,
536
+ by,
537
+ from: null
538
+ };
539
+ break;
540
+ case "message.reaction_added":
541
+ case "message.reaction_removed":
542
+ type = change.type === "message.reaction_added" ? "reaction.added" : "reaction.removed";
543
+ payload = {
544
+ ...base,
545
+ emoji: change.emoji,
546
+ by,
547
+ from: null
548
+ };
549
+ break;
550
+ case "message.labeled": {
551
+ const changed = Object.entries(change.labels ?? {});
552
+ type = "labels.changed";
553
+ payload = {
554
+ ...base,
555
+ set: Object.fromEntries(changed.flatMap(([key, { to }]) => to === void 0 ? [] : [[key, to]])),
556
+ unset: changed.flatMap(([key, { to }]) => to === void 0 ? [key] : []),
557
+ by
558
+ };
559
+ break;
560
+ }
561
+ default: return null;
562
+ }
563
+ return {
564
+ cursor,
565
+ type,
566
+ createdAt: iso(change.createdAt),
567
+ payload,
568
+ message,
569
+ actor: actorView(change.actor)
570
+ };
571
+ }
572
+ /**
573
+ * One read of every channel's feed from `position`: its events, merged by time, each with the
574
+ * position right after it, and the position after them all.
575
+ */
576
+ async function read(who, listed, position, filters, signal) {
577
+ const path = await onBoard(who, "/changes");
578
+ const pages = await Promise.all(listed.map(async (channel) => {
579
+ const from = position.feeds[channel.id];
580
+ const page = await api(who, "GET", path, {
581
+ query: {
582
+ channelId: channel.id,
583
+ ...from?.at ? { cursor: from.at } : {}
584
+ },
585
+ signal
586
+ });
587
+ const skip = from?.skip ?? 0;
588
+ const events = page.events.map((change, i) => ({
589
+ change,
590
+ i
591
+ })).slice(skip).filter(({ change }) => from || change.createdAt > position.t);
592
+ return {
593
+ channel,
594
+ at: from?.at ?? null,
595
+ next: page.cursor,
596
+ events
597
+ };
598
+ }));
599
+ const mine = await actorOf(who);
600
+ const running = { ...position.feeds };
601
+ const merged = pages.flatMap((page) => page.events.map((event) => ({
602
+ ...event,
603
+ page
604
+ }))).sort((a, b) => a.change.createdAt - b.change.createdAt);
605
+ const events = [];
606
+ for (const { change, i, page } of merged) {
607
+ running[page.channel.id] = {
608
+ at: page.at,
609
+ skip: i + 1
610
+ };
611
+ const event = await eventOf(who, change, page.channel.name, cursorOf({
612
+ t: change.createdAt,
613
+ feeds: { ...running }
614
+ }), filters, mine);
615
+ if (event) events.push(event);
616
+ }
617
+ const after = { ...position.feeds };
618
+ for (const page of pages) after[page.channel.id] = {
619
+ at: page.next,
620
+ skip: 0
621
+ };
622
+ return {
623
+ events,
624
+ position: {
625
+ t: Math.max(position.t, ...merged.map(({ change }) => change.createdAt)),
626
+ feeds: after
627
+ }
628
+ };
629
+ }
630
+ /** Whether a read filters on what messages say, who wrote them, or their labels: what the API can't narrow by. */
631
+ const filtered = (filters) => filters.by !== void 0 || filters.mentions !== void 0 || filters.text !== void 0 || filters.from !== void 0 || (filters.label?.length ?? 0) > 0;
632
+ /**
633
+ * One channel's messages, from the API's message list: the latest `limit`, or those after one of
634
+ * its cursors. A filtered read checks each page here, and asks only for as many as it still needs,
635
+ * so the cursor it answers with never passes a match it left out.
636
+ */
637
+ async function channelMessages(who, channel, query, signal) {
638
+ const path = await onBoard(who, "/messages");
639
+ const limit = query.limit ?? 50;
640
+ const thread = threadReader(who, signal);
641
+ const narrowed = filtered(query);
642
+ const me = narrowed ? await actorOf(who) : null;
643
+ const keep = async (message) => !narrowed || matches(query, message, query.label?.length ? await thread(message.threadId) : { id: message.threadId }, me);
644
+ const kept = [];
645
+ if (query.after === void 0) {
646
+ const page = await api(who, "GET", path, {
647
+ query: {
648
+ channelId: channel.id,
649
+ limit: String(narrowed ? PAGE_MAX : Math.min(limit, PAGE_MAX))
650
+ },
651
+ signal
652
+ });
653
+ for (const message of page.messages) if (await keep(message)) kept.push(message);
654
+ return {
655
+ messages: kept.slice(-limit),
656
+ cursor: page.cursor
657
+ };
658
+ }
659
+ let cursor = query.after;
660
+ for (let pages = 0; pages < SCAN_PAGES && kept.length < limit; pages++) {
661
+ const page = await api(who, "GET", path, {
662
+ query: {
663
+ channelId: channel.id,
664
+ after: cursor,
665
+ limit: String(Math.min(limit - kept.length, PAGE_MAX))
666
+ },
667
+ signal
668
+ });
669
+ cursor = page.cursor;
670
+ for (const message of page.messages) if (await keep(message)) kept.push(message);
671
+ if (page.messages.length === 0) break;
672
+ }
673
+ return {
674
+ messages: kept,
675
+ cursor
676
+ };
677
+ }
678
+ /** One channel's events, from the API's feed after one of its cursors, or from now: a long-poll holds there, not here. */
679
+ async function channelEvents(who, channel, query, signal) {
680
+ const path = await onBoard(who, "/changes");
681
+ const thread = threadReader(who, signal);
682
+ const me = await actorOf(who);
683
+ const until = Date.now() + (query.wait ?? 0) * 1e3;
684
+ let cursor = query.after;
685
+ for (;;) {
686
+ const wait = Math.min(WAIT_MAX_S, Math.max(0, Math.ceil((until - Date.now()) / 1e3)));
687
+ const page = await api(who, "GET", path, {
688
+ query: {
689
+ channelId: channel.id,
690
+ include: "message",
691
+ ...cursor === void 0 ? { from: "latest" } : { cursor },
692
+ ...wait > 0 ? { wait: String(wait) } : {}
693
+ },
694
+ signal
695
+ });
696
+ cursor = page.cursor;
697
+ const events = [];
698
+ for (const change of page.events) {
699
+ const message = change.currentMessage;
700
+ const found = message && change.threadId ? {
701
+ message,
702
+ thread: query.label?.length ? await thread(change.threadId) : { id: change.threadId }
703
+ } : null;
704
+ if (!found) continue;
705
+ const event = await eventOf(who, change, channel.name, change.cursor ?? page.cursor, query, me, found);
706
+ if (event) events.push(event);
707
+ }
708
+ if (events.length > 0 || Date.now() >= until || signal.aborted) return {
709
+ events,
710
+ cursor
711
+ };
712
+ }
713
+ }
714
+ /** The one channel a read names, when the API can read it directly: not a subtree, and from one of its cursors or now. */
715
+ async function oneChannel(who, access, query) {
716
+ if (query.channel === void 0 || query.channel.endsWith("/") || query.re !== void 0) return null;
717
+ if (query.after !== void 0 && !API_CURSOR.test(query.after)) return null;
718
+ const [channel] = await channelsFor(who, query.channel, access);
719
+ return channel ?? null;
720
+ }
721
+ /** Who wrote or reacted in the latest messages of each channel `access` reads. */
722
+ async function actorsSeen(who, access, signal) {
723
+ const path = await onBoard(who, "/messages");
724
+ const seen = /* @__PURE__ */ new Set();
725
+ for (const channel of await channelsFor(who, void 0, access)) {
726
+ const { messages } = await api(who, "GET", path, {
727
+ query: {
728
+ channelId: channel.id,
729
+ limit: String(PAGE_MAX)
730
+ },
731
+ signal
732
+ });
733
+ for (const message of messages) {
734
+ seen.add(message.createdBy.id);
735
+ for (const actors of Object.values(message.reactors ?? {})) for (const actor of actors) seen.add(actor);
736
+ }
737
+ }
738
+ return seen;
739
+ }
740
+ /** What a gateway caller does here, as this deployment: never what's the project's own. */
741
+ function checkCaller(method, caller) {
742
+ if ("identity" in caller && PROJECTS_OWN.has(method)) throw forbidden(`this board's callers act as its project on Vercel's board, which can't tell them apart: ${method} is the project's own to do there`);
743
+ }
744
+ const meOf = (identity) => {
745
+ const { principal, author, actor } = identity;
746
+ return {
747
+ actor: actor.id,
748
+ principal: principal.id,
749
+ kind: actor.kind,
750
+ handle: actor.handle,
751
+ name: actor.name,
752
+ avatar: actor.avatar,
753
+ profile: author.profile,
754
+ session: author.session,
755
+ expiresAt: principal.expiresAt?.toISOString() ?? null,
756
+ can: principal.access.summary()
757
+ };
758
+ };
759
+ /** A watcher's tail registers with the API as its own: an API watcher, so a match is the API's to make. */
760
+ const tagOf = async (watch) => (await sha256(canonicalWatchers(watch))).slice(0, 32);
761
+ /** Who `--by` names, as the API's authors: an actor, a principal, or everyone here with that handle or profile, as of now. */
762
+ async function authorsOf(who, by) {
763
+ if (by === "me") return [await actorOf(who)];
764
+ if (by.startsWith("vercel:")) return [by];
765
+ const named = (await all(who, await onBoard(who, "/actors"), "actors")).filter((actor) => actor.handle === by || actor.id.endsWith(`+${by.toLowerCase()}`)).map((actor) => actor.id);
766
+ if (named.length === 0) throw new HttpError(400, "bad_watcher", `--by ${by}: nobody here goes by that yet: name them by their actor ID`);
767
+ return named;
768
+ }
769
+ /**
770
+ * A tail as the filter the API matches. The API reads no tails: channels, people, and threads are
771
+ * looked up here, now, and what it can't match (mentions, other sources) is refused rather than
772
+ * left to match nothing.
773
+ */
774
+ async function filterOf(who, id, spec) {
775
+ const bad = (message) => new HttpError(400, "bad_watcher", `watch ${id}: ${message}`);
776
+ if (!RemoteWatcherId.safeParse(id).success) throw bad("ids start with a letter and use up to 32 letters, numbers, and hyphens");
777
+ let filters;
778
+ try {
779
+ ({filters} = compileWatcher(id, spec));
780
+ } catch (error) {
781
+ throw new HttpError(400, "bad_watcher", error.message);
782
+ }
783
+ if (filters.mentions !== void 0) throw bad("Vercel's boards can't match mentions yet");
784
+ if (filters.from !== void 0 && filters.from !== "board") throw bad(`nothing comes from ${filters.from} here: every message is written on the board`);
785
+ const filter = {};
786
+ if (filters.channel !== void 0) {
787
+ if (filters.channel.endsWith("/")) filter.channelTree = filters.channel.slice(0, -1);
788
+ else {
789
+ const found = await channelNamed(who, filters.channel);
790
+ if (found) filter.channelIds = [found.id];
791
+ else filter.channelTree = filters.channel;
792
+ }
793
+ }
794
+ if (filters.re !== void 0) {
795
+ const at = locate(filters.re);
796
+ if (!at) throw bad(`--re ${filters.re} names no message here`);
797
+ filter.threadId = at.threadId;
798
+ }
799
+ if (filters.by !== void 0) filter.authors = await authorsOf(who, filters.by);
800
+ if (filters.text !== void 0) filter.text = filters.text;
801
+ if (filters.label?.length) filter.labels = filters.label.map(([key, values]) => values === null ? { key } : {
802
+ key,
803
+ values: [...values]
804
+ });
805
+ return {
806
+ filter,
807
+ when: spec.when ?? "matches"
808
+ };
809
+ }
810
+ /** A filter shown as a tail again, for listing: what it matches, if not always as it was written. */
811
+ async function tailOf(who, { filter }) {
812
+ const parts = [];
813
+ if (filter.channelIds?.[0]) parts.push((await channelById(who, filter.channelIds[0])).name);
814
+ else if (filter.channelTree !== void 0) parts.push(`${filter.channelTree}/`);
815
+ if (filter.threadId !== void 0) parts.push(`--re ${idAt(filter.threadId, 1)}`);
816
+ if (filter.authors?.[0]) parts.push(`--by ${filter.authors[0]}`);
817
+ if (filter.text !== void 0) parts.push(`--text ${JSON.stringify(filter.text)}`);
818
+ for (const { key, values } of filter.labels ?? []) parts.push(`--label ${values ? `${key}=${values.join("|")}` : key}`);
819
+ return parts.join(" ");
820
+ }
821
+ /** Where a registration is woken, as board shows it: its URL, or a path on its project (and a preview's branch). */
822
+ const whereOf = (found) => found.path ? {
823
+ url: null,
824
+ path: found.path,
825
+ ...found.branch ? { branch: found.branch } : {}
826
+ } : { url: found.url };
827
+ async function registerAs(who, where, watch) {
828
+ const specs = Object.fromEntries(Object.entries(watch).map(([id, spec]) => [id, {
829
+ tail: spec.tail,
830
+ when: spec.when ?? "matches"
831
+ }]));
832
+ const filters = Object.fromEntries(await Promise.all(Object.entries(specs).map(async ([id, spec]) => [id, await filterOf(who, id, spec)])));
833
+ const made = await api(who, "PUT", await onBoard(who, "/watchers"), { body: {
834
+ ...where,
835
+ watch: filters,
836
+ tag: await tagOf(specs)
837
+ } });
838
+ return {
839
+ owner: made.owner,
840
+ ...whereOf(made),
841
+ watch: specs,
842
+ sync: [],
843
+ hash: made.hash
844
+ };
845
+ }
846
+ /** A match as vboard hands it out: its event as `vb tail --json` prints it, with its message as it is now. */
847
+ async function matchOf(who, match, me) {
848
+ const found = await messageById(who, match.event.messageId).catch((error) => {
849
+ if (error instanceof HttpError && error.status === 404) return null;
850
+ throw error;
851
+ });
852
+ const channel = found && await channelById(who, found.thread.channelId);
853
+ const event = found && channel && await eventOf(who, match.event, channel.name, match.id, {}, me, found);
854
+ if (!event?.message) return null;
855
+ return {
856
+ id: match.id,
857
+ watcher: match.watcher,
858
+ thread: event.message.thread,
859
+ lease: match.lease,
860
+ attempt: match.attempt,
861
+ event
862
+ };
863
+ }
864
+ async function pullAs(who, query, signal) {
865
+ const { matches } = await api(who, "GET", await onBoard(who, "/watchers/matches"), {
866
+ query: {
867
+ limit: query.limit?.toString(),
868
+ wait: query.wait?.toString()
869
+ },
870
+ ...signal ? { signal } : {}
871
+ });
872
+ const me = await actorOf(who);
873
+ const shown = await Promise.all(matches.map((match) => matchOf(who, match, me)));
874
+ await Promise.all(matches.map((match, i) => shown[i] ? null : ackAs(who, match.id, {
875
+ lease: match.lease,
876
+ skip: true
877
+ }).catch(() => {})));
878
+ return shown.filter((match) => match !== null);
879
+ }
880
+ async function ackAs(who, id, body) {
881
+ await api(who, "POST", await onBoard(who, `/watchers/matches/${encodeURIComponent(id)}/ack`), { body: {
882
+ lease: body.lease,
883
+ ...body.error !== void 0 ? { error: body.error } : {},
884
+ ...body.skip ? { skip: true } : {}
885
+ } });
886
+ }
887
+ /**
888
+ * The API keeps one registration per principal, and this board reaches it as its own deployment:
889
+ * one a caller registered through it would replace the deployment's, and every other caller's.
890
+ */
891
+ function ownWatchers(caller, what) {
892
+ if ("identity" in caller) throw forbidden(`this board registers watchers as its own deployment, so it can't ${what} for you: run them in its config, or register with Vercel's API as yourself`);
893
+ }
894
+ /** This deployment's own: what its config's watchers run as. */
895
+ const deployment = {
896
+ profile: null,
897
+ session: null
898
+ };
899
+ /** This board's config's watchers, which run here when the API wakes it. */
900
+ let tails = {};
901
+ const bound = () => {
902
+ if (!binding) throw new Error("vercel: used before board() bound it");
903
+ return binding;
904
+ };
905
+ const runner = registrant({
906
+ remote: "Vercel's API",
907
+ registry: {
908
+ register: (where) => registerAs(deployment, where, tails),
909
+ matches: ({ signal, ...query }) => pullAs(deployment, query, signal),
910
+ ack: (id, body) => ackAs(deployment, id, body)
911
+ },
912
+ wakeAt: async (here) => wakePath(here) ?? { url: here },
913
+ binding: bound,
914
+ hash: () => tagOf(tails),
915
+ hasWork: () => Object.keys(tails).length > 0,
916
+ rethrow: (error) => {
917
+ throw error;
918
+ },
919
+ registerFor: async (caller, request) => json(await methods.register(caller, RegisterBody.parse(await request.json()), request.signal))
920
+ });
921
+ const methods = {
922
+ ...Object.fromEntries(METHODS.map((method) => [method, async () => {
923
+ const unkept = UNKEPT[method];
924
+ if (unkept) throw new HttpError(501, unkept[0], unkept[1]);
925
+ throw notSupported(method === "register" || method === "watchers" || method === "matches" || method === "ack" ? "watchers" : method);
926
+ }])),
927
+ async discovery() {
928
+ const who = {
929
+ profile: null,
930
+ session: null
931
+ };
932
+ const team = await teamId();
933
+ return {
934
+ version: VERSION,
935
+ url: `${origin}/v1/messageboard${await onBoard(who)}`,
936
+ audience: origin,
937
+ team_id: team?.startsWith("team_") ? team : (await self(who)).teamId,
938
+ project_id: null,
939
+ sync: [],
940
+ wake_by_path: true
941
+ };
942
+ },
943
+ async me(caller) {
944
+ if ("identity" in caller) return meOf(caller.identity);
945
+ const found = await api(whoOf(caller), "GET", "/me");
946
+ const { view: named } = found;
947
+ const shown = named ? {
948
+ kind: apiKind(named.kind),
949
+ handle: named.handle,
950
+ name: named.name ?? named.handle,
951
+ avatar: named.avatar ?? null
952
+ } : actorView(found.actor);
953
+ return {
954
+ actor: found.actor.id,
955
+ principal: principalId(found.actor.principal),
956
+ kind: shown.kind,
957
+ handle: shown.handle,
958
+ name: shown.name,
959
+ avatar: shown.avatar,
960
+ profile: found.actor.profile?.name ?? null,
961
+ session: found.actor.session ?? null,
962
+ expiresAt: null,
963
+ can: { "*": [
964
+ "read",
965
+ "post",
966
+ "react",
967
+ "label"
968
+ ] }
969
+ };
970
+ },
971
+ async channels(caller) {
972
+ return {
973
+ channels: (await channelsFor(whoOf(caller), void 0, accessOf(caller))).map((channel) => ({
974
+ path: channel.name,
975
+ lastIndex: 0,
976
+ messages: 0,
977
+ threads: 0
978
+ })),
979
+ cursor: cursorOf({
980
+ t: Date.now(),
981
+ feeds: {}
982
+ })
983
+ };
984
+ },
985
+ async actors(caller, _input, signal) {
986
+ const who = whoOf(caller);
987
+ const listed = (await all(who, await onBoard(who, "/actors"), "actors", {}, signal)).map((actor) => ({
988
+ id: actor.id,
989
+ kind: apiKind(actor.kind),
990
+ handle: actor.handle,
991
+ name: actor.name ?? actor.handle,
992
+ avatar: actor.avatar ?? null
993
+ }));
994
+ const cursor = cursorOf({
995
+ t: Date.now(),
996
+ feeds: {}
997
+ });
998
+ if (!("identity" in caller) || caller.identity.principal.access.readsAll) return {
999
+ actors: listed,
1000
+ cursor
1001
+ };
1002
+ const { actor: self, principal } = caller.identity;
1003
+ const seen = await actorsSeen(who, principal.access, signal);
1004
+ return {
1005
+ actors: [...listed.filter((actor) => seen.has(actor.id) && actor.id !== self.id), {
1006
+ id: self.id,
1007
+ kind: self.kind,
1008
+ handle: self.handle,
1009
+ name: self.name,
1010
+ avatar: self.avatar
1011
+ }],
1012
+ cursor
1013
+ };
1014
+ },
1015
+ async messages(caller, query, signal) {
1016
+ const who = whoOf(caller);
1017
+ const access = accessOf(caller);
1018
+ const channel = query.before === void 0 ? await oneChannel(who, access, query) : null;
1019
+ if (channel) {
1020
+ const page = await channelMessages(who, channel, query, signal);
1021
+ return {
1022
+ messages: await Promise.all(page.messages.map((message) => view(who, message, channel.name))),
1023
+ cursor: page.cursor
1024
+ };
1025
+ }
1026
+ if (query.after !== void 0 && API_CURSOR.test(query.after)) throw new HttpError(400, "invalid_request", `after: ${query.after} is one channel's cursor: read that channel`);
1027
+ const { messages, feeds, newest } = await collect(who, access, query, signal);
1028
+ const after = query.after !== void 0 ? await timeOf(who, query.after) : void 0;
1029
+ const before = query.before !== void 0 ? await timeOf(who, query.before) : void 0;
1030
+ const limit = query.limit ?? 50;
1031
+ const bounded = messages.filter(({ message }) => (after === void 0 || message.createdAt > after) && (before === void 0 || message.createdAt < before));
1032
+ const page = after !== void 0 ? bounded.slice(0, limit) : bounded.slice(-limit);
1033
+ return {
1034
+ messages: await Promise.all(page.map(({ message, channel }) => view(who, message, channel.name))),
1035
+ cursor: cursorOf({
1036
+ t: newest,
1037
+ feeds
1038
+ })
1039
+ };
1040
+ },
1041
+ async threads(caller, query, signal) {
1042
+ const who = whoOf(caller);
1043
+ const after = query.after !== void 0 ? await timeOf(who, query.after) : void 0;
1044
+ const limit = query.limit ?? 50;
1045
+ const pick = (found) => {
1046
+ const ordered = found.filter(({ at }) => after === void 0 || at > after).sort((a, b) => a.at - b.at);
1047
+ return after !== void 0 ? ordered.slice(0, limit) : ordered.slice(-limit);
1048
+ };
1049
+ if (query.re !== void 0 || query.by !== void 0 || query.mentions !== void 0 || query.text !== void 0 || query.from !== void 0 && query.from !== "board") {
1050
+ const { messages, feeds, newest } = await collect(who, accessOf(caller), query, signal);
1051
+ const latest = /* @__PURE__ */ new Map();
1052
+ for (const { message, thread, channel } of messages) latest.set(thread.id, {
1053
+ thread,
1054
+ channel,
1055
+ at: Math.max(latest.get(thread.id)?.at ?? 0, message.createdAt)
1056
+ });
1057
+ return {
1058
+ threads: await Promise.all(pick([...latest.values()]).map(({ thread, channel }) => summary(who, thread, channel, signal))),
1059
+ cursor: cursorOf({
1060
+ t: newest,
1061
+ feeds
1062
+ })
1063
+ };
1064
+ }
1065
+ const listed = await channelsFor(who, query.channel, accessOf(caller));
1066
+ const feeds = await heads(who, listed);
1067
+ const label = indexed(query);
1068
+ const found = (await Promise.all(listed.map(async (channel) => (await latestThreads(who, channel, label, {
1069
+ ...after !== void 0 ? { after } : {},
1070
+ ...query.label?.length ? {} : { limit }
1071
+ }, signal)).map((thread) => ({
1072
+ thread,
1073
+ channel,
1074
+ at: thread.lastActivityAt
1075
+ }))))).flat();
1076
+ const rooted = found.filter(({ thread }) => (query.label ?? []).every(([key, values]) => thread.labels !== void 0 && Object.hasOwn(thread.labels, key) && (values === null || values.includes(thread.labels[key]))));
1077
+ const newest = found.reduce((max, { at }) => Math.max(max, at), 0);
1078
+ return {
1079
+ threads: await Promise.all(pick(rooted).map(({ thread, channel }) => summary(who, thread, channel, signal))),
1080
+ cursor: cursorOf({
1081
+ t: newest,
1082
+ feeds
1083
+ })
1084
+ };
1085
+ },
1086
+ async message(caller, { id }, signal) {
1087
+ const who = whoOf(caller);
1088
+ const { thread, channel } = await readableMessage(who, accessOf(caller), id, signal);
1089
+ const [root, ...replies] = await threadMessages(who, thread.id, signal);
1090
+ if (!root) throw notFound(`message not found: ${id}`, "message_not_found");
1091
+ return {
1092
+ message: await view(who, root, channel.name),
1093
+ replies: await Promise.all(replies.map((reply) => view(who, reply, channel.name))),
1094
+ cursor: cursorOf({
1095
+ t: Math.max(root.createdAt, ...replies.map((reply) => reply.createdAt)),
1096
+ feeds: {}
1097
+ })
1098
+ };
1099
+ },
1100
+ async post(caller, input, signal) {
1101
+ checkCaller("post", caller);
1102
+ if (input.attachments?.length) throw new HttpError(501, "no_files", "Vercel's boards keep no files yet");
1103
+ if (input.sync?.length) throw notSupported("sync");
1104
+ const who = whoOf(caller);
1105
+ const access = accessOf(caller);
1106
+ const labeled = Object.keys(input.labels ?? {}).length > 0;
1107
+ const clientId = input.id !== void 0 ? await postId(caller, input.id) : void 0;
1108
+ let made;
1109
+ if (input.channel !== void 0) {
1110
+ need(access, "post", input.channel);
1111
+ if (labeled) need(access, "label", input.channel);
1112
+ const channel = await channelToPost(who, input.channel);
1113
+ made = await api(who, "POST", await onBoard(who, "/threads"), {
1114
+ body: {
1115
+ operationId: operationId(),
1116
+ channelId: channel.id,
1117
+ text: input.text,
1118
+ ...input.labels && Object.keys(input.labels).length ? { labels: input.labels } : {},
1119
+ ...clientId ? { clientId } : {}
1120
+ },
1121
+ signal
1122
+ });
1123
+ } else {
1124
+ const { thread, message: to, channel } = await readableMessage(who, access, input.re, signal);
1125
+ need(access, "post", channel.name);
1126
+ if (labeled) need(access, "label", channel.name);
1127
+ made = await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(thread.id)}/messages`), {
1128
+ body: {
1129
+ operationId: operationId(),
1130
+ text: input.text,
1131
+ ...to.sequence > 1 ? { replyTo: to.sequence } : {},
1132
+ ...clientId ? { clientId } : {}
1133
+ },
1134
+ signal
1135
+ });
1136
+ if (input.labels && Object.keys(input.labels).length) {
1137
+ const labeled = await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(thread.id)}/messages/${made.message.sequence}/labels`), {
1138
+ body: {
1139
+ operationId: operationId(),
1140
+ set: input.labels
1141
+ },
1142
+ signal
1143
+ });
1144
+ made = {
1145
+ ...made,
1146
+ message: {
1147
+ ...made.message,
1148
+ ...labeled.meta.labels ? { labels: labeled.meta.labels } : {},
1149
+ version: labeled.meta.version
1150
+ }
1151
+ };
1152
+ }
1153
+ }
1154
+ const channel = await channelById(who, made.thread.channelId);
1155
+ return { message: await view(who, made.message, channel.name) };
1156
+ },
1157
+ async posted(caller, { id }, signal) {
1158
+ checkCaller("posted", caller);
1159
+ const who = whoOf(caller);
1160
+ const found = await api(who, "GET", await onBoard(who, `/posts/${encodeURIComponent(await postId(caller, id))}`), { signal });
1161
+ const channel = await channelById(who, found.thread.channelId);
1162
+ const access = accessOf(caller);
1163
+ if (access && !access.readable(channel.name)) throw notFound(`no post of yours has id ${id}`, "post_not_found");
1164
+ return { message: await view(who, found.message, channel.name) };
1165
+ },
1166
+ async edit(caller, { id, text }, signal) {
1167
+ checkCaller("edit", caller);
1168
+ const who = whoOf(caller);
1169
+ const at = locate(id);
1170
+ if (!at) throw notFound(`message not found: ${id}`, "message_not_found");
1171
+ await api(who, "PATCH", await onBoard(who, `/threads/${encodeURIComponent(at.threadId)}/messages/${at.sequence}`), {
1172
+ body: {
1173
+ operationId: operationId(),
1174
+ text
1175
+ },
1176
+ signal
1177
+ });
1178
+ return { ok: true };
1179
+ },
1180
+ async delete(caller, { id }, signal) {
1181
+ checkCaller("delete", caller);
1182
+ const who = whoOf(caller);
1183
+ const at = locate(id);
1184
+ if (!at) throw notFound(`message not found: ${id}`, "message_not_found");
1185
+ await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(at.threadId)}/messages/${at.sequence}/delete`), {
1186
+ body: { operationId: operationId() },
1187
+ signal
1188
+ });
1189
+ return { ok: true };
1190
+ },
1191
+ async react(caller, { id, emoji, remove }, signal) {
1192
+ checkCaller("react", caller);
1193
+ const who = whoOf(caller);
1194
+ const at = locate(id);
1195
+ if (!at) throw notFound(`message not found: ${id}`, "message_not_found");
1196
+ const access = accessOf(caller);
1197
+ if (access) need(access, "react", (await readableMessage(who, access, id, signal)).channel.name);
1198
+ await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(at.threadId)}/messages/${at.sequence}/reactions`), {
1199
+ body: {
1200
+ operationId: operationId(),
1201
+ emoji,
1202
+ ...remove ? { remove: true } : {}
1203
+ },
1204
+ signal
1205
+ });
1206
+ return { ok: true };
1207
+ },
1208
+ async label(caller, { id, set, unset }, signal) {
1209
+ checkCaller("label", caller);
1210
+ const who = whoOf(caller);
1211
+ const at = locate(id);
1212
+ if (!at) throw notFound(`message not found: ${id}`, "message_not_found");
1213
+ const access = accessOf(caller);
1214
+ if (access) need(access, "label", (await readableMessage(who, access, id, signal)).channel.name);
1215
+ await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(at.threadId)}/messages/${at.sequence}/labels`), {
1216
+ body: {
1217
+ operationId: operationId(),
1218
+ ...set ? { set } : {},
1219
+ ...unset ? { unset } : {}
1220
+ },
1221
+ signal
1222
+ });
1223
+ return { ok: true };
1224
+ },
1225
+ async events(caller, query, signal) {
1226
+ checkCaller("events", caller);
1227
+ const who = whoOf(caller);
1228
+ const access = accessOf(caller);
1229
+ const channel = await oneChannel(who, access, query);
1230
+ if (channel) return channelEvents(who, channel, query, signal);
1231
+ if (query.after !== void 0 && API_CURSOR.test(query.after)) throw new HttpError(400, "invalid_request", `after: ${query.after} is one channel's cursor: read that channel`);
1232
+ const listed = await channelsFor(who, query.channel, access);
1233
+ let position = query.after === void 0 ? {
1234
+ t: Date.now(),
1235
+ feeds: await heads(who, listed)
1236
+ } : locate(query.after) ? {
1237
+ t: await timeOf(who, query.after),
1238
+ feeds: {}
1239
+ } : positionOf(query.after);
1240
+ const until = Date.now() + (query.wait ?? 0) * 1e3;
1241
+ for (;;) {
1242
+ const page = await read(who, listed, position, query, signal);
1243
+ position = page.position;
1244
+ if (page.events.length > 0 || Date.now() >= until || signal.aborted) return {
1245
+ events: page.events,
1246
+ cursor: cursorOf(position)
1247
+ };
1248
+ await new Promise((resolve) => setTimeout(resolve, Math.min(1e3, until - Date.now())));
1249
+ }
1250
+ },
1251
+ async typing() {
1252
+ return { targets: [] };
1253
+ },
1254
+ async readSource(_caller, { key }) {
1255
+ throw notFound(`this board doesn't run sync entry ${key}`, "not_synced_here");
1256
+ },
1257
+ async watchers(caller) {
1258
+ ownWatchers(caller, "list watchers");
1259
+ const who = whoOf(caller);
1260
+ const { watchers } = await api(who, "GET", await onBoard(who, "/watchers"));
1261
+ return { watchers: await Promise.all(watchers.map(async (found) => ({
1262
+ owner: found.owner,
1263
+ ...whereOf(found),
1264
+ watch: Object.fromEntries(await Promise.all(Object.entries(found.watch).map(async ([id, spec]) => [id, {
1265
+ tail: await tailOf(who, spec),
1266
+ when: spec.when
1267
+ }]))),
1268
+ sync: [],
1269
+ hash: found.hash
1270
+ }))) };
1271
+ },
1272
+ async register(caller, body) {
1273
+ ownWatchers(caller, "register watchers");
1274
+ if ((body.sync?.length ?? 0) > 0) throw notSupported("sync");
1275
+ return registerAs(whoOf(caller), typeof body.path === "string" ? {
1276
+ path: body.path,
1277
+ ...body.branch ? { branch: body.branch } : {}
1278
+ } : { url: body.url ?? null }, body.watch);
1279
+ },
1280
+ async matches(caller, query, signal) {
1281
+ ownWatchers(caller, "pull matches");
1282
+ return { matches: await pullAs(whoOf(caller), query, signal) };
1283
+ },
1284
+ async ack(caller, { id, ...body }) {
1285
+ ownWatchers(caller, "ack matches");
1286
+ await ackAs(whoOf(caller), id, body);
1287
+ return { ok: true };
1288
+ }
1289
+ };
1290
+ return {
1291
+ ...Object.fromEntries(Object.entries(methods).map(([name, method]) => [name, (...args) => {
1292
+ if (binding) runner.start();
1293
+ return method(...args);
1294
+ }])),
1295
+ routes: runner.routes,
1296
+ keeps: false,
1297
+ bind(next) {
1298
+ if (binding) throw new Error("vercel: this backend already serves a board: create one vercel() per board()");
1299
+ if (Object.values(next.subscriptions).map((subscription) => subscription.kind).includes("sync")) throw new Error("vercel: Vercel's boards don't run sync yet: leave `sync` out");
1300
+ for (const id of Object.keys(next.subscriptions)) own.add(id);
1301
+ tails = Object.fromEntries(Object.entries(next.subscriptions).flatMap(([id, subscription]) => subscription.kind === "tail" ? [[id, {
1302
+ tail: subscription.tail,
1303
+ when: subscription.when
1304
+ }]] : []));
1305
+ binding = next;
1306
+ },
1307
+ /** Registers this board's watchers with Vercel's API now, as `POST /watchers` does. */
1308
+ registerWatchers: runner.registerNow,
1309
+ /** Stops pulling, for a board that pulls on its own. */
1310
+ close: runner.close
1311
+ };
1312
+ }
1313
+ //#endregion
1314
+ export { createVercel as t };