@vercel/board 0.0.0 → 0.16.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 (126) hide show
  1. package/README.md +180 -0
  2. package/dist/a2-C41hD8SF.mjs +3045 -0
  3. package/dist/a2-D24cvwpN.d.mts +3359 -0
  4. package/dist/access-CJU2-vaa.mjs +226 -0
  5. package/dist/access-CV96SYgr.d.mts +32 -0
  6. package/dist/access.d.mts +30 -0
  7. package/dist/access.mjs +4 -0
  8. package/dist/actors-DjMeZpnU.mjs +224 -0
  9. package/dist/attempts-Bxp9KgPf.mjs +919 -0
  10. package/dist/auth-COzc09sn.mjs +276 -0
  11. package/dist/auth-CU56Q2Xh.d.mts +204 -0
  12. package/dist/backend-a2/blob-vercel.d.mts +8 -0
  13. package/dist/backend-a2/blob-vercel.mjs +81 -0
  14. package/dist/backend-a2/contract.d.mts +18 -0
  15. package/dist/backend-a2/contract.mjs +5 -0
  16. package/dist/backend-a2/scheduler-local.d.mts +11 -0
  17. package/dist/backend-a2/scheduler-local.mjs +2 -0
  18. package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
  19. package/dist/backend-a2/scheduler-qstash.mjs +2 -0
  20. package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
  21. package/dist/backend-a2/scheduler-vercel.mjs +2 -0
  22. package/dist/backend-a2/store-memory.d.mts +11 -0
  23. package/dist/backend-a2/store-memory.mjs +19 -0
  24. package/dist/backend-a2/store-postgres.d.mts +38 -0
  25. package/dist/backend-a2/store-postgres.mjs +110 -0
  26. package/dist/backend-a2/store-redis-http.d.mts +17 -0
  27. package/dist/backend-a2/store-redis-http.mjs +32 -0
  28. package/dist/backend-a2/store-redis.d.mts +23 -0
  29. package/dist/backend-a2/store-redis.mjs +39 -0
  30. package/dist/backend-a2/store-sqlite.d.mts +12 -0
  31. package/dist/backend-a2/store-sqlite.mjs +31 -0
  32. package/dist/backend-a2.d.mts +4 -0
  33. package/dist/backend-a2.mjs +2 -0
  34. package/dist/backend-http-0roEmjl0.mjs +613 -0
  35. package/dist/backend-http-BVRno-ca.mjs +469 -0
  36. package/dist/backend-http-Bid7RIpC.d.mts +630 -0
  37. package/dist/backend-http.d.mts +10 -0
  38. package/dist/backend-http.mjs +2 -0
  39. package/dist/backend-http.node.d.mts +10 -0
  40. package/dist/backend-http.node.mjs +2 -0
  41. package/dist/blob-C0J3wbGv.d.mts +39 -0
  42. package/dist/bots-CGkO76qH.mjs +67 -0
  43. package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
  44. package/dist/chat-provider-jFSeyNmB.mjs +389 -0
  45. package/dist/cli.d.mts +1 -0
  46. package/dist/cli.mjs +2252 -0
  47. package/dist/client-DGDPaN7U.mjs +380 -0
  48. package/dist/conformance-DRR23YNy.mjs +512 -0
  49. package/dist/conformance.d.mts +30 -0
  50. package/dist/conformance.mjs +2 -0
  51. package/dist/contract-CuJmlrhw.mjs +566 -0
  52. package/dist/emoji-CTLpvH-A.mjs +445 -0
  53. package/dist/engine-Bir5s8ts.mjs +1449 -0
  54. package/dist/errors-DX3hpwWt.mjs +64 -0
  55. package/dist/exports-DLiE7fLY.d.mts +4 -0
  56. package/dist/exports-Dvc1o52I.mjs +1285 -0
  57. package/dist/files-SniSmeEr.mjs +47 -0
  58. package/dist/index.d.mts +13 -0
  59. package/dist/index.default-Bkg77Pxn.mjs +12 -0
  60. package/dist/index.mjs +19 -0
  61. package/dist/index.node.d.mts +14 -0
  62. package/dist/index.node.mjs +5 -0
  63. package/dist/inspect/app.js +416 -0
  64. package/dist/inspect/engine.js +5 -0
  65. package/dist/local-Cr_dSjvs.mjs +44 -0
  66. package/dist/own-OwgPWdmh.mjs +15 -0
  67. package/dist/project-CFiArraX.mjs +124 -0
  68. package/dist/provider-BGjz-mDa.d.mts +2473 -0
  69. package/dist/provider-CuewcDNZ.mjs +205 -0
  70. package/dist/redis-tables-B35vv4CZ.mjs +842 -0
  71. package/dist/reducers-B_vUGQRp.d.mts +1172 -0
  72. package/dist/reducers-mUPFuFJV.mjs +293 -0
  73. package/dist/schemas-Be08nyfW.mjs +74 -0
  74. package/dist/schemas-CWxs3ECJ.d.mts +50 -0
  75. package/dist/serve-BsOOc-6_.mjs +170 -0
  76. package/dist/server-jwPY7_wi.mjs +17 -0
  77. package/dist/serving-BNbzc96L.mjs +2 -0
  78. package/dist/sql-DbiEnqM0.mjs +571 -0
  79. package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
  80. package/dist/sync-github.d.mts +473 -0
  81. package/dist/sync-github.mjs +603 -0
  82. package/dist/sync-kapso.d.mts +175 -0
  83. package/dist/sync-kapso.mjs +86 -0
  84. package/dist/sync-slack.d.mts +364 -0
  85. package/dist/sync-slack.mjs +728 -0
  86. package/dist/sync.chat.d.mts +2 -0
  87. package/dist/sync.chat.mjs +2 -0
  88. package/dist/sync.d.mts +3 -0
  89. package/dist/sync.mjs +4 -0
  90. package/dist/targets-yM5JBUgr.mjs +94 -0
  91. package/dist/tokens-DKLn5P9B.mjs +256 -0
  92. package/dist/types-BHCjTZdB.d.mts +104 -0
  93. package/dist/users-Dls7_ANV.mjs +42 -0
  94. package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
  95. package/dist/watchers-hash-CLjsa6od.mjs +225 -0
  96. package/docs/01-board/01-mounting.md +506 -0
  97. package/docs/01-board/02-database.md +258 -0
  98. package/docs/01-board/03-identity.md +264 -0
  99. package/docs/01-board/04-messages.md +192 -0
  100. package/docs/01-board/05-reading.md +177 -0
  101. package/docs/01-board/06-watchers.md +249 -0
  102. package/docs/01-board/07-schedules.md +133 -0
  103. package/docs/01-board/08-middleware.md +139 -0
  104. package/docs/01-board/09-access.md +501 -0
  105. package/docs/01-quickstart.md +196 -0
  106. package/docs/02-sync/01-overview.md +290 -0
  107. package/docs/02-sync/02-callbacks.md +265 -0
  108. package/docs/02-sync/03-slack.md +248 -0
  109. package/docs/02-sync/04-github.md +201 -0
  110. package/docs/02-sync/05-whatsapp.md +148 -0
  111. package/docs/02-sync/06-providers.md +228 -0
  112. package/docs/03-guides/01-coding-agents.md +226 -0
  113. package/docs/03-guides/02-inspector.md +104 -0
  114. package/docs/03-guides/03-ci.md +251 -0
  115. package/docs/03-guides/04-triage.md +233 -0
  116. package/docs/03-guides/05-agent-factory.md +142 -0
  117. package/docs/04-reference/01-cli.md +606 -0
  118. package/docs/04-reference/02-board.md +579 -0
  119. package/docs/04-reference/03-http.md +489 -0
  120. package/docs/04-reference/04-log.md +250 -0
  121. package/docs/04-reference/05-conformance.md +97 -0
  122. package/docs/index.md +186 -0
  123. package/package.json +168 -1
  124. package/sql/postgres/board.sql +145 -0
  125. package/sql/postgres/notifications.sql +24 -0
  126. package/sql/postgres/schema.sql +451 -0
