@vercel/board 0.24.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.
- package/dist/{a2-BxfKZFvJ.mjs → a2-CQ3WcqMv.mjs} +6 -6
- package/dist/access.mjs +3 -3
- package/dist/{actors-rPnEQLZu.mjs → actors-CDDhhFNY.mjs} +2 -2
- package/dist/{auth-5nLsx9qB.mjs → auth-BwWH3Twu.mjs} +1 -1
- package/dist/{backend-D0GDYiFr.d.mts → backend-DmXjVnil.d.mts} +6 -2
- package/dist/backend-a2.d.mts +2 -2
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-rfiG-AKP.mjs → backend-http-BJ7AdK0c.mjs} +6 -6
- package/dist/{backend-http-Dvq2p__1.d.mts → backend-http-BsP0Xq9e.d.mts} +2 -2
- package/dist/{backend-http-tGUo0e8j.mjs → backend-http-Cs-UKpYp.mjs} +2 -2
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-vercel-C3zuxmcD.mjs → backend-vercel-B0Q0K0O5.mjs} +3 -3
- package/dist/{backend-vercel-01QKDobk.d.mts → backend-vercel-CED673bx.d.mts} +2 -2
- package/dist/{backend-vercel-DM5PyMc2.mjs → backend-vercel-CG2SYa03.mjs} +3 -3
- package/dist/backend-vercel.d.mts +1 -1
- package/dist/backend-vercel.mjs +1 -1
- package/dist/backend-vercel.node.d.mts +1 -1
- package/dist/backend-vercel.node.mjs +1 -1
- package/dist/{chat-provider-D0k3y8zY.d.mts → chat-provider-6eYkL2ef.d.mts} +1 -1
- package/dist/cli-embedded.d.mts +55 -4
- package/dist/cli-embedded.mjs +38 -18
- package/dist/cli.mjs +4 -4
- package/dist/{client-ClznIBMg.mjs → client-BY-BCggB.mjs} +13 -7
- package/dist/{conformance-DQ9qVzWP.mjs → conformance-BZ53bxrG.mjs} +1 -1
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-kHfSOXnQ.mjs → engine-SeaJ_CIY.mjs} +2 -2
- package/dist/{errors-CQ-fwxM3.mjs → errors-Bven0k29.mjs} +7 -3
- package/dist/{exports-Bw81oDDl.mjs → exports-ByvYtxFT.mjs} +6 -6
- package/dist/exports-CPrIkpz3.d.mts +4 -0
- package/dist/{index-B_hWanKF.d.mts → index-k2LfZxl-.d.mts} +2 -2
- package/dist/index.d.mts +3 -3
- package/dist/{index.default-B6oPlvEa.mjs → index.default-DmfhBZZi.mjs} +1 -1
- package/dist/index.mjs +6 -6
- package/dist/index.node.d.mts +3 -3
- package/dist/index.node.mjs +4 -4
- package/dist/inspect/app.js +1 -1
- package/dist/{main-CzWvRy91.mjs → main-BNkywL2P.mjs} +706 -141
- package/dist/{middleware-DUjyQP_a.mjs → middleware-B0J7QBq7.mjs} +1 -1
- package/dist/{node-CirFYqw0.mjs → node-C51_xzCb.mjs} +23 -23
- package/dist/{provider-D11Dfda0.d.mts → provider-C_sj4-GL.d.mts} +10 -1
- package/dist/{serve-DHyA-6FR.mjs → serve-DYQ64Q5R.mjs} +6 -5
- package/dist/{server-Cp2oIQOj.mjs → server-BgK1sgj7.mjs} +4 -4
- package/dist/{serving-B307gppu.mjs → serving-rXQcEgTO.mjs} +1 -1
- package/dist/sync-github.d.mts +2 -2
- package/dist/sync-kapso.d.mts +2 -2
- package/dist/sync-linear.d.mts +2 -2
- package/dist/sync-slack.d.mts +2 -2
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.d.mts +1 -1
- package/dist/{tokens-DUXaXwOj.mjs → tokens-U-XRlEfq.mjs} +2 -2
- package/dist/{watchers-hash-C_1jGA53.mjs → watchers-hash-C4CJg7K1.mjs} +2 -2
- package/docs/01-board/03-identity.md +16 -7
- package/docs/01-board/04-messages.md +2 -2
- package/docs/01-board/05-reading.md +16 -8
- package/docs/01-board/07-schedules.md +2 -2
- package/docs/01-board/09-access.md +5 -4
- package/docs/03-guides/01-coding-agents.md +4 -2
- package/docs/03-guides/03-ci.md +2 -2
- package/docs/04-reference/01-cli.md +78 -19
- package/docs/04-reference/02-board.md +2 -1
- package/docs/04-reference/03-http.md +4 -0
- package/package.json +1 -1
- package/dist/exports-87_yESpp.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-
|
|
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-
|
|
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";
|
|
@@ -99,7 +99,7 @@ function detectAgent(env = process.env) {
|
|
|
99
99
|
profile: named,
|
|
100
100
|
session: null
|
|
101
101
|
});
|
|
102
|
-
if (found.length > 1) throw new BoardError("ambiguous_agent", `
|
|
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." });
|
|
103
103
|
return found[0] ?? null;
|
|
104
104
|
}
|
|
105
105
|
const expiry = (token) => {
|
|
@@ -157,7 +157,7 @@ async function personToken(audience, discovery) {
|
|
|
157
157
|
return filled;
|
|
158
158
|
}
|
|
159
159
|
if (cached) return cached;
|
|
160
|
-
if (!discovery.project_id) throw new BoardError("no_project", `
|
|
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." });
|
|
161
161
|
const authToken = await vercelLogin().catch(() => null);
|
|
162
162
|
const project = await boardProject(audience, discovery, authToken);
|
|
163
163
|
let development;
|
|
@@ -167,7 +167,7 @@ async function personToken(audience, discovery) {
|
|
|
167
167
|
method: "POST",
|
|
168
168
|
headers: { authorization: `Bearer ${authToken}` }
|
|
169
169
|
});
|
|
170
|
-
if (response.status === 403 || response.status === 404) throw new BoardError("no_access", "
|
|
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." });
|
|
171
171
|
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
172
172
|
development = (await response.json()).token;
|
|
173
173
|
} catch (error) {
|
|
@@ -210,8 +210,8 @@ async function boardProject(url, discovery, authToken) {
|
|
|
210
210
|
});
|
|
211
211
|
return response.ok ? await response.json() : null;
|
|
212
212
|
} : vercelCliApi, host);
|
|
213
|
-
if (!found) throw new BoardError("no_access", `
|
|
214
|
-
if (found.projectId !== discovery.project_id) throw new BoardError("wrong_project", `
|
|
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);
|
|
215
215
|
return {
|
|
216
216
|
id: found.projectId,
|
|
217
217
|
team: found.ownerId.startsWith("team_") ? found.ownerId : null
|
|
@@ -230,7 +230,7 @@ async function tokenFromVercelCli(project) {
|
|
|
230
230
|
const { stdout } = await promisify(execFile)("vercel", args, { timeout: 3e4 });
|
|
231
231
|
return JSON.parse(stdout).token;
|
|
232
232
|
} catch {
|
|
233
|
-
throw new BoardError("not_logged_in", "not
|
|
233
|
+
throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
/** Your Vercel username, display name, and avatar (Vercel serves avatars publicly). */
|
|
@@ -252,13 +252,13 @@ async function githubActionsToken(audience) {
|
|
|
252
252
|
const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);
|
|
253
253
|
url.searchParams.set("audience", audience);
|
|
254
254
|
const response = await fetch(url, { headers: { authorization: `bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}` } });
|
|
255
|
-
if (!response.ok) throw new BoardError("no_token", `GitHub Actions refused an OIDC token (${response.status}):
|
|
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`." });
|
|
256
256
|
return (await response.json()).value;
|
|
257
257
|
}
|
|
258
258
|
/** The board the environment points at: `BOARD_URL`. */
|
|
259
259
|
function boardUrl() {
|
|
260
260
|
const url = process.env.BOARD_URL?.replace(/\/+$/, "");
|
|
261
|
-
if (!url) throw new BoardError("no_board", "no board URL: set BOARD_URL
|
|
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()." });
|
|
262
262
|
return url;
|
|
263
263
|
}
|
|
264
264
|
/**
|
|
@@ -272,9 +272,9 @@ function connect(url, options = {}) {
|
|
|
272
272
|
const doFetch = guarded(url, options.fetch ?? ((input, init) => globalThis.fetch(input, init)), options.token);
|
|
273
273
|
let discovery;
|
|
274
274
|
const discover = () => discovery ??= doFetch(url).then(async (response) => {
|
|
275
|
-
if (!response.ok) throw new BoardError("no_board", `no board at ${url}
|
|
275
|
+
if (!response.ok) throw new BoardError("no_board", `There's no board at ${url}: it answered ${response.status}.`, response.status);
|
|
276
276
|
const found = await response.json();
|
|
277
|
-
if (found.audience !== url) throw new BoardError("wrong_audience", `
|
|
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}.` });
|
|
278
278
|
return found;
|
|
279
279
|
});
|
|
280
280
|
let cached = null;
|
|
@@ -388,7 +388,7 @@ async function hostedToken(url) {
|
|
|
388
388
|
try {
|
|
389
389
|
return await getVercelOidcToken({ audience: url });
|
|
390
390
|
} catch (cause) {
|
|
391
|
-
throw Object.assign(new BoardError("no_token", `
|
|
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 });
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
394
|
/** A host this deployment answers on, as Vercel's system variables name them (bare hosts). */
|
|
@@ -414,7 +414,7 @@ function guarded(url, doFetch, suppliedToken) {
|
|
|
414
414
|
return await through(doFetch, input, init, fromEnv);
|
|
415
415
|
} catch (error) {
|
|
416
416
|
if (!(error instanceof Wall)) throw error;
|
|
417
|
-
if (process.env.VERCEL !== "1") throw new BoardError("protected", `${url} is behind Vercel Deployment Protection and rejected VERCEL_AUTOMATION_BYPASS_SECRET
|
|
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}.` });
|
|
418
418
|
envRejected = true;
|
|
419
419
|
}
|
|
420
420
|
if (process.env.VERCEL === "1") {
|
|
@@ -423,7 +423,7 @@ function guarded(url, doFetch, suppliedToken) {
|
|
|
423
423
|
headers.set("x-vercel-trusted-oidc-idp-token", token);
|
|
424
424
|
}
|
|
425
425
|
return through(doFetch, input, init, null).catch((error) => {
|
|
426
|
-
throw error instanceof Wall ? new BoardError("protected", `${url} rejected Vercel OIDC
|
|
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;
|
|
427
427
|
});
|
|
428
428
|
}
|
|
429
429
|
const used = await (secret ??= readBypass(host));
|
|
@@ -445,7 +445,7 @@ function guarded(url, doFetch, suppliedToken) {
|
|
|
445
445
|
return through(doFetch, input, init, found);
|
|
446
446
|
});
|
|
447
447
|
} catch (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
|
|
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;
|
|
449
449
|
}
|
|
450
450
|
};
|
|
451
451
|
}
|
|
@@ -475,7 +475,7 @@ async function through(doFetch, input, init, bypass) {
|
|
|
475
475
|
const next = new URL(location, target);
|
|
476
476
|
if (next.pathname.startsWith("/sso-api")) throw new Wall(response.status);
|
|
477
477
|
if (method === "GET" || method === "HEAD") {
|
|
478
|
-
if (hops >= 20) throw new BoardError("too_many_redirects",
|
|
478
|
+
if (hops >= 20) throw new BoardError("too_many_redirects", `${target} redirected more than 20 times.`, response.status);
|
|
479
479
|
if (next.origin !== new URL(target).origin) {
|
|
480
480
|
headers = new Headers(headers);
|
|
481
481
|
headers.delete("authorization");
|
|
@@ -517,7 +517,7 @@ const automationBypass = (bypasses) => Object.entries(bypasses ?? {}).find(([, v
|
|
|
517
517
|
* `protectionBypass` has it. When there's none, it's created.
|
|
518
518
|
*/
|
|
519
519
|
async function projectBypass(url, host, doFetch) {
|
|
520
|
-
const blocked = (why) => new BoardError("protected", `${url} is behind Vercel Deployment Protection, and your Vercel login ${why}, or set VERCEL_AUTOMATION_BYPASS_SECRET (${WHERE})
|
|
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}).` });
|
|
521
521
|
let api;
|
|
522
522
|
try {
|
|
523
523
|
const token = await vercelLogin();
|
|
@@ -536,19 +536,19 @@ async function projectBypass(url, host, doFetch) {
|
|
|
536
536
|
api = vercelCliApi;
|
|
537
537
|
}
|
|
538
538
|
const project = await findProject(api, host);
|
|
539
|
-
if (!project) throw blocked("can't see its project
|
|
539
|
+
if (!project) throw blocked("can't see its project", "Log in as someone on its project's team (vercel login), ask for access");
|
|
540
540
|
const scope = project.ownerId.startsWith("team_") ? `?teamId=${project.ownerId}` : "";
|
|
541
541
|
const details = await api("GET", `/v9/projects/${project.projectId}${scope}`);
|
|
542
|
-
if (!details) throw blocked("can't read its project's settings
|
|
542
|
+
if (!details) throw blocked("can't read its project's settings", "Ask for access to its project");
|
|
543
543
|
const existing = automationBypass(details.protectionBypass);
|
|
544
544
|
if (existing) return {
|
|
545
545
|
secret: existing,
|
|
546
546
|
created: false
|
|
547
547
|
};
|
|
548
|
-
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
|
|
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);
|
|
549
549
|
const created = await api("PATCH", `/v1/projects/${project.projectId}/protection-bypass${scope}`);
|
|
550
550
|
const secret = automationBypass(created?.protectionBypass);
|
|
551
|
-
if (!secret) throw blocked("can't read or create its project's automation bypass
|
|
551
|
+
if (!secret) throw blocked("can't read or create its project's automation bypass", "Ask a project admin to create one");
|
|
552
552
|
process.stderr.write(`created a Protection Bypass for Automation on the Vercel project ${details?.name ?? project.projectId}\n`);
|
|
553
553
|
return {
|
|
554
554
|
secret,
|
|
@@ -579,7 +579,7 @@ async function findProject(api, host) {
|
|
|
579
579
|
let vercelCli;
|
|
580
580
|
const vercelCliApi = async (method, path) => {
|
|
581
581
|
vercelCli ??= promisify(execFile)("vercel", ["whoami", "--non-interactive"], { timeout: 1e4 }).then(() => true, () => false);
|
|
582
|
-
if (!await vercelCli) throw new BoardError("not_logged_in", "not
|
|
582
|
+
if (!await vercelCli) throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
|
|
583
583
|
const args = [
|
|
584
584
|
"api",
|
|
585
585
|
path,
|
|
@@ -595,7 +595,7 @@ const vercelCliApi = async (method, path) => {
|
|
|
595
595
|
run.child.stdin?.end(method === "PATCH" ? "{}" : "");
|
|
596
596
|
return JSON.parse((await run).stdout);
|
|
597
597
|
} catch (error) {
|
|
598
|
-
if (error.code === "ENOENT") throw new BoardError("not_logged_in", "not
|
|
598
|
+
if (error.code === "ENOENT") throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
|
|
599
599
|
return null;
|
|
600
600
|
}
|
|
601
601
|
};
|
|
@@ -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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import { t as
|
|
4
|
-
import {
|
|
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
|
|
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,5 +1,5 @@
|
|
|
1
|
-
import { t as createBoard } from "./exports-
|
|
2
|
-
import { t as http } from "./backend-http-
|
|
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
|
|
@@ -9,8 +9,8 @@ import { t as http } from "./backend-http-tGUo0e8j.mjs";
|
|
|
9
9
|
function board(options = {}) {
|
|
10
10
|
return createBoard(options, {
|
|
11
11
|
http: () => http(),
|
|
12
|
-
sync: () => import("./engine-
|
|
13
|
-
serving: () => import("./serving-
|
|
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-
|
|
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 };
|
package/dist/sync-github.d.mts
CHANGED
|
@@ -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-
|
|
2
|
-
import { n as ChatSyncProvider } from "./chat-provider-
|
|
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
|
package/dist/sync-kapso.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as SyncUnit, f as SyncContext, y as SyncFn } from "./provider-
|
|
2
|
-
import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-
|
|
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
|
package/dist/sync-linear.d.mts
CHANGED
|
@@ -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-
|
|
2
|
-
import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-
|
|
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
|
package/dist/sync-slack.d.mts
CHANGED
|
@@ -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-
|
|
3
|
-
import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-
|
|
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
|
package/dist/sync.chat.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as externalAuthor, n as ChatSyncProvider, r as ChatTransport, t as ChatInput } from "./chat-provider-
|
|
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-
|
|
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-
|
|
2
|
-
import { f as markVerified, m as trustedIssuers, n as TOKENS, r as VERCEL_ISSUER, u as issuerName } from "./auth-
|
|
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,7 +1,7 @@
|
|
|
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-
|
|
4
|
+
import { r as badRequest, t as HttpError } from "./errors-Bven0k29.mjs";
|
|
5
5
|
import { o as options } from "./commands-BtL4jFq8.mjs";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
//#region src/api/routes.ts
|
|
@@ -662,7 +662,7 @@ const routes = {
|
|
|
662
662
|
const SYNC = "@sync";
|
|
663
663
|
//#endregion
|
|
664
664
|
//#region src/version.ts
|
|
665
|
-
const VERSION = "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. */
|
|
@@ -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
|
-
|
|
74
|
-
error:
|
|
75
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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`
|
|
146
|
-
``
|
|
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
|
|
32
|
-
root, its reply count, and its latest reply or `null`.
|
|
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
|
|
36
|
-
|
|
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
|
|
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
|
|
129
|
-
exits 124, the GNU `timeout`
|
|
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`
|
|
109
|
-
``
|
|
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
|
-
|
|
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: `
|
|
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
|
-
`
|
|
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
|
|
@@ -21,7 +21,7 @@ which team.
|
|
|
21
21
|
- **Credentials stay with you.** You sign in with your own `vercel login`,
|
|
22
22
|
and tokens are cached per user. Access to the board is access to its Vercel
|
|
23
23
|
project: an account without it gets
|
|
24
|
-
`error:
|
|
24
|
+
`error: Your Vercel account can't access this board's project.`
|
|
25
25
|
- **The URL picks a board, not a channel.** Agents choose channels per task.
|
|
26
26
|
|
|
27
27
|
Every command finds the board in this order, and the first match wins:
|
|
@@ -61,7 +61,9 @@ saying so), keep it per host in `~/.config/board/bypass/`, and send it as
|
|
|
61
61
|
sent instead. When neither gets through, the CLI says why:
|
|
62
62
|
|
|
63
63
|
```text
|
|
64
|
-
error: https://team.example/api/board is behind Vercel Deployment Protection, and your Vercel login can't see its project
|
|
64
|
+
error: https://team.example/api/board is behind Vercel Deployment Protection, and your Vercel login can't see its project.
|
|
65
|
+
|
|
66
|
+
Log in as someone on its project's team (vercel login), ask for access, or set VERCEL_AUTOMATION_BYPASS_SECRET (Project → Settings → Deployment Protection → Protection Bypass for Automation).
|
|
65
67
|
```
|
|
66
68
|
|
|
67
69
|
## Install the skill
|
package/docs/03-guides/03-ci.md
CHANGED
|
@@ -72,8 +72,8 @@ export const GET = team.fetch, POST = team.fetch
|
|
|
72
72
|
`githubActions()` matches the token's `repository`, plus `workflow`, `event`
|
|
73
73
|
(`push`, `pull_request`, or a list), and `ref` when you give them. The
|
|
74
74
|
workflow posts as `github:repo:<repository_id>`, with the handle
|
|
75
|
-
`acme/www`. A workflow that matches no rule gets
|
|
76
|
-
`
|
|
75
|
+
`acme/www`. A workflow that matches no rule gets a 403:
|
|
76
|
+
`this token can't use this board: add githubActions({ repository: 'acme/www', can }) to access`.
|
|
77
77
|
|
|
78
78
|
`can` says what it may do where. This one posts and labels in `errors-www`, for
|
|
79
79
|
the step above, and reads and posts in `deploys`, which the wait below needs.
|