@vercel/board 0.23.0 → 0.25.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 (74) hide show
  1. package/README.md +14 -19
  2. package/dist/{a2-NMEbIKNZ.mjs → a2-CQ3WcqMv.mjs} +6 -6
  3. package/dist/access.mjs +3 -3
  4. package/dist/{actors-rPnEQLZu.mjs → actors-CDDhhFNY.mjs} +2 -2
  5. package/dist/{auth-5nLsx9qB.mjs → auth-BwWH3Twu.mjs} +1 -1
  6. package/dist/{backend-CWmd4UH-.d.mts → backend-DmXjVnil.d.mts} +7 -3
  7. package/dist/backend-a2.d.mts +2 -2
  8. package/dist/backend-a2.mjs +1 -1
  9. package/dist/{backend-http-DQUsVgML.mjs → backend-http-BJ7AdK0c.mjs} +6 -6
  10. package/dist/{backend-http-BfWQfvqB.d.mts → backend-http-BsP0Xq9e.d.mts} +2 -2
  11. package/dist/{backend-http-D_rjJVmW.mjs → backend-http-Cs-UKpYp.mjs} +3 -3
  12. package/dist/backend-http.d.mts +1 -1
  13. package/dist/backend-http.mjs +1 -1
  14. package/dist/backend-http.node.d.mts +2 -2
  15. package/dist/backend-http.node.mjs +1 -1
  16. package/dist/{backend-vercel-BSuIfOyF.mjs → backend-vercel-B0Q0K0O5.mjs} +3 -3
  17. package/dist/{backend-vercel-CYOqtLPc.d.mts → backend-vercel-CED673bx.d.mts} +2 -2
  18. package/dist/{backend-vercel-BWVAihZC.mjs → backend-vercel-CG2SYa03.mjs} +3 -3
  19. package/dist/backend-vercel.d.mts +1 -1
  20. package/dist/backend-vercel.mjs +1 -1
  21. package/dist/backend-vercel.node.d.mts +1 -1
  22. package/dist/backend-vercel.node.mjs +1 -1
  23. package/dist/{chat-provider-D0k3y8zY.d.mts → chat-provider-6eYkL2ef.d.mts} +1 -1
  24. package/dist/cli-commands.mjs +1 -1
  25. package/dist/cli-embedded.d.mts +57 -6
  26. package/dist/cli-embedded.mjs +40 -20
  27. package/dist/cli.mjs +44 -37
  28. package/dist/{client-ClznIBMg.mjs → client-BY-BCggB.mjs} +13 -7
  29. package/dist/{commands-D_ODblQY.mjs → commands-BtL4jFq8.mjs} +0 -30
  30. package/dist/{conformance-DQ9qVzWP.mjs → conformance-BZ53bxrG.mjs} +1 -1
  31. package/dist/conformance.d.mts +1 -1
  32. package/dist/conformance.mjs +1 -1
  33. package/dist/{engine-kHfSOXnQ.mjs → engine-SeaJ_CIY.mjs} +2 -2
  34. package/dist/{errors-CQ-fwxM3.mjs → errors-Bven0k29.mjs} +7 -3
  35. package/dist/{exports-DAVHon9y.mjs → exports-ByvYtxFT.mjs} +6 -6
  36. package/dist/exports-CPrIkpz3.d.mts +4 -0
  37. package/dist/{index-DsMPCv_K.d.mts → index-k2LfZxl-.d.mts} +3 -3
  38. package/dist/index.d.mts +3 -3
  39. package/dist/{index.default-TBybqjGV.mjs → index.default-DmfhBZZi.mjs} +1 -1
  40. package/dist/index.mjs +6 -6
  41. package/dist/index.node.d.mts +3 -3
  42. package/dist/index.node.mjs +4 -4
  43. package/dist/inspect/app.js +1 -1
  44. package/dist/{main-B5BCEQ0l.mjs → main-BNkywL2P.mjs} +713 -189
  45. package/dist/{middleware-DUjyQP_a.mjs → middleware-B0J7QBq7.mjs} +1 -1
  46. package/dist/{node-BeKPcokJ.mjs → node-C51_xzCb.mjs} +29 -52
  47. package/dist/{provider-D11Dfda0.d.mts → provider-C_sj4-GL.d.mts} +10 -1
  48. package/dist/{serve-BC218-NQ.mjs → serve-DYQ64Q5R.mjs} +6 -5
  49. package/dist/{server-BxUruJxv.mjs → server-BgK1sgj7.mjs} +5 -5
  50. package/dist/{serving-B307gppu.mjs → serving-rXQcEgTO.mjs} +1 -1
  51. package/dist/sync-github.d.mts +2 -2
  52. package/dist/sync-kapso.d.mts +2 -2
  53. package/dist/sync-linear.d.mts +2 -2
  54. package/dist/sync-slack.d.mts +2 -2
  55. package/dist/sync.chat.d.mts +1 -1
  56. package/dist/sync.d.mts +1 -1
  57. package/dist/{tokens-DUXaXwOj.mjs → tokens-U-XRlEfq.mjs} +2 -2
  58. package/dist/{watchers-hash-DYu8WWXj.mjs → watchers-hash-C4CJg7K1.mjs} +3 -3
  59. package/docs/01-board/01-mounting.md +4 -5
  60. package/docs/01-board/03-identity.md +16 -7
  61. package/docs/01-board/04-messages.md +2 -2
  62. package/docs/01-board/05-reading.md +16 -8
  63. package/docs/01-board/07-schedules.md +2 -2
  64. package/docs/01-board/09-access.md +5 -4
  65. package/docs/01-quickstart.md +8 -8
  66. package/docs/03-guides/01-coding-agents.md +21 -22
  67. package/docs/03-guides/02-inspector.md +1 -1
  68. package/docs/03-guides/03-ci.md +2 -2
  69. package/docs/04-reference/01-cli.md +95 -58
  70. package/docs/04-reference/02-board.md +4 -5
  71. package/docs/04-reference/03-http.md +4 -0
  72. package/docs/index.md +2 -3
  73. package/package.json +1 -1
  74. package/dist/exports-ggfE9jNv.d.mts +0 -4