@@ -0,0 +1,613 @@
1
+ import { t as BoardError } from "./client-DGDPaN7U.mjs";
2
+ import { t as createHttp } from "./backend-http-BVRno-ca.mjs";
3
+ import { execFile } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
6
+ import { existsSync, readFileSync, statSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { promisify } from "node:util";
10
+ import { decodeJwt } from "jose";
11
+ import { exchangeVercelOidcToken, getVercelOidcToken, getVercelToken } from "@vercel/oidc";
12
+ //#region src/client/node.ts
13
+ const configDir = () => join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "board");
14
+ /** The board saved by `board use <url>`. */
15
+ function savedUrl() {
16
+ try {
17
+ const { url } = JSON.parse(readFileSync(join(configDir(), "config.json"), "utf8"));
18
+ return url;
19
+ } catch {
20
+ return;
21
+ }
22
+ }
23
+ /**
24
+ * A Git checkout's root: `.git` is a directory with a `HEAD`, or a worktree's
25
+ * `gitdir:` file. A stray empty `.git` (say, in `~` or `/tmp`) isn't one.
26
+ */
27
+ function isCheckout(directory) {
28
+ const git = join(directory, ".git");
29
+ try {
30
+ if (statSync(git).isDirectory()) return existsSync(join(git, "HEAD"));
31
+ return readFileSync(git, "utf8").startsWith("gitdir:");
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ function projectDir(cwd = process.cwd()) {
37
+ let directory = resolve(cwd);
38
+ while (true) {
39
+ if (existsSync(join(directory, ".board", "config.json")) || isCheckout(directory)) return directory;
40
+ const parent = dirname(directory);
41
+ if (parent === directory) return resolve(cwd);
42
+ directory = parent;
43
+ }
44
+ }
45
+ function projectUrl() {
46
+ const path = join(projectDir(), ".board", "config.json");
47
+ let contents;
48
+ try {
49
+ contents = readFileSync(path, "utf8");
50
+ } catch (error) {
51
+ if (error.code === "ENOENT") return void 0;
52
+ throw error;
53
+ }
54
+ try {
55
+ const { url } = JSON.parse(contents);
56
+ if (typeof url === "string" && ["http:", "https:"].includes(new URL(url).protocol)) return url;
57
+ } catch {}
58
+ throw new BoardError("invalid_config", `invalid board link at ${path}: expected an http(s) url`, 0);
59
+ }
60
+ const LOOPBACK = /* @__PURE__ */ new Set([
61
+ "127.0.0.1",
62
+ "localhost",
63
+ "[::1]"
64
+ ]);
65
+ /** Where `vb serve` leaves the token for the board it runs at `url`: only this user can read it. */
66
+ function localTokenFile(url) {
67
+ const normalized = url.replace(/\/+$/, "");
68
+ return join(configDir(), "local", `${createHash("sha256").update(normalized).digest("hex").slice(0, 32)}.json`);
69
+ }
70
+ /** The token a local board on this machine left for you, if `url` is one. */
71
+ function localToken(url) {
72
+ try {
73
+ if (!LOOPBACK.has(new URL(url).hostname)) return null;
74
+ const saved = JSON.parse(readFileSync(localTokenFile(url), "utf8"));
75
+ if (saved.url !== url.replace(/\/+$/, "") || typeof saved.token !== "string") return null;
76
+ return {
77
+ token: saved.token,
78
+ handle: saved.handle ?? null
79
+ };
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ /** Claude Code, Codex, and Grok sessions select their own profile, like Just Chat's CLI. */
85
+ function detectAgent(env = process.env) {
86
+ const found = [];
87
+ if (env.CLAUDECODE === "1" || env.CLAUDE_CODE_SESSION_ID) found.push({
88
+ profile: "claude",
89
+ session: env.CLAUDE_CODE_SESSION_ID || null
90
+ });
91
+ if (env.CODEX_THREAD_ID || env.CODEX_SESSION_ID) found.push({
92
+ profile: "codex",
93
+ session: env.CODEX_THREAD_ID || env.CODEX_SESSION_ID || null
94
+ });
95
+ if (env.GROK_SESSION_ID) found.push({
96
+ profile: "grok",
97
+ session: env.GROK_SESSION_ID
98
+ });
99
+ if (env.CURSOR_AGENT === "1") found.push({
100
+ profile: "cursor",
101
+ session: null
102
+ });
103
+ if (env.PI_CODING_AGENT === "true" || env.AI_AGENT === "pi") found.push({
104
+ profile: "pi",
105
+ session: env.PI_SESSION_ID || null
106
+ });
107
+ const named = env.AI_AGENT?.trim().toLowerCase();
108
+ if (named && /^[a-z][a-z0-9-]{0,31}$/.test(named) && !found.some((agent) => agent.profile === named)) found.push({
109
+ profile: named,
110
+ session: null
111
+ });
112
+ 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);
113
+ return found[0] ?? null;
114
+ }
115
+ const expiry = (token) => {
116
+ try {
117
+ const { exp } = decodeJwt(token);
118
+ return exp ? exp * 1e3 : Date.now() + 3e5;
119
+ } catch {
120
+ return Date.now() + 3e5;
121
+ }
122
+ };
123
+ const cacheFile = (audience) => join(configDir(), "tokens", `${createHash("sha256").update(audience).digest("hex").slice(0, 32)}.json`);
124
+ async function readCache(audience) {
125
+ try {
126
+ const cached = JSON.parse(await readFile(cacheFile(audience), "utf8"));
127
+ return cached.expiresAt - 3e5 > Date.now() ? cached : null;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+ async function writeCache(audience, cached) {
133
+ await mkdir(join(configDir(), "tokens"), {
134
+ recursive: true,
135
+ mode: 448
136
+ });
137
+ await writeFile(cacheFile(audience), JSON.stringify(cached), { mode: 384 });
138
+ }
139
+ /**
140
+ * A person's token: their Vercel CLI login mints a development token for the
141
+ * project the board's host is a deployment of, exchanged so its audience is the
142
+ * board's URL. Cached on disk for its 12-hour life.
143
+ */
144
+ async function personToken(audience, discovery) {
145
+ const cached = await readCache(audience);
146
+ if (cached && cached.name === void 0) {
147
+ const user = await getVercelToken().then(vercelUser).catch(() => null);
148
+ const filled = {
149
+ ...cached,
150
+ name: user?.name ?? null,
151
+ avatar: user?.avatar ?? null
152
+ };
153
+ await writeCache(audience, filled).catch(() => {});
154
+ return filled;
155
+ }
156
+ if (cached) return cached;
157
+ if (!discovery.project_id) throw new BoardError("no_project", `the board at ${discovery.url} doesn't know its Vercel project`, 0);
158
+ const authToken = await getVercelToken().catch(() => null);
159
+ const project = await boardProject(audience, discovery, authToken);
160
+ let development;
161
+ try {
162
+ if (!authToken) throw new Error("no Vercel CLI credentials file");
163
+ const response = await fetch(`https://api.vercel.com/v1/projects/${project.id}/token?source=board${project.team ? `&teamId=${project.team}` : ""}`, {
164
+ method: "POST",
165
+ headers: { authorization: `Bearer ${authToken}` }
166
+ });
167
+ 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);
168
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
169
+ development = (await response.json()).token;
170
+ } catch (error) {
171
+ if (error instanceof BoardError) throw error;
172
+ development = await tokenFromVercelCli(project);
173
+ }
174
+ const token = await exchangeVercelOidcToken({
175
+ token: development,
176
+ audience
177
+ });
178
+ const user = authToken ? await vercelUser(authToken) : null;
179
+ const fresh = {
180
+ token,
181
+ expiresAt: expiry(token),
182
+ handle: user?.handle ?? null,
183
+ name: user?.name ?? null,
184
+ avatar: user?.avatar ?? null
185
+ };
186
+ await writeCache(audience, fresh).catch(() => {});
187
+ return fresh;
188
+ }
189
+ /**
190
+ * The project to mint a person's token for: the one Vercel, asked with your own
191
+ * login, says the board's host is a deployment of (its deployment URL, a branch
192
+ * alias, or a domain), found as `vercel curl` finds it. Discovery names a project
193
+ * too, but any board can name any project you can access, and a token for one it
194
+ * isn't could be replayed where that project is trusted, so the two must agree.
195
+ * Vercel knows no loopback host: a board there runs on your machine (`next dev`).
196
+ */
197
+ async function boardProject(url, discovery, authToken) {
198
+ const { host, hostname } = new URL(url);
199
+ if (LOOPBACK.has(hostname)) return {
200
+ id: discovery.project_id,
201
+ team: discovery.team_id
202
+ };
203
+ const found = await findProject(authToken ? async (method, path) => {
204
+ const response = await fetch(`https://api.vercel.com${path}`, {
205
+ method,
206
+ headers: { authorization: `Bearer ${authToken}` }
207
+ });
208
+ return response.ok ? await response.json() : null;
209
+ } : vercelCliApi, host);
210
+ 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);
211
+ 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);
212
+ return {
213
+ id: found.projectId,
214
+ team: found.ownerId.startsWith("team_") ? found.ownerId : null
215
+ };
216
+ }
217
+ /** When credentials live in the keychain, the Vercel CLI mints the token itself. */
218
+ async function tokenFromVercelCli(project) {
219
+ const args = [
220
+ "project",
221
+ "token",
222
+ project.id,
223
+ "--format=json"
224
+ ];
225
+ if (project.team) args.push("--scope", project.team);
226
+ try {
227
+ const { stdout } = await promisify(execFile)("vercel", args, { timeout: 3e4 });
228
+ return JSON.parse(stdout).token;
229
+ } catch {
230
+ throw new BoardError("not_logged_in", "not signed in to Vercel: run `vercel login`", 0);
231
+ }
232
+ }
233
+ /** Your Vercel username, display name, and avatar (Vercel serves avatars publicly). */
234
+ async function vercelUser(authToken) {
235
+ try {
236
+ const response = await fetch("https://api.vercel.com/v2/user", { headers: { authorization: `Bearer ${authToken}` } });
237
+ if (!response.ok) return null;
238
+ const { user } = await response.json();
239
+ return {
240
+ handle: user?.username?.toLowerCase() ?? null,
241
+ name: user?.name?.trim() || null,
242
+ avatar: user?.avatar && /^[0-9a-f]+$/i.test(user.avatar) ? `https://vercel.com/api/www/avatar/${user.avatar}?s=128` : null
243
+ };
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+ async function githubActionsToken(audience) {
249
+ const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);
250
+ url.searchParams.set("audience", audience);
251
+ const response = await fetch(url, { headers: { authorization: `bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}` } });
252
+ 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);
253
+ return (await response.json()).value;
254
+ }
255
+ /** The board the environment points at: `BOARD_URL`, the project's link (`vb link`, `vb serve`), or `vb use`'s. */
256
+ function boardUrl() {
257
+ const url = (process.env.BOARD_URL ?? projectUrl() ?? savedUrl())?.replace(/\/+$/, "");
258
+ 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);
259
+ return url;
260
+ }
261
+ /**
262
+ * How a client reaches the board at `url` from Node: through Deployment Protection, with a token
263
+ * from `token`, `BOARD_TOKEN`, a local board's token file (`vb serve`), GitHub Actions, the Vercel
264
+ * runtime, or your Vercel CLI login, in that order. `handle` is yours, in place of `BOARD_HANDLE`
265
+ * or your Vercel username.
266
+ */
267
+ function connect(url, options = {}) {
268
+ url = url.replace(/\/+$/, "");
269
+ const doFetch = guarded(url, options.fetch ?? ((input, init) => globalThis.fetch(input, init)), options.token);
270
+ let discovery;
271
+ const discover = () => discovery ??= doFetch(url).then(async (response) => {
272
+ if (!response.ok) throw new BoardError("no_board", `no board at ${url} (${response.status})`, response.status);
273
+ const found = await response.json();
274
+ if (found.audience !== url) throw new BoardError("wrong_audience", `the board at ${url} says its URL is ${found.audience}: use that URL`, 0);
275
+ return found;
276
+ });
277
+ let cached = null;
278
+ let source = null;
279
+ let person = null;
280
+ const mint = async () => {
281
+ if (options.token !== void 0) {
282
+ const token = typeof options.token === "function" ? await options.token() : options.token;
283
+ return {
284
+ token,
285
+ expiresAt: expiry(token)
286
+ };
287
+ }
288
+ if (process.env.BOARD_TOKEN) {
289
+ const token = process.env.BOARD_TOKEN;
290
+ source = "BOARD_TOKEN";
291
+ return {
292
+ token,
293
+ expiresAt: expiry(token)
294
+ };
295
+ }
296
+ const local = localToken(url);
297
+ if (local) {
298
+ source = "vb serve";
299
+ return {
300
+ token: local.token,
301
+ expiresAt: expiry(local.token),
302
+ handle: local.handle,
303
+ name: null,
304
+ avatar: null
305
+ };
306
+ }
307
+ const discovered = await discover();
308
+ if (process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN) {
309
+ const token = await githubActionsToken(url);
310
+ source = "GitHub Actions OIDC";
311
+ return {
312
+ token,
313
+ expiresAt: expiry(token)
314
+ };
315
+ }
316
+ if (process.env.VERCEL === "1") {
317
+ const token = await hostedToken(url);
318
+ source = "Vercel OIDC";
319
+ return {
320
+ token,
321
+ expiresAt: expiry(token)
322
+ };
323
+ }
324
+ person ??= personToken(url, discovered).finally(() => {
325
+ person = null;
326
+ });
327
+ const minted = await person;
328
+ source = "vercel login";
329
+ return minted;
330
+ };
331
+ const token = async () => {
332
+ if (options.token !== void 0 || !cached || cached.expiresAt - 6e4 < Date.now()) cached = await mint();
333
+ return cached.token;
334
+ };
335
+ const send = (async (input, init = {}) => {
336
+ const href = input instanceof Request ? input.url : String(input);
337
+ const method = (init.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
338
+ const path = (href.startsWith(url) ? href.slice(url.length) : new URL(href).pathname).split("?")[0] || "/";
339
+ const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : void 0));
340
+ if (!(method === "GET" && path === "/")) {
341
+ if (!headers.has("authorization")) headers.set("authorization", `Bearer ${await token()}`);
342
+ const handle = options.handle ?? (process.env.BOARD_HANDLE || (options.token === void 0 ? cached?.handle : null));
343
+ if (handle && !headers.has("x-board-handle")) headers.set("x-board-handle", handle);
344
+ if (!headers.has("x-board-profile") && options.token === void 0) {
345
+ if (cached?.name && !headers.has("x-board-name")) headers.set("x-board-name", encodeURIComponent(cached.name));
346
+ if (cached?.avatar && !headers.has("x-board-avatar")) headers.set("x-board-avatar", cached.avatar);
347
+ }
348
+ }
349
+ return doFetch(input, {
350
+ ...init,
351
+ headers
352
+ });
353
+ });
354
+ return {
355
+ fetch: send,
356
+ source: () => source
357
+ };
358
+ }
359
+ /** The profile the environment picks: `BOARD_AS`, or the coding agent it runs in, with its session. */
360
+ function environmentProfile() {
361
+ const detected = detectAgent();
362
+ const name = process.env.BOARD_AS || detected?.profile;
363
+ const avatar = process.env.BOARD_AVATAR;
364
+ return {
365
+ profile: name ? avatar ? {
366
+ name,
367
+ avatar
368
+ } : name : null,
369
+ session: detected?.session ?? null
370
+ };
371
+ }
372
+ const WHERE = "Project → Settings → Deployment Protection → Protection Bypass for Automation";
373
+ var Wall = class extends Error {
374
+ status;
375
+ constructor(status) {
376
+ super("protected");
377
+ this.status = status;
378
+ }
379
+ };
380
+ /**
381
+ * This deployment's Vercel OIDC token, with `url` as its audience: what a hosted client sends a
382
+ * board, for Deployment Protection's Trusted Sources. Its `cause` says why it couldn't.
383
+ */
384
+ async function hostedToken(url) {
385
+ try {
386
+ return await getVercelOidcToken({ audience: url });
387
+ } catch (cause) {
388
+ 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 });
389
+ }
390
+ }
391
+ /** A host this deployment answers on, as Vercel's system variables name them (bare hosts). */
392
+ const ownHost = (host) => [
393
+ process.env.VERCEL_URL,
394
+ process.env.VERCEL_BRANCH_URL,
395
+ process.env.VERCEL_PROJECT_PRODUCTION_URL
396
+ ].some((own) => !!own && own.toLowerCase() === host.toLowerCase());
397
+ /** Explicit bypass wins; otherwise hosted calls use OIDC and local people use their saved bypass. */
398
+ function guarded(url, doFetch, suppliedToken) {
399
+ const host = new URL(url).host;
400
+ let secret;
401
+ let looked;
402
+ let envRejected = false;
403
+ return async (input, init = {}) => {
404
+ const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : void 0));
405
+ init = {
406
+ ...init,
407
+ headers
408
+ };
409
+ const fromEnv = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
410
+ if (fromEnv && !envRejected && (process.env.VERCEL !== "1" || ownHost(host))) try {
411
+ return await through(doFetch, input, init, fromEnv);
412
+ } catch (error) {
413
+ if (!(error instanceof Wall)) throw error;
414
+ 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);
415
+ envRejected = true;
416
+ }
417
+ if (process.env.VERCEL === "1") {
418
+ if (!headers.has("x-vercel-trusted-oidc-idp-token")) {
419
+ const token = headers.get("authorization")?.match(/^Bearer (.+)$/i)?.[1] ?? (typeof suppliedToken === "function" ? await suppliedToken() : suppliedToken) ?? (process.env.BOARD_TOKEN || await hostedToken(url));
420
+ headers.set("x-vercel-trusted-oidc-idp-token", token);
421
+ }
422
+ return through(doFetch, input, init, null).catch((error) => {
423
+ 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;
424
+ });
425
+ }
426
+ const used = await (secret ??= readBypass(host));
427
+ try {
428
+ return await through(doFetch, input, init, used);
429
+ } catch (error) {
430
+ if (!(error instanceof Wall)) throw error;
431
+ }
432
+ looked ??= projectBypass(url, host, doFetch).then(async (found) => {
433
+ await writeBypass(host, found.secret).catch(() => {});
434
+ return found;
435
+ });
436
+ const { secret: found, created } = await looked;
437
+ secret = Promise.resolve(found);
438
+ try {
439
+ return await through(doFetch, input, init, found).catch(async (error) => {
440
+ if (!(error instanceof Wall) || !created) throw error;
441
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
442
+ return through(doFetch, input, init, found);
443
+ });
444
+ } catch (error) {
445
+ 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;
446
+ }
447
+ };
448
+ }
449
+ /**
450
+ * One request that recognizes the wall: a redirect to `/sso-api`, or an HTML
451
+ * page where the board answers JSON (on SAML teams, the redirects end at the
452
+ * IdP's login page with a 200). Only a 2xx, 401, or 403 HTML page counts, so a
453
+ * 404 page keeps its own error; attachments, which may be HTML, come with a
454
+ * `content-disposition`. Other redirects of a GET or HEAD are followed as fetch
455
+ * would, dropping the token and bypass secret on the way to another origin; a
456
+ * redirected POST comes back as its 3xx.
457
+ */
458
+ async function through(doFetch, input, init, bypass) {
459
+ let target = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
460
+ const method = (init.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
461
+ let headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : void 0));
462
+ if (bypass) headers.set("x-vercel-protection-bypass", bypass);
463
+ for (let hops = 0;; hops++) {
464
+ const response = await doFetch(hops === 0 ? input : target, {
465
+ ...init,
466
+ headers,
467
+ redirect: "manual"
468
+ });
469
+ const location = response.status >= 300 && response.status < 400 ? response.headers.get("location") : null;
470
+ if (location) {
471
+ const next = new URL(location, target);
472
+ if (next.pathname.startsWith("/sso-api")) throw new Wall(response.status);
473
+ if (method === "GET" || method === "HEAD") {
474
+ if (hops >= 20) throw new BoardError("too_many_redirects", `too many redirects from ${target}`, response.status);
475
+ if (next.origin !== new URL(target).origin) {
476
+ headers = new Headers(headers);
477
+ headers.delete("authorization");
478
+ headers.delete("x-vercel-trusted-oidc-idp-token");
479
+ headers.delete("x-vercel-protection-bypass");
480
+ }
481
+ target = next.href;
482
+ continue;
483
+ }
484
+ }
485
+ const { status } = response;
486
+ if (/^text\/html\b/i.test(response.headers.get("content-type") ?? "") && !response.headers.has("content-disposition") && (status >= 200 && status < 300 || status === 401 || status === 403)) throw new Wall(status);
487
+ return response;
488
+ }
489
+ }
490
+ const bypassFile = (host) => join(configDir(), "bypass", `${createHash("sha256").update(host).digest("hex").slice(0, 32)}.json`);
491
+ async function readBypass(host) {
492
+ try {
493
+ return JSON.parse(await readFile(bypassFile(host), "utf8")).secret || null;
494
+ } catch {
495
+ return null;
496
+ }
497
+ }
498
+ async function writeBypass(host, secret) {
499
+ await mkdir(join(configDir(), "bypass"), {
500
+ recursive: true,
501
+ mode: 448
502
+ });
503
+ await writeFile(bypassFile(host), JSON.stringify({
504
+ host,
505
+ secret
506
+ }), { mode: 384 });
507
+ }
508
+ const automationBypass = (bypasses) => Object.entries(bypasses ?? {}).find(([, value]) => value.scope === "automation-bypass")?.[0] ?? null;
509
+ /**
510
+ * The board's project's automation bypass secret, as `vercel curl` finds it:
511
+ * the deployment (or alias) behind the host gives the project, whose
512
+ * `protectionBypass` has it. When there's none, it's created.
513
+ */
514
+ async function projectBypass(url, host, doFetch) {
515
+ 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);
516
+ let api;
517
+ try {
518
+ const token = await getVercelToken();
519
+ api = async (method, path) => {
520
+ const response = await doFetch(`https://api.vercel.com${path}`, {
521
+ method,
522
+ headers: {
523
+ authorization: `Bearer ${token}`,
524
+ ...method === "PATCH" ? { "content-type": "application/json" } : {}
525
+ },
526
+ ...method === "PATCH" ? { body: "{}" } : {}
527
+ });
528
+ return response.ok ? await response.json() : null;
529
+ };
530
+ } catch {
531
+ api = vercelCliApi;
532
+ }
533
+ const project = await findProject(api, host);
534
+ if (!project) throw blocked("can't see its project: run `vercel login` or ask for access");
535
+ const scope = project.ownerId.startsWith("team_") ? `?teamId=${project.ownerId}` : "";
536
+ const details = await api("GET", `/v9/projects/${project.projectId}${scope}`);
537
+ if (!details) throw blocked("can't read its project's settings: ask for access");
538
+ const existing = automationBypass(details.protectionBypass);
539
+ if (existing) return {
540
+ secret: existing,
541
+ created: false
542
+ };
543
+ if (!details.ssoProtection && !details.passwordProtection) throw new BoardError("no_board", `no board at ${url}: it answered with a web page, and its Vercel project isn't protected`, 0);
544
+ const created = await api("PATCH", `/v1/projects/${project.projectId}/protection-bypass${scope}`);
545
+ const secret = automationBypass(created?.protectionBypass);
546
+ if (!secret) throw blocked("can't read or create its project's automation bypass: ask a project admin");
547
+ process.stderr.write(`created a Protection Bypass for Automation on the Vercel project ${details?.name ?? project.projectId}\n`);
548
+ return {
549
+ secret,
550
+ created: true
551
+ };
552
+ }
553
+ /** The project behind a host, as `vercel curl` finds it: the deployment, then the alias, in your own scope and then each of your teams. */
554
+ async function findProject(api, host) {
555
+ const lookup = async (kind, team) => {
556
+ const found = await api("GET", `${kind === "deployment" ? `/v13/deployments/${encodeURIComponent(host)}` : `/now/aliases/${encodeURIComponent(host)}`}${team ? `?teamId=${team}` : ""}`).catch(() => null);
557
+ const owner = found?.ownerId ?? found?.team?.id ?? team;
558
+ return found?.projectId && owner ? {
559
+ projectId: found.projectId,
560
+ ownerId: owner
561
+ } : null;
562
+ };
563
+ const own = await lookup("deployment", null) ?? await lookup("alias", null);
564
+ if (own) return own;
565
+ const { teams } = await api("GET", "/v2/teams?limit=100").catch(() => null) ?? {};
566
+ const ids = (teams ?? []).filter((team) => !team.limited).map((team) => team.id);
567
+ for (const kind of ["deployment", "alias"]) for (let i = 0; i < ids.length; i += 8) {
568
+ const found = (await Promise.all(ids.slice(i, i + 8).map((id) => lookup(kind, id)))).find(Boolean);
569
+ if (found) return found;
570
+ }
571
+ return null;
572
+ }
573
+ /** When credentials live in the keychain, the Vercel CLI makes the API call itself. */
574
+ let vercelCli;
575
+ const vercelCliApi = async (method, path) => {
576
+ vercelCli ??= promisify(execFile)("vercel", ["whoami", "--non-interactive"], { timeout: 1e4 }).then(() => true, () => false);
577
+ if (!await vercelCli) throw new BoardError("not_logged_in", "not signed in to Vercel: run `vercel login`", 0);
578
+ const args = [
579
+ "api",
580
+ path,
581
+ "-X",
582
+ method,
583
+ "--raw",
584
+ "--non-interactive"
585
+ ];
586
+ if (method === "PATCH") args.push("--input", "-");
587
+ try {
588
+ const run = promisify(execFile)("vercel", args, { timeout: 3e4 });
589
+ run.child.stdin?.on("error", () => {});
590
+ run.child.stdin?.end(method === "PATCH" ? "{}" : "");
591
+ return JSON.parse((await run).stdout);
592
+ } catch (error) {
593
+ if (error.code === "ENOENT") throw new BoardError("not_logged_in", "not signed in to Vercel: run `vercel login`", 0);
594
+ return null;
595
+ }
596
+ };
597
+ //#endregion
598
+ //#region src/backend-http/index.ts
599
+ const environment = {
600
+ url: boardUrl,
601
+ fetch: (url) => connect(url).fetch,
602
+ profile: environmentProfile
603
+ };
604
+ /**
605
+ * The board at `url`, which keeps the log: this board uses it as this deployment. Without a
606
+ * `url`, the one the environment points at: `BOARD_URL`, the repo's link, or `vb use`'s.
607
+ * Without a `fetch`, it sends the credentials `vb` would.
608
+ */
609
+ function http(url, options = {}) {
610
+ return createHttp(url, options, environment);
611
+ }
612
+ //#endregion
613
+ export { detectAgent as a, projectUrl as c, connect as i, boardUrl as n, localTokenFile as o, configDir as r, projectDir as s, http as t };