@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.
- package/README.md +14 -19
- package/dist/{a2-NMEbIKNZ.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-CWmd4UH-.d.mts → backend-DmXjVnil.d.mts} +7 -3
- package/dist/backend-a2.d.mts +2 -2
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-DQUsVgML.mjs → backend-http-BJ7AdK0c.mjs} +6 -6
- package/dist/{backend-http-BfWQfvqB.d.mts → backend-http-BsP0Xq9e.d.mts} +2 -2
- package/dist/{backend-http-D_rjJVmW.mjs → backend-http-Cs-UKpYp.mjs} +3 -3
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +2 -2
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-vercel-BSuIfOyF.mjs → backend-vercel-B0Q0K0O5.mjs} +3 -3
- package/dist/{backend-vercel-CYOqtLPc.d.mts → backend-vercel-CED673bx.d.mts} +2 -2
- package/dist/{backend-vercel-BWVAihZC.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-commands.mjs +1 -1
- package/dist/cli-embedded.d.mts +57 -6
- package/dist/cli-embedded.mjs +40 -20
- package/dist/cli.mjs +44 -37
- package/dist/{client-ClznIBMg.mjs → client-BY-BCggB.mjs} +13 -7
- package/dist/{commands-D_ODblQY.mjs → commands-BtL4jFq8.mjs} +0 -30
- 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-DAVHon9y.mjs → exports-ByvYtxFT.mjs} +6 -6
- package/dist/exports-CPrIkpz3.d.mts +4 -0
- package/dist/{index-DsMPCv_K.d.mts → index-k2LfZxl-.d.mts} +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/{index.default-TBybqjGV.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-B5BCEQ0l.mjs → main-BNkywL2P.mjs} +713 -189
- package/dist/{middleware-DUjyQP_a.mjs → middleware-B0J7QBq7.mjs} +1 -1
- package/dist/{node-BeKPcokJ.mjs → node-C51_xzCb.mjs} +29 -52
- package/dist/{provider-D11Dfda0.d.mts → provider-C_sj4-GL.d.mts} +10 -1
- package/dist/{serve-BC218-NQ.mjs → serve-DYQ64Q5R.mjs} +6 -5
- package/dist/{server-BxUruJxv.mjs → server-BgK1sgj7.mjs} +5 -5
- 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-DYu8WWXj.mjs → watchers-hash-C4CJg7K1.mjs} +3 -3
- package/docs/01-board/01-mounting.md +4 -5
- 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/01-quickstart.md +8 -8
- package/docs/03-guides/01-coding-agents.md +21 -22
- package/docs/03-guides/02-inspector.md +1 -1
- package/docs/03-guides/03-ci.md +2 -2
- package/docs/04-reference/01-cli.md +95 -58
- package/docs/04-reference/02-board.md +4 -5
- package/docs/04-reference/03-http.md +4 -0
- package/docs/index.md +2 -3
- package/package.json +1 -1
- 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-
|
|
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";
|
|
@@ -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 (
|
|
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
|
|
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", `
|
|
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", `
|
|
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", "
|
|
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", `
|
|
237
|
-
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);
|
|
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
|
|
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}):
|
|
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
|
|
258
|
+
/** The board the environment points at: `BOARD_URL`. */
|
|
282
259
|
function boardUrl() {
|
|
283
|
-
const url =
|
|
284
|
-
if (!url) throw new BoardError("no_board", "no board URL:
|
|
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}
|
|
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", `
|
|
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", `
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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})
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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,16 +1,16 @@
|
|
|
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
|
|
6
6
|
* deployment, `http(url)` uses another. Without one, it's `http()` to the board the environment
|
|
7
|
-
* points at: `BOARD_URL
|
|
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-
|
|
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,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-
|
|
5
|
-
import { o as options } from "./commands-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|