@@ -1,5 +1,5 @@
1
1
  import { m as MESSAGE_TEXT_MAX, p as Labels } from "./schemas-Be08nyfW.mjs";
2
- import { t as HttpError } from "./errors-CQ-fwxM3.mjs";
2
+ import { t as HttpError } from "./errors-Bven0k29.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/server/middleware.ts
5
5
  /** A write the middleware didn't let through: the API answers 422 with what it responded. */
@@ -1,4 +1,4 @@
1
- import { t as BoardError } from "./client-ClznIBMg.mjs";
1
+ import { t as BoardError } from "./client-BY-BCggB.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -10,15 +10,6 @@ import { decodeJwt } from "jose";
10
10
  import { exchangeVercelOidcToken, getVercelOidcToken, getVercelToken } from "@vercel/oidc";
11
11
  //#region src/client/node.ts
12
12
  const configDir = () => join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "board");
13
- /** The board saved by `board use <url>`. */
14
- function savedUrl() {
15
- try {
16
- const { url } = JSON.parse(readFileSync(join(configDir(), "config.json"), "utf8"));
17
- return url;
18
- } catch {
19
- return;
20
- }
21
- }
22
13
  /**
23
14
  * A Git checkout's root: `.git` is a directory with a `HEAD`, or a worktree's
24
15
  * `gitdir:` file. A stray empty `.git` (say, in `~` or `/tmp`) isn't one.
@@ -32,10 +23,11 @@ function isCheckout(directory) {
32
23
  return false;
33
24
  }
34
25
  }
26
+ /** The project `cwd` is in: its Git checkout's root, or `cwd` outside one. */
35
27
  function projectDir(cwd = process.cwd()) {
36
28
  let directory = resolve(cwd);
37
29
  while (true) {
38
- if (existsSync(join(directory, ".board", "config.json")) || isCheckout(directory)) return directory;
30
+ if (isCheckout(directory)) return directory;
39
31
  const parent = dirname(directory);
40
32
  if (parent === directory) return resolve(cwd);
41
33
  directory = parent;
@@ -43,7 +35,7 @@ function projectDir(cwd = process.cwd()) {
43
35
  }
44
36
  /**
45
37
  * The nearest Vercel project link (`.vercel/project.json`), searching up from the current
46
- * directory to a Git checkout's root, as a project link is found. Null when there's none.
38
+ * directory to a Git checkout's root. Null when there's none.
47
39
  */
48
40
  function vercelProjectFile(cwd = process.cwd()) {
49
41
  let directory = resolve(cwd);
@@ -55,21 +47,6 @@ function vercelProjectFile(cwd = process.cwd()) {
55
47
  directory = parent;
56
48
  }
57
49
  }
58
- function projectUrl() {
59
- const path = join(projectDir(), ".board", "config.json");
60
- let contents;
61
- try {
62
- contents = readFileSync(path, "utf8");
63
- } catch (error) {
64
- if (error.code === "ENOENT") return void 0;
65
- throw error;
66
- }
67
- try {
68
- const { url } = JSON.parse(contents);
69
- if (typeof url === "string" && ["http:", "https:"].includes(new URL(url).protocol)) return url;
70
- } catch {}
71
- throw new BoardError("invalid_config", `invalid board link at ${path}: expected an http(s) url`, 0);
72
- }
73
50
  const LOOPBACK = /* @__PURE__ */ new Set([
74
51
  "127.0.0.1",
75
52
  "localhost",
@@ -122,7 +99,7 @@ function detectAgent(env = process.env) {
122
99
  profile: named,
123
100
  session: null
124
101
  });
125
- if (found.length > 1) throw new BoardError("ambiguous_agent", `several agent environments detected (${found.map((a) => a.profile).join(", ")}): choose one with a profile, or none`, 0);
102
+ if (found.length > 1) throw new BoardError("ambiguous_agent", `Several coding agents' environments are set (${found.map((a) => a.profile).join(", ")}), so it can't tell which one is writing.`, 0, { hint: "Pick one with a profile, or none to write as yourself." });
126
103
  return found[0] ?? null;
127
104
  }
128
105
  const expiry = (token) => {
@@ -180,7 +157,7 @@ async function personToken(audience, discovery) {
180
157
  return filled;
181
158
  }
182
159
  if (cached) return cached;
183
- if (!discovery.project_id) throw new BoardError("no_project", `the board at ${discovery.url} doesn't know its Vercel project`, 0);
160
+ if (!discovery.project_id) throw new BoardError("no_project", `The board at ${discovery.url} doesn't know its Vercel project, so there's no Vercel login token to mint for it.`, 0, { hint: "Pass a token it accepts in BOARD_TOKEN." });
184
161
  const authToken = await vercelLogin().catch(() => null);
185
162
  const project = await boardProject(audience, discovery, authToken);
186
163
  let development;
@@ -190,7 +167,7 @@ async function personToken(audience, discovery) {
190
167
  method: "POST",
191
168
  headers: { authorization: `Bearer ${authToken}` }
192
169
  });
193
- if (response.status === 403 || response.status === 404) throw new BoardError("no_access", "your Vercel account can't access this board's project: ask for access to it", response.status);
170
+ if (response.status === 403 || response.status === 404) throw new BoardError("no_access", "Your Vercel account can't access this board's project.", response.status, { hint: "Ask someone on the project's team for access to it." });
194
171
  if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
195
172
  development = (await response.json()).token;
196
173
  } catch (error) {
@@ -233,8 +210,8 @@ async function boardProject(url, discovery, authToken) {
233
210
  });
234
211
  return response.ok ? await response.json() : null;
235
212
  } : vercelCliApi, host);
236
- if (!found) throw new BoardError("no_access", `your Vercel login can't see the project ${host} belongs to: run \`vercel login\` or ask for access to it`, 0);
237
- if (found.projectId !== discovery.project_id) throw new BoardError("wrong_project", `the board at ${url} says its project is ${discovery.project_id}, but it's a deployment of ${found.projectId}, so vb won't sign in to it`, 0);
213
+ if (!found) throw new BoardError("no_access", `Your Vercel login can't see the project ${host} belongs to.`, 0, { hint: "Log in as someone on the project's team (vercel login), or ask for access to it." });
214
+ if (found.projectId !== discovery.project_id) throw new BoardError("wrong_project", `The board at ${url} says its project is ${discovery.project_id}, but it's a deployment of ${found.projectId}, so no token is minted for it.`, 0);
238
215
  return {
239
216
  id: found.projectId,
240
217
  team: found.ownerId.startsWith("team_") ? found.ownerId : null
@@ -253,7 +230,7 @@ async function tokenFromVercelCli(project) {
253
230
  const { stdout } = await promisify(execFile)("vercel", args, { timeout: 3e4 });
254
231
  return JSON.parse(stdout).token;
255
232
  } catch {
256
- throw new BoardError("not_logged_in", "not signed in to Vercel: run `vercel login`", 0);
233
+ throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
257
234
  }
258
235
  }
259
236
  /** Your Vercel username, display name, and avatar (Vercel serves avatars publicly). */
@@ -275,13 +252,13 @@ async function githubActionsToken(audience) {
275
252
  const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);
276
253
  url.searchParams.set("audience", audience);
277
254
  const response = await fetch(url, { headers: { authorization: `bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}` } });
278
- if (!response.ok) throw new BoardError("no_token", `GitHub Actions refused an OIDC token (${response.status}): does the job have \`permissions: id-token: write\`?`, 0);
255
+ if (!response.ok) throw new BoardError("no_token", `GitHub Actions refused an OIDC token (${response.status}).`, 0, { hint: "Give the job `permissions: id-token: write`." });
279
256
  return (await response.json()).value;
280
257
  }
281
- /** The board the environment points at: `BOARD_URL`, the project's link (`vb link`, `vb serve`), or `vb use`'s. */
258
+ /** The board the environment points at: `BOARD_URL`. */
282
259
  function boardUrl() {
283
- const url = (process.env.BOARD_URL ?? projectUrl() ?? savedUrl())?.replace(/\/+$/, "");
284
- if (!url) throw new BoardError("no_board", "no board URL: run `board link <url>`, set BOARD_URL, run `board use <url>`, or `board serve` for a local one", 0);
260
+ const url = process.env.BOARD_URL?.replace(/\/+$/, "");
261
+ if (!url) throw new BoardError("no_board", "There's no board URL: BOARD_URL isn't set.", 0, { hint: "Set BOARD_URL, or pass the URL to http()." });
285
262
  return url;
286
263
  }
287
264
  /**
@@ -295,9 +272,9 @@ function connect(url, options = {}) {
295
272
  const doFetch = guarded(url, options.fetch ?? ((input, init) => globalThis.fetch(input, init)), options.token);
296
273
  let discovery;
297
274
  const discover = () => discovery ??= doFetch(url).then(async (response) => {
298
- if (!response.ok) throw new BoardError("no_board", `no board at ${url} (${response.status})`, response.status);
275
+ if (!response.ok) throw new BoardError("no_board", `There's no board at ${url}: it answered ${response.status}.`, response.status);
299
276
  const found = await response.json();
300
- if (found.audience !== url) throw new BoardError("wrong_audience", `the board at ${url} says its URL is ${found.audience}: use that URL`, 0);
277
+ if (found.audience !== url) throw new BoardError("wrong_audience", `The board at ${url} says its URL is ${found.audience}.`, 0, { hint: `Use ${found.audience}.` });
301
278
  return found;
302
279
  });
303
280
  let cached = null;
@@ -411,7 +388,7 @@ async function hostedToken(url) {
411
388
  try {
412
389
  return await getVercelOidcToken({ audience: url });
413
390
  } catch (cause) {
414
- throw Object.assign(new BoardError("no_token", `cannot obtain Vercel OIDC credentials for ${url}: enable OIDC on the calling project or supply token / BOARD_TOKEN; configure Deployment Protection Trusted Sources for its project and environment`, 0), { cause });
391
+ throw Object.assign(new BoardError("no_token", `This deployment can't get a Vercel OIDC token for ${url}.`, 0, { hint: "Enable OIDC on its project, or pass a token (BOARD_TOKEN), and add its project to the board project's Deployment Protection Trusted Sources." }), { cause });
415
392
  }
416
393
  }
417
394
  /** A host this deployment answers on, as Vercel's system variables name them (bare hosts). */
@@ -437,7 +414,7 @@ function guarded(url, doFetch, suppliedToken) {
437
414
  return await through(doFetch, input, init, fromEnv);
438
415
  } catch (error) {
439
416
  if (!(error instanceof Wall)) throw error;
440
- if (process.env.VERCEL !== "1") throw new BoardError("protected", `${url} is behind Vercel Deployment Protection and rejected VERCEL_AUTOMATION_BYPASS_SECRET; check it against ${WHERE}`, error.status);
417
+ if (process.env.VERCEL !== "1") throw new BoardError("protected", `${url} is behind Vercel Deployment Protection, and it rejected VERCEL_AUTOMATION_BYPASS_SECRET.`, error.status, { hint: `Check VERCEL_AUTOMATION_BYPASS_SECRET against ${WHERE}.` });
441
418
  envRejected = true;
442
419
  }
443
420
  if (process.env.VERCEL === "1") {
@@ -446,7 +423,7 @@ function guarded(url, doFetch, suppliedToken) {
446
423
  headers.set("x-vercel-trusted-oidc-idp-token", token);
447
424
  }
448
425
  return through(doFetch, input, init, null).catch((error) => {
449
- throw error instanceof Wall ? new BoardError("protected", `${url} rejected Vercel OIDC access: configure Deployment Protection Trusted Sources for the calling project and environment`, error.status) : error;
426
+ throw error instanceof Wall ? new BoardError("protected", `${url} is behind Vercel Deployment Protection, and it rejected this deployment's Vercel OIDC token.`, error.status, { hint: "Add this project and environment to the board project's Deployment Protection Trusted Sources." }) : error;
450
427
  });
451
428
  }
452
429
  const used = await (secret ??= readBypass(host));
@@ -468,7 +445,7 @@ function guarded(url, doFetch, suppliedToken) {
468
445
  return through(doFetch, input, init, found);
469
446
  });
470
447
  } catch (error) {
471
- throw error instanceof Wall ? new BoardError("protected", `${url} is behind Vercel Deployment Protection and its project's automation bypass didn't get through; set VERCEL_AUTOMATION_BYPASS_SECRET (${WHERE})`, error.status) : error;
448
+ throw error instanceof Wall ? new BoardError("protected", `${url} is behind Vercel Deployment Protection, and its project's automation bypass didn't get through.`, error.status, { hint: `Set VERCEL_AUTOMATION_BYPASS_SECRET (${WHERE}).` }) : error;
472
449
  }
473
450
  };
474
451
  }
@@ -498,7 +475,7 @@ async function through(doFetch, input, init, bypass) {
498
475
  const next = new URL(location, target);
499
476
  if (next.pathname.startsWith("/sso-api")) throw new Wall(response.status);
500
477
  if (method === "GET" || method === "HEAD") {
501
- if (hops >= 20) throw new BoardError("too_many_redirects", `too many redirects from ${target}`, response.status);
478
+ if (hops >= 20) throw new BoardError("too_many_redirects", `${target} redirected more than 20 times.`, response.status);
502
479
  if (next.origin !== new URL(target).origin) {
503
480
  headers = new Headers(headers);
504
481
  headers.delete("authorization");
@@ -540,7 +517,7 @@ const automationBypass = (bypasses) => Object.entries(bypasses ?? {}).find(([, v
540
517
  * `protectionBypass` has it. When there's none, it's created.
541
518
  */
542
519
  async function projectBypass(url, host, doFetch) {
543
- const blocked = (why) => new BoardError("protected", `${url} is behind Vercel Deployment Protection, and your Vercel login ${why}, or set VERCEL_AUTOMATION_BYPASS_SECRET (${WHERE})`, 0);
520
+ const blocked = (why, fix) => new BoardError("protected", `${url} is behind Vercel Deployment Protection, and your Vercel login ${why}.`, 0, { hint: `${fix}, or set VERCEL_AUTOMATION_BYPASS_SECRET (${WHERE}).` });
544
521
  let api;
545
522
  try {
546
523
  const token = await vercelLogin();
@@ -559,19 +536,19 @@ async function projectBypass(url, host, doFetch) {
559
536
  api = vercelCliApi;
560
537
  }
561
538
  const project = await findProject(api, host);
562
- if (!project) throw blocked("can't see its project: run `vercel login` or ask for access");
539
+ if (!project) throw blocked("can't see its project", "Log in as someone on its project's team (vercel login), ask for access");
563
540
  const scope = project.ownerId.startsWith("team_") ? `?teamId=${project.ownerId}` : "";
564
541
  const details = await api("GET", `/v9/projects/${project.projectId}${scope}`);
565
- if (!details) throw blocked("can't read its project's settings: ask for access");
542
+ if (!details) throw blocked("can't read its project's settings", "Ask for access to its project");
566
543
  const existing = automationBypass(details.protectionBypass);
567
544
  if (existing) return {
568
545
  secret: existing,
569
546
  created: false
570
547
  };
571
- if (!details.ssoProtection && !details.passwordProtection && !details.passport) throw new BoardError("no_board", `no board at ${url}: it answered with a web page, and its Vercel project isn't protected`, 0);
548
+ if (!details.ssoProtection && !details.passwordProtection && !details.passport) throw new BoardError("no_board", `There's no board at ${url}: it answered with a web page, and its Vercel project isn't protected.`, 0);
572
549
  const created = await api("PATCH", `/v1/projects/${project.projectId}/protection-bypass${scope}`);
573
550
  const secret = automationBypass(created?.protectionBypass);
574
- if (!secret) throw blocked("can't read or create its project's automation bypass: ask a project admin");
551
+ if (!secret) throw blocked("can't read or create its project's automation bypass", "Ask a project admin to create one");
575
552
  process.stderr.write(`created a Protection Bypass for Automation on the Vercel project ${details?.name ?? project.projectId}\n`);
576
553
  return {
577
554
  secret,
@@ -602,7 +579,7 @@ async function findProject(api, host) {
602
579
  let vercelCli;
603
580
  const vercelCliApi = async (method, path) => {
604
581
  vercelCli ??= promisify(execFile)("vercel", ["whoami", "--non-interactive"], { timeout: 1e4 }).then(() => true, () => false);
605
- if (!await vercelCli) throw new BoardError("not_logged_in", "not signed in to Vercel: run `vercel login`", 0);
582
+ if (!await vercelCli) throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
606
583
  const args = [
607
584
  "api",
608
585
  path,
@@ -618,9 +595,9 @@ const vercelCliApi = async (method, path) => {
618
595
  run.child.stdin?.end(method === "PATCH" ? "{}" : "");
619
596
  return JSON.parse((await run).stdout);
620
597
  } catch (error) {
621
- if (error.code === "ENOENT") throw new BoardError("not_logged_in", "not signed in to Vercel: run `vercel login`", 0);
598
+ if (error.code === "ENOENT") throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
622
599
  return null;
623
600
  }
624
601
  };
625
602
  //#endregion
626
- export { localTokenFile as a, vercelLogin as c, environmentProfile as i, vercelProjectFile as l, configDir as n, projectDir as o, connect as r, projectUrl as s, boardUrl as t };
603
+ export { projectDir as a, localTokenFile as i, connect as n, vercelLogin as o, environmentProfile as r, vercelProjectFile as s, boardUrl as t };
@@ -919,10 +919,12 @@ type SourceJoin = {
919
919
  name: string | null;
920
920
  joined: boolean;
921
921
  };
922
+ /** A failure: `code` to branch on, `message` to read, and `hint`, when the board knows one, for what fixes it. */
922
923
  type ApiError = {
923
924
  error: {
924
925
  code: string;
925
926
  message: string;
927
+ hint?: string;
926
928
  };
927
929
  };
928
930
  /** A raw platform event, as `GET /inbox/:key` returns it. */
@@ -1696,10 +1698,17 @@ declare const routes: {
1696
1698
  type RouteName = keyof typeof routes;
1697
1699
  //#endregion
1698
1700
  //#region src/client/client.d.ts
1701
+ /**
1702
+ * What went wrong: `message` says what and why, `code` is what to branch on, and `hint`, when the
1703
+ * handle knows one, says what fixes it (`Set VERCEL_AUTOMATION_BYPASS_SECRET…`).
1704
+ */
1699
1705
  declare class BoardError extends Error {
1700
1706
  readonly code: string;
1701
1707
  readonly status: number;
1702
- constructor(code: string, message: string, status: number);
1708
+ readonly hint: string | undefined;
1709
+ constructor(code: string, message: string, status: number, options?: {
1710
+ hint?: string;
1711
+ });
1703
1712
  }
1704
1713
  type Resolvable<T> = T | (() => T | Promise<T>);
1705
1714
  /** A profile name, `{ name, avatar }`, or null for no profile. */
@@ -1,7 +1,8 @@
1
- import { a as localTokenFile } from "./node-BeKPcokJ.mjs";
2
- import { t as board } from "./server-BxUruJxv.mjs";
3
- import { t as oidcTokens } from "./tokens-DUXaXwOj.mjs";
4
- import { n as fsBlob, t as a2 } from "./a2-NMEbIKNZ.mjs";
1
+ import { t as BoardError } from "./client-BY-BCggB.mjs";
2
+ import { i as localTokenFile } from "./node-C51_xzCb.mjs";
3
+ import { t as board } from "./server-BgK1sgj7.mjs";
4
+ import { t as oidcTokens } from "./tokens-U-XRlEfq.mjs";
5
+ import { n as fsBlob, t as a2 } from "./a2-CQ3WcqMv.mjs";
5
6
  import { t as localScheduler } from "./local-DhkrAqPP.mjs";
6
7
  import { mkdir, rm, writeFile } from "node:fs/promises";
7
8
  import { userInfo } from "node:os";
@@ -53,7 +54,7 @@ async function serveLocal(options) {
53
54
  port = await listen(server, options.port ?? 4100);
54
55
  } catch (error) {
55
56
  await store.close();
56
- if (error.code === "EADDRINUSE") throw new Error(`port ${options.port ?? 4100} is taken: is a board already running? Pick another with --port`);
57
+ if (error.code === "EADDRINUSE") throw new BoardError("port_taken", `Port ${options.port ?? 4100} is taken. Is a board already running there?`, 0);
57
58
  throw error;
58
59
  }
59
60
  const url = `http://127.0.0.1:${port}/api/board`;
@@ -1,16 +1,16 @@
1
- import { t as createBoard } from "./exports-DAVHon9y.mjs";
2
- import { t as http } from "./backend-http-D_rjJVmW.mjs";
1
+ import { t as createBoard } from "./exports-ByvYtxFT.mjs";
2
+ import { t as http } from "./backend-http-Cs-UKpYp.mjs";
3
3
  //#region src/server/index.ts
4
4
  /**
5
5
  * The handle on a board. With a `backend`, the board lives there: `a2()` keeps it in this
6
6
  * deployment, `http(url)` uses another. Without one, it's `http()` to the board the environment
7
- * points at: `BOARD_URL`, then the repo's link (`vb link`, `vb serve`), then `vb use`'s.
7
+ * points at: `BOARD_URL`.
8
8
  */
9
9
  function board(options = {}) {
10
10
  return createBoard(options, {
11
11
  http: () => http(),
12
- sync: () => import("./engine-kHfSOXnQ.mjs"),
13
- serving: () => import("./serving-B307gppu.mjs")
12
+ sync: () => import("./engine-SeaJ_CIY.mjs"),
13
+ serving: () => import("./serving-rXQcEgTO.mjs")
14
14
  });
15
15
  }
16
16
  //#endregion
@@ -1,2 +1,2 @@
1
- import { i as ownTeamId, n as discoveryIdentity, r as ownProjectId, t as oidcTokens } from "./tokens-DUXaXwOj.mjs";
1
+ import { i as ownTeamId, n as discoveryIdentity, r as ownProjectId, t as oidcTokens } from "./tokens-U-XRlEfq.mjs";
2
2
  export { discoveryIdentity, oidcTokens, ownProjectId, ownTeamId };
@@ -1,5 +1,5 @@
1
- import { S as SyncUnit, _ as withUserTokens, d as ResolvedTarget, f as SyncContext, h as UserTokens, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-D11Dfda0.mjs";
2
- import { n as ChatSyncProvider } from "./chat-provider-D0k3y8zY.mjs";
1
+ import { S as SyncUnit, _ as withUserTokens, d as ResolvedTarget, f as SyncContext, h as UserTokens, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-C_sj4-GL.mjs";
2
+ import { n as ChatSyncProvider } from "./chat-provider-6eYkL2ef.mjs";
3
3
  import { z } from "zod";
4
4
  import { Adapter } from "chat";
5
5
  //#region src/sync/github.d.ts
@@ -1,5 +1,5 @@
1
- import { S as SyncUnit, f as SyncContext, y as SyncFn } from "./provider-D11Dfda0.mjs";
2
- import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-D0k3y8zY.mjs";
1
+ import { S as SyncUnit, f as SyncContext, y as SyncFn } from "./provider-C_sj4-GL.mjs";
2
+ import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-6eYkL2ef.mjs";
3
3
  import { z } from "zod";
4
4
  import { Adapter } from "chat";
5
5
  //#region src/sync/kapso.d.ts
@@ -1,5 +1,5 @@
1
- import { S as SyncUnit, c as ReceivedEvent, d as ResolvedTarget, f as SyncContext, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-D11Dfda0.mjs";
2
- import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-D0k3y8zY.mjs";
1
+ import { S as SyncUnit, c as ReceivedEvent, d as ResolvedTarget, f as SyncContext, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-C_sj4-GL.mjs";
2
+ import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-6eYkL2ef.mjs";
3
3
  import { z } from "zod";
4
4
  import { Adapter } from "chat";
5
5
  //#region src/sync/linear.d.ts
@@ -1,6 +1,6 @@
1
1
  import { f as ExternalLink } from "./folds-CRYY14kc.mjs";
2
- import { C as channels, _ as withUserTokens, d as ResolvedTarget, f as SyncContext, h as UserTokens, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-D11Dfda0.mjs";
3
- import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-D0k3y8zY.mjs";
2
+ import { C as channels, _ as withUserTokens, d as ResolvedTarget, f as SyncContext, h as UserTokens, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-C_sj4-GL.mjs";
3
+ import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-6eYkL2ef.mjs";
4
4
  import { z } from "zod";
5
5
  import { Adapter } from "chat";
6
6
  //#region src/sync/slack.d.ts
@@ -1,2 +1,2 @@
1
- import { i as externalAuthor, n as ChatSyncProvider, r as ChatTransport, t as ChatInput } from "./chat-provider-D0k3y8zY.mjs";
1
+ import { i as externalAuthor, n as ChatSyncProvider, r as ChatTransport, t as ChatInput } from "./chat-provider-6eYkL2ef.mjs";
2
2
  export { ChatInput, ChatSyncProvider, ChatTransport, externalAuthor };
package/dist/sync.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { d as ExternalFile, l as ExternalAuthor, u as ExternalEvent } from "./folds-CRYY14kc.mjs";
2
- import { A as SyncOverrides, C as channels, D as EventBase, E as render, M as SyncRoot, N as envelope, O as ProviderEvent, P as eventSchema, S as SyncUnit, T as mirror, _ as withUserTokens, a as ProviderOptions, b as SyncIn, c as ReceivedEvent, d as ResolvedTarget, f as SyncContext, g as connectUserTokens, h as UserTokens, i as ProviderFile, j as SyncResult, k as RootOverride, l as RenderInput, m as UnreadableError, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, u as Renderer, v as Sync, w as into, x as SyncOut, y as SyncFn } from "./provider-D11Dfda0.mjs";
2
+ import { A as SyncOverrides, C as channels, D as EventBase, E as render, M as SyncRoot, N as envelope, O as ProviderEvent, P as eventSchema, S as SyncUnit, T as mirror, _ as withUserTokens, a as ProviderOptions, b as SyncIn, c as ReceivedEvent, d as ResolvedTarget, f as SyncContext, g as connectUserTokens, h as UserTokens, i as ProviderFile, j as SyncResult, k as RootOverride, l as RenderInput, m as UnreadableError, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, u as Renderer, v as Sync, w as into, x as SyncOut, y as SyncFn } from "./provider-C_sj4-GL.mjs";
3
3
  export { EventBase, type ExternalAuthor, ExternalEvent, type ExternalFile, type FileDelivery, type ProviderDelivery, type ProviderEvent, type ProviderFile, type ProviderOptions, type ProviderPost, type Receive, ReceivedEvent, type RenderInput, type Renderer, ResolvedTarget, type RootOverride, type Sync, type SyncContext, type SyncFn, type SyncIn, type SyncOut, SyncOverrides, SyncProvider, type SyncResult, type SyncRoot, type SyncUnit, UnreadableError, type UserTokens, channels, connectUserTokens, envelope, eventSchema, into, mirror, render, withUserTokens };
@@ -1,5 +1,5 @@
1
- import { l as unauthorized } from "./errors-CQ-fwxM3.mjs";
2
- import { f as markVerified, m as trustedIssuers, n as TOKENS, r as VERCEL_ISSUER, u as issuerName } from "./auth-5nLsx9qB.mjs";
1
+ import { l as unauthorized } from "./errors-Bven0k29.mjs";
2
+ import { f as markVerified, m as trustedIssuers, n as TOKENS, r as VERCEL_ISSUER, u as issuerName } from "./auth-BwWH3Twu.mjs";
3
3
  import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
4
4
  //#region src/server/identity.ts
5
5
  let fromDisk;
@@ -1,8 +1,8 @@
1
1
  import { n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
2
  import { S as storable, a as AttachmentId, c as ChannelPath, d as LabelKey, g as MessageText, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, p as Labels, x as platformAvatar } from "./schemas-Be08nyfW.mjs";
3
3
  import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin, t as ExternalAuthor } from "./external-BV_cUncs.mjs";
4
- import { r as badRequest, t as HttpError } from "./errors-CQ-fwxM3.mjs";
5
- import { o as options } from "./commands-D_ODblQY.mjs";
4
+ import { r as badRequest, t as HttpError } from "./errors-Bven0k29.mjs";
5
+ import { o as options } from "./commands-BtL4jFq8.mjs";
6
6
  import { z } from "zod";
7
7
  //#region src/api/routes.ts
8
8
  /**
@@ -662,7 +662,7 @@ const routes = {
662
662
  const SYNC = "@sync";
663
663
  //#endregion
664
664
  //#region src/version.ts
665
- const VERSION = "0.23.0";
665
+ const VERSION = "0.25.0";
666
666
  //#endregion
667
667
  //#region src/server/routing.ts
668
668
  /** board()'s own routes: the files it serves, and sync's, which run where board() does. */
@@ -19,7 +19,7 @@ export const GET = team.fetch, POST = team.fetch
19
19
  ```
20
20
 
21
21
  That's a board. It answers at `/api/board`, and the CLI points at it with
22
- `BOARD_URL` or `vb link`. `fetch` is its one handler; Next.js wants it under
22
+ `BOARD_URL` or `--url`. `fetch` is its one handler; Next.js wants it under
23
23
  each method's name. `a2()` keeps the board in the store you give it, or in
24
24
  SQLite at `.a2/dev.db` without one, which a serverless function can't write:
25
25
  see [Without a store](#without-a-store). Call `board()` once, at module scope.
@@ -63,7 +63,7 @@ A board on `http()` keeps nothing:
63
63
  - **It answers only wake-ups and webhooks,** and the sign-in routes of the
64
64
  sync it runs. Anything else, discovery included, answers 404 `<its URL>
65
65
  answers only wake-ups and webhooks`, and verifies nothing. People and the
66
- CLI use the other board: a repo links it with `vb link <its URL>`. With an
66
+ CLI use the other board, by its URL. With an
67
67
  `access` of its own, it serves its callers too, as a gateway: see
68
68
  [A board on `http()`](./09-access.md#a-board-on-http).
69
69
  - **Its handle writes there as this deployment:**
@@ -125,7 +125,7 @@ with credentials it recognizes:
125
125
  | `fetch` | How requests reach it, credentials included. In Node it defaults to what `vb` uses: a token whose audience is the other board's URL, from `BOARD_TOKEN`, `vb serve`'s, GitHub Actions, the Vercel runtime (a deployment's OIDC token), then `vercel login`, through Deployment Protection, with your handle (`BOARD_HANDLE`, then your Vercel username). In a browser it's `fetch`, sending the page's own session |
126
126
 
127
127
  In Node, `http()` without a `url` uses the board the environment points at:
128
- `BOARD_URL`, then the repo's link (`vb link`, `vb serve`), then `vb use`'s.
128
+ `BOARD_URL`.
129
129
 
130
130
  Set `url` when the board sits behind a proxy or rewrite that changes the
131
131
  origin it sees, so the audience it checks matches the one clients mint for.
@@ -335,8 +335,7 @@ const team = board()
335
335
  await team.post('deploys', 'www@a1b2c3 is live')
336
336
  ```
337
337
 
338
- In Node it finds that board the way `vb` does: `BOARD_URL`, then the repo's
339
- link (`vb link`, `vb serve`), then `vb use`'s. It acts as whoever its `fetch`'s
338
+ In Node it finds that board in `BOARD_URL`, as `vb` does. It acts as whoever its `fetch`'s
340
339
  credentials say, and the default one sends a token from `BOARD_TOKEN`, a local
341
340
  board's token file (`vb serve`), GitHub Actions OIDC, Vercel OIDC, then your
342
341
  `vercel login`. Its profile is `BOARD_AS`, or the coding agent it runs in,
@@ -67,18 +67,26 @@ source vercel login
67
67
  token expires in 11h 42m
68
68
  ```
69
69
 
70
- When it can't, it says what fixes it:
70
+ When it can't, it says why, then what fixes it:
71
71
 
72
72
  ```text
73
- error: not signed in to Vercel: run `vercel login`
74
- error: your Vercel login can't see the project www.example.com belongs to: run `vercel login` or ask for access to it
75
- error: your Vercel account can't access this board's project: ask for access to it
73
+ $ vb whoami
74
+ error: You're not logged in to Vercel.
75
+
76
+ Log in:
77
+ vercel login
78
+ ```
79
+
80
+ ```text
81
+ error: Your Vercel login can't see the project www.example.com belongs to.
82
+
83
+ Log in as someone on the project's team (vercel login), or ask for access to it.
76
84
  ```
77
85
 
78
86
  When the board's host is another project's deployment, it gets no token:
79
87
 
80
88
  ```text
81
- error: the board at https://www.example.com/api/board says its project is prj_abc123, but it's a deployment of prj_def456, so vb won't sign in to it
89
+ error: The board at https://www.example.com/api/board says its project is prj_abc123, but it's a deployment of prj_def456, so no token is minted for it.
82
90
  ```
83
91
 
84
92
  ## How the board verifies
@@ -248,10 +256,11 @@ vb reply 812 "@ada can you look? cc @julian/claude"
248
256
 
249
257
  ```text
250
258
  $ vb post errors-www "@ada the rollback is live"
251
- error: ambiguous mention @ada: use @[slack:user:U0456] (ada) or @[vercel:user:u_9f] (ada)
259
+ error: Can't post to errors-www.
260
+ ambiguous mention @ada: use @[slack:user:U0456] (ada) or @[vercel:user:u_9f] (ada)
252
261
  ```
253
262
 
254
- An `@[actor-id]` the board doesn't know fails with `error: unknown actor`. For
263
+ An `@[actor-id]` the board doesn't know fails too, the board saying `unknown actor`. For
255
264
  a caller that can't read every channel, an ambiguous mention lists no
256
265
  candidates: `use @[actor-id]`. A caller that can't read the channel it writes
257
266
  in gets no warning for a handle no one has, since it would say which handles
@@ -142,8 +142,8 @@ vb download nXHY2NjUHuzE5F1QpXyBdQ # saves after.png here; a path, or
142
142
  board's copy. See [Sync](../02-sync/01-overview.md).
143
143
  - **They need a blob store.** A board on `a2()` without one keeps no files:
144
144
  an upload, a post or schedule that carries files, and a download answer 501
145
- `no_files`, and `vb` prints
146
- ``error: this board keeps no files: pass `blob` to `a2()` ``. Sync brings a
145
+ `no_files`, and `vb` says it can't, with the board's words under it:
146
+ ``this board keeps no files: pass `blob` to `a2()` ``. Sync brings a
147
147
  platform's files over as links to where they live instead. See
148
148
  [Mounting](./01-mounting.md#without-a-blob-store).
149
149
 
@@ -28,12 +28,19 @@ errors-www · 2 threads · cursor 815
28
28
  pending [scheduled messages](./07-schedules.md).
29
29
  - **`vb read`** with no channel reads across every channel, and each summary
30
30
  names its channel.
31
- - **`--json`** prints one `{ message, replies, last }` per line (JSONL): the
32
- root, its reply count, and its latest reply or `null`. `vb read <id> --json`
33
- prints one message per line.
31
+ - **`--json`** prints `{ threads, cursor }`, each thread `{ message, replies,
32
+ last }`: the root, its reply count, and its latest reply or `null`.
34
33
 
35
- A channel that doesn't exist is an error (`no channel errors-wwww`), so a typo
36
- doesn't read as an empty channel.
34
+ A channel that doesn't exist is an error, so a typo doesn't read as an empty
35
+ channel:
36
+
37
+ ```text
38
+ $ vb read errors-wwww
39
+ error: Couldn't find channel errors-wwww. Did you mean errors-www?
40
+
41
+ See its channels:
42
+ vb channels
43
+ ```
37
44
 
38
45
  ## One thread
39
46
 
@@ -49,7 +56,7 @@ $ vb read 812
49
56
  ```
50
57
 
51
58
  `vb read <id>` prints every message in the thread, oldest first, and `--json`
52
- prints one message per line. Filters narrow it to the root and the replies that
59
+ prints `{ message, replies, cursor, schedules }`. Filters narrow it to the root and the replies that
53
60
  match: `vb read 812 --by ada --label status=investigating`. Each reaction
54
61
  names its first three people and counts the rest (`👀 ada, claude, codex +9`);
55
62
  `--json` has them all.
@@ -125,8 +132,9 @@ vb wait --re 812 --timeout 30m
125
132
  ```
126
133
 
127
134
  `vb wait [channel] [filters]` blocks until the next matching event, prints it,
128
- and exits 0. If nothing matches in time it prints `timed out` on stderr and
129
- exits 124, the GNU `timeout` convention. The timeout defaults to `30m` and takes
135
+ and exits 0. If nothing matches in time it says so on stderr
136
+ (`error: Nothing matched within 30m.`) and exits 124, the GNU `timeout`
137
+ convention. The timeout defaults to `30m` and takes
130
138
  `30s`, `10m`, `1h`, or `1d`.
131
139
 
132
140
  Like `tail`, it starts at the head. Pass `--after` to start from a known
@@ -105,8 +105,8 @@ await team.cancelSchedule(reminder.id) // { schedule, canceled }
105
105
 
106
106
  - **It runs on the scheduler.** A board on `a2()` without one refuses:
107
107
  `POST /schedules`, `schedule()`, and `vb schedule` answer 501
108
- `no_scheduler`, and `vb` prints
109
- ``error: this board has no scheduler: pass `scheduler` to `a2()` to send later``.
108
+ `no_scheduler`, and `vb` says it can't, with the board's words under it:
109
+ ``this board has no scheduler: pass `scheduler` to `a2()` to send later``.
110
110
  See [Without a scheduler](./01-mounting.md#without-a-scheduler). In
111
111
  production, pass `vercelQueues()` or `qstash()` and mount
112
112
  [its route](./01-mounting.md#the-scheduler-route). On a dev server, in
@@ -25,14 +25,15 @@ The board's environment is the one Vercel runs it in (`VERCEL_TARGET_ENV`,
25
25
  which names custom environments, or `VERCEL_ENV`). A production board trusts
26
26
  its production and refuses its previews, which run any branch's code. A
27
27
  preview board trusts its own previews, so a preview can call itself. Outside
28
- Vercel, only people get in without a rule.
28
+ Vercel, only people get in without a rule. A refused token gets a 403 that
29
+ says what would let it in:
29
30
 
30
31
  ```text
31
- error: this token can't use this board: it runs in production, so its preview deployments need vercelProject({ project: 'self', environments: ['preview'], can })
32
+ this token can't use this board: it runs in production, so its preview deployments need vercelProject({ project: 'self', environments: ['preview'], can })
32
33
  ```
33
34
 
34
35
  A token no rule matches is refused the same way, with the rule that would let
35
- it in: `error: this token can't use this board: add githubActions({ repository:
36
+ it in: `this token can't use this board: add githubActions({ repository:
36
37
  'acme/www', can }) to access`.
37
38
 
38
39
  ## Configure it
@@ -408,7 +409,7 @@ Reads show only what the caller can read, everywhere:
408
409
  - **What you can't read answers 404,** as if it didn't exist. On `a2()`,
409
410
  message IDs are sequential, so a 403 would confirm a message is there.
410
411
  - **A refused write you can read answers 403** and names what's missing:
411
- `error: this token can't label in dev`.
412
+ `this token can't label in dev`.
412
413
  - **A channel the request names is yours to know,** so a post to a channel you
413
414
  can't post in is 403, read or not.
414
415
  - **Every check is against the channel the board looked up.** A reply's