@vercel/board 0.25.0 → 0.26.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 (84) hide show
  1. package/dist/{a2-CQ3WcqMv.mjs → a2-Qzj0kCWU.mjs} +6 -6
  2. package/dist/access.d.mts +1 -2
  3. package/dist/access.mjs +3 -3
  4. package/dist/{actors-CDDhhFNY.mjs → actors-BLXVp4oZ.mjs} +2 -2
  5. package/dist/{auth-BwWH3Twu.mjs → auth-lTkOgvcE.mjs} +1 -1
  6. package/dist/{backend-DmXjVnil.d.mts → backend-ZKYTe3xa.d.mts} +5 -15
  7. package/dist/backend-a2/blob-vercel.d.mts +1 -1
  8. package/dist/backend-a2/contract.d.mts +1 -2
  9. package/dist/backend-a2/store-memory.d.mts +1 -1
  10. package/dist/backend-a2/store-postgres.d.mts +1 -1
  11. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  12. package/dist/backend-a2/store-redis.d.mts +1 -1
  13. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  14. package/dist/backend-a2.d.mts +5 -6
  15. package/dist/backend-a2.mjs +1 -1
  16. package/dist/{backend-http-BJ7AdK0c.mjs → backend-http-BB0Gld6Q.mjs} +8 -5
  17. package/dist/{backend-http-BsP0Xq9e.d.mts → backend-http-E7YU0qhn.d.mts} +3 -4
  18. package/dist/{backend-http-Cs-UKpYp.mjs → backend-http-F0W6e36G.mjs} +2 -2
  19. package/dist/backend-http.d.mts +1 -1
  20. package/dist/backend-http.mjs +1 -1
  21. package/dist/backend-http.node.d.mts +1 -1
  22. package/dist/backend-http.node.mjs +1 -1
  23. package/dist/{backend-vercel-CED673bx.d.mts → backend-vercel-CIRObA0t.d.mts} +3 -4
  24. package/dist/{backend-vercel-CG2SYa03.mjs → backend-vercel-CtB46vMQ.mjs} +3 -3
  25. package/dist/{backend-vercel-B0Q0K0O5.mjs → backend-vercel-DUWIcFi9.mjs} +24 -8
  26. package/dist/backend-vercel.d.mts +1 -1
  27. package/dist/backend-vercel.mjs +1 -1
  28. package/dist/backend-vercel.node.d.mts +6 -1
  29. package/dist/backend-vercel.node.mjs +2 -2
  30. package/dist/{blob-C0J3wbGv.d.mts → blob-B-9AvYis.d.mts} +2 -1
  31. package/dist/{chat-provider-6eYkL2ef.d.mts → chat-provider-DsNEBQiA.d.mts} +3 -2
  32. package/dist/cli-commands.d.mts +1 -1
  33. package/dist/cli-commands.mjs +1 -1
  34. package/dist/cli-embedded.d.mts +7 -3
  35. package/dist/cli-embedded.mjs +8 -4
  36. package/dist/cli.mjs +4 -4
  37. package/dist/{client-BY-BCggB.mjs → client-D0Px7KzF.mjs} +16 -4
  38. package/dist/{provider-C_sj4-GL.d.mts → client-xLxeDz0f.d.mts} +13 -689
  39. package/dist/{commands-D0wXq6CY.d.mts → commands-D6gvrOh3.d.mts} +4 -4
  40. package/dist/{commands-BtL4jFq8.mjs → commands-nEG47RM_.mjs} +7 -7
  41. package/dist/{conformance-BZ53bxrG.mjs → conformance-DlnfuzEB.mjs} +1 -1
  42. package/dist/conformance.d.mts +1 -1
  43. package/dist/conformance.mjs +1 -1
  44. package/dist/{engine-SeaJ_CIY.mjs → engine-ROa7-IQY.mjs} +2 -2
  45. package/dist/errors-CB4tnZGz.d.mts +16 -0
  46. package/dist/{errors-Bven0k29.mjs → errors-CusN4x2l.mjs} +14 -6
  47. package/dist/exports-D22M68rk.d.mts +5 -0
  48. package/dist/{exports-ByvYtxFT.mjs → exports-NFjBgIT2.mjs} +6 -6
  49. package/dist/{folds-CRYY14kc.d.mts → folds-jS9wlh9_.d.mts} +49 -2
  50. package/dist/{index-k2LfZxl-.d.mts → index-C05it0mC.d.mts} +2 -2
  51. package/dist/index.d.mts +5 -4
  52. package/dist/{index.default-DmfhBZZi.mjs → index.default-Boe7x-mK.mjs} +1 -1
  53. package/dist/index.mjs +6 -6
  54. package/dist/index.node.d.mts +5 -4
  55. package/dist/index.node.mjs +4 -4
  56. package/dist/inspect/app.js +108 -108
  57. package/dist/{main-BNkywL2P.mjs → main-BuahKR16.mjs} +84 -69
  58. package/dist/{middleware-B0J7QBq7.mjs → middleware-ChKM1zuu.mjs} +1 -1
  59. package/dist/{node-C51_xzCb.mjs → node-Ybi9E6bO.mjs} +104 -46
  60. package/dist/provider-Cdg1qadh.d.mts +688 -0
  61. package/dist/{serve-DYQ64Q5R.mjs → serve-BbHEQk_Y.mjs} +5 -5
  62. package/dist/{server-BgK1sgj7.mjs → server-Bphio_82.mjs} +4 -4
  63. package/dist/{serving-rXQcEgTO.mjs → serving-CvOWEym2.mjs} +1 -1
  64. package/dist/sync-github.d.mts +2 -2
  65. package/dist/sync-kapso.d.mts +2 -2
  66. package/dist/sync-linear.d.mts +2 -2
  67. package/dist/sync-slack.d.mts +3 -3
  68. package/dist/sync.chat.d.mts +1 -1
  69. package/dist/sync.d.mts +2 -2
  70. package/dist/{tokens-U-XRlEfq.mjs → tokens-BuCU-61R.mjs} +2 -2
  71. package/dist/{types-8yq2VnKI.d.mts → types-aCAf6lGU.d.mts} +1 -1
  72. package/dist/{watchers-hash-C4CJg7K1.mjs → watchers-hash-BfDPW95j.mjs} +5 -5
  73. package/docs/01-board/04-messages.md +1 -1
  74. package/docs/01-board/05-reading.md +2 -2
  75. package/docs/01-board/06-watchers.md +3 -3
  76. package/docs/02-sync/01-overview.md +2 -2
  77. package/docs/03-guides/03-ci.md +2 -2
  78. package/docs/03-guides/04-triage.md +5 -5
  79. package/docs/04-reference/01-cli.md +9 -4
  80. package/docs/04-reference/02-board.md +6 -2
  81. package/docs/index.md +2 -2
  82. package/package.json +1 -1
  83. package/dist/exports-CPrIkpz3.d.mts +0 -4
  84. package/dist/schemas-CWxs3ECJ.d.mts +0 -50
@@ -1,14 +1,14 @@
1
1
  import { i as fileSize, n as DIRECT_UPLOAD_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
2
  import { a as AttachmentId } from "./schemas-Be08nyfW.mjs";
3
- import { d as VERSION, i as withMentions } from "./watchers-hash-C4CJg7K1.mjs";
3
+ import { d as VERSION, i as withMentions } from "./watchers-hash-BfDPW95j.mjs";
4
4
  import { n as displayEmoji, r as normalizeEmoji } from "./emoji-CTLpvH-A.mjs";
5
- import { t as BoardError } from "./client-BY-BCggB.mjs";
6
- import { i as groups, n as filters$1, o as options, r as globalOptions, s as usage$1, t as commands } from "./commands-BtL4jFq8.mjs";
7
- import { n as connect, s as vercelProjectFile, t as boardUrl } from "./node-C51_xzCb.mjs";
8
- import { t as http } from "./backend-http-Cs-UKpYp.mjs";
9
- import { t as board } from "./server-BgK1sgj7.mjs";
10
- import { t as conformance } from "./conformance-BZ53bxrG.mjs";
11
- import { n as vercel, t as credentialSource } from "./backend-vercel-B0Q0K0O5.mjs";
5
+ import { t as BoardError } from "./client-D0Px7KzF.mjs";
6
+ import { i as groups, n as filters$1, o as options, r as globalOptions, s as usage$1, t as commands } from "./commands-nEG47RM_.mjs";
7
+ import { i as handLogin, l as vercelProjectFile, n as connect, t as boardUrl } from "./node-Ybi9E6bO.mjs";
8
+ import { t as http } from "./backend-http-F0W6e36G.mjs";
9
+ import { t as board } from "./server-Bphio_82.mjs";
10
+ import { t as conformance } from "./conformance-DlnfuzEB.mjs";
11
+ import { n as handTeam, r as vercel, t as credentialSource } from "./backend-vercel-DUWIcFi9.mjs";
12
12
  import { t as displayText } from "./bots-CGkO76qH.mjs";
13
13
  import { spawn } from "node:child_process";
14
14
  import { readFile, stat, writeFile } from "node:fs/promises";
@@ -916,7 +916,7 @@ function filters(channel) {
916
916
  ...flags.by ? { by: flags.by.replace(/^@/, "") } : {},
917
917
  ...flags.mentions ? { mentions: flags.mentions.replace(/^@/, "") } : {},
918
918
  ...flags.label?.length ? { label: flags.label } : {},
919
- ...flags.from ? { from: flags.from } : {},
919
+ ...flags.source ? { from: flags.source } : {},
920
920
  ...flags.text ? { text: flags.text } : {},
921
921
  ...flags.after !== void 0 ? { after: flags.after.replace(/^#/, "") } : {}
922
922
  };
@@ -1011,14 +1011,15 @@ function missing() {
1011
1011
  if (command === "download") return `attachment ${a}`;
1012
1012
  }
1013
1013
  /**
1014
- * What `error` means for this run, in the Vercel CLI's terms. The handle's own errors (`vercel
1015
- * login` missing, Deployment Protection) say what and why themselves; the board's are said by what
1016
- * this run was doing, with the board's own words under it.
1014
+ * What `error` means for this run, in the Vercel CLI's terms. What the handle raised itself says
1015
+ * what and why, and its hint what fixes it. What a board answered is said by what this run was
1016
+ * doing, with the board's words under it and in `data`, never in what to run: a board can answer
1017
+ * with any code, the handle's own included.
1017
1018
  */
1018
1019
  function failureOf(error) {
1019
1020
  if (error instanceof Failed) return error.failure;
1020
1021
  const message = error instanceof Error ? error.message : String(error);
1021
- const { code, status, hint } = error;
1022
+ const { code, status, hint, origin } = error;
1022
1023
  if (typeof code !== "string" || typeof status !== "number") return {
1023
1024
  class: "unknown",
1024
1025
  reason: "unexpected_error",
@@ -1028,61 +1029,65 @@ function failureOf(error) {
1028
1029
  const said = { error: {
1029
1030
  code,
1030
1031
  message,
1031
- status
1032
+ status,
1033
+ ...typeof hint === "string" ? { hint } : {}
1032
1034
  } };
1033
- const fix = typeof hint === "string" ? [{ instruction: hint }] : [];
1034
- const own = (kind, reason, next = fix) => ({
1035
- class: kind,
1036
- reason,
1037
- message,
1038
- ...next.length ? { next } : {},
1039
- data: said
1040
- });
1041
- switch (code) {
1042
- case "not_logged_in": return {
1043
- class: "auth",
1044
- reason: "login_required",
1045
- message: "You're not logged in to Vercel.",
1046
- next: [{
1047
- instruction: "Log in",
1048
- argv: ["vercel", "login"]
1049
- }],
1035
+ if (origin === "handle") {
1036
+ const fix = typeof hint === "string" ? [{ instruction: hint }] : [];
1037
+ const own = (kind, reason, next = fix) => ({
1038
+ class: kind,
1039
+ reason,
1040
+ message,
1041
+ ...next.length ? { next } : {},
1050
1042
  data: said
1051
- };
1052
- case "no_access": return own("permission", "permission_denied");
1053
- case "wrong_project": return own("permission", "wrong_project");
1054
- case "protected": return own("permission", "deployment_protected");
1055
- case "no_project": return own("platform", "no_project");
1056
- case "no_token": return own("auth", "token_unavailable");
1057
- case "no_board": return own("not_found", "board_not_found");
1058
- case "too_many_redirects": return own("network", "network_error");
1059
- case "invalid_duration": return own("usage", "invalid_arguments", [helpRecovery()]);
1060
- case "wrong_audience": return own("usage", "wrong_url");
1061
- case "ambiguous_agent": return own("usage", "ambiguous_agent", [{
1062
- instruction: "Write as one of them",
1063
- argv: rerun("--as", "<PROFILE>")
1064
- }, {
1065
- instruction: "Or as yourself",
1066
- argv: rerun("--human")
1067
- }]);
1068
- case "too_large": return {
1069
- ...own("limit", "payload_too_large"),
1070
- message: `Can't ${doing()}. ${sentence(message)}`
1071
- };
1072
- case "upload_failed": return {
1073
- ...own("platform", "upload_failed"),
1074
- message: `Failed to ${doing()}.`,
1075
- details: [message]
1076
- };
1077
- case "port_taken": return own("conflict", "port_taken", [{
1078
- instruction: "Pick another port",
1079
- argv: [
1080
- ...cli(),
1081
- current.command ?? "serve",
1082
- "--port",
1083
- "<PORT>"
1084
- ]
1085
- }]);
1043
+ });
1044
+ switch (code) {
1045
+ case "not_logged_in": return {
1046
+ class: "auth",
1047
+ reason: "login_required",
1048
+ message: "You're not logged in to Vercel.",
1049
+ next: [{
1050
+ instruction: "Log in",
1051
+ argv: ["vercel", "login"]
1052
+ }],
1053
+ data: said
1054
+ };
1055
+ case "no_access": return own("permission", "permission_denied");
1056
+ case "wrong_project": return own("permission", "wrong_project");
1057
+ case "protected": return own("permission", "deployment_protected");
1058
+ case "no_project": return own("platform", "no_project");
1059
+ case "no_token": return own("auth", "token_unavailable");
1060
+ case "no_board": return own("not_found", "board_not_found");
1061
+ case "too_many_redirects": return own("network", "network_error");
1062
+ case "invalid_duration": return own("usage", "invalid_arguments", [helpRecovery()]);
1063
+ case "wrong_audience":
1064
+ case "redirected": return own("usage", "wrong_url");
1065
+ case "ambiguous_agent": return own("usage", "ambiguous_agent", [{
1066
+ instruction: "Write as one of them",
1067
+ argv: rerun("--as", "<PROFILE>")
1068
+ }, {
1069
+ instruction: "Or as yourself",
1070
+ argv: rerun("--human")
1071
+ }]);
1072
+ case "too_large": return {
1073
+ ...own("limit", "payload_too_large"),
1074
+ message: `Can't ${doing()}. ${sentence(message)}`
1075
+ };
1076
+ case "upload_failed": return {
1077
+ ...own("platform", "upload_failed"),
1078
+ message: `Failed to ${doing()}.`,
1079
+ details: [message]
1080
+ };
1081
+ case "port_taken": return own("conflict", "port_taken", [{
1082
+ instruction: "Pick another port",
1083
+ argv: [
1084
+ ...cli(),
1085
+ current.command ?? "serve",
1086
+ "--port",
1087
+ "<PORT>"
1088
+ ]
1089
+ }]);
1090
+ }
1086
1091
  }
1087
1092
  const theirs = (kind, reason, headline, next = []) => ({
1088
1093
  class: kind,
@@ -1459,7 +1464,7 @@ async function run(command, args) {
1459
1464
  };
1460
1465
  const old = outdated("This board can't list threads.");
1461
1466
  const page = await board.threads(query).catch((error) => {
1462
- throw error instanceof BoardError && error.status === 404 ? old : error;
1467
+ throw error instanceof BoardError && error.status === 404 && /^no route: /.test(error.message) ? old : error;
1463
1468
  });
1464
1469
  if (!Array.isArray(page.threads)) throw old;
1465
1470
  if (!page.threads.length && target) {
@@ -1504,7 +1509,7 @@ async function run(command, args) {
1504
1509
  text: content,
1505
1510
  ...files.length ? { attachments: files } : {},
1506
1511
  ...flags.label?.length ? { labels: labelPairs(flags.label) } : {},
1507
- ...flags.id ? { id: flags.id } : {},
1512
+ ...flags["idempotency-key"] ? { id: flags["idempotency-key"] } : {},
1508
1513
  ...flags["no-sync"] ? { noSync: true } : {}
1509
1514
  };
1510
1515
  const message = re === void 0 ? await board.post(target, {
@@ -1523,7 +1528,7 @@ async function run(command, args) {
1523
1528
  const message = await board.reply(id, {
1524
1529
  text: content,
1525
1530
  ...files.length ? { attachments: files } : {},
1526
- ...flags.id ? { id: flags.id } : {},
1531
+ ...flags["idempotency-key"] ? { id: flags["idempotency-key"] } : {},
1527
1532
  ...flags["no-sync"] ? { noSync: true } : {}
1528
1533
  });
1529
1534
  warnUnknownMentions(message.unknownMentions);
@@ -1825,6 +1830,16 @@ const context = () => ({
1825
1830
  * stopped.
1826
1831
  */
1827
1832
  async function main(argv, options) {
1833
+ handLogin(options.login ?? null);
1834
+ handTeam(options.team ?? null);
1835
+ try {
1836
+ return await runOnce(argv, options);
1837
+ } finally {
1838
+ handLogin(null);
1839
+ handTeam(null);
1840
+ }
1841
+ }
1842
+ async function runOnce(argv, options) {
1828
1843
  app = options;
1829
1844
  typed = argv;
1830
1845
  flags = {};
@@ -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-Bven0k29.mjs";
2
+ import { t as HttpError } from "./errors-CusN4x2l.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,7 +1,7 @@
1
- import { t as BoardError } from "./client-BY-BCggB.mjs";
1
+ import { t as BoardError } from "./client-D0Px7KzF.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
- import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
5
5
  import { existsSync, readFileSync, statSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { dirname, join, resolve } from "node:path";
@@ -52,6 +52,13 @@ const LOOPBACK = /* @__PURE__ */ new Set([
52
52
  "localhost",
53
53
  "[::1]"
54
54
  ]);
55
+ const PROJECT_ID = /^prj_[A-Za-z0-9]{1,64}$/;
56
+ const URL_SHAPE = /^https?:\/\/[A-Za-z0-9.-]{1,253}(:\d{1,5})?(\/[A-Za-z0-9._~%/-]{0,2000})?$/;
57
+ /**
58
+ * A value a board said, as a message can show it: only when it's what it should be (a project
59
+ * ID, a URL), since the handle's messages are trusted where a board's words aren't.
60
+ */
61
+ const shown = (value, shape) => typeof value === "string" && shape.test(value) ? value : void 0;
55
62
  /** Where `vb serve` leaves the token for the board it runs at `url`: only this user can read it. */
56
63
  function localTokenFile(url) {
57
64
  const normalized = url.replace(/\/+$/, "");
@@ -111,54 +118,89 @@ const expiry = (token) => {
111
118
  }
112
119
  };
113
120
  /**
114
- * Where `audience`'s token is cached: one per board, and with a login the Vercel CLI hands over
115
- * (`VERCEL_AUTH_TOKEN`), one per login too, since it can be someone else's than `vercel login`'s.
121
+ * Where `audience`'s token is cached: one per board, and with a login a CLI hands over, one per
122
+ * login too, since it can be someone else's than `vercel login`'s.
116
123
  */
117
- const cacheFile = (audience) => {
118
- const login = process.env.VERCEL_AUTH_TOKEN;
124
+ const cacheFile = (audience, login) => {
119
125
  const key = login ? `${audience}\n${login}` : audience;
120
126
  return join(configDir(), "tokens", `${createHash("sha256").update(key).digest("hex").slice(0, 32)}.json`);
121
127
  };
122
- async function readCache(audience) {
128
+ async function readCache(audience, login) {
123
129
  try {
124
- const cached = JSON.parse(await readFile(cacheFile(audience), "utf8"));
130
+ const cached = JSON.parse(await readFile(cacheFile(audience, login), "utf8"));
125
131
  return cached.expiresAt - 3e5 > Date.now() ? cached : null;
126
132
  } catch {
127
133
  return null;
128
134
  }
129
135
  }
130
- async function writeCache(audience, cached) {
131
- await mkdir(join(configDir(), "tokens"), {
136
+ async function writeCache(audience, login, cached) {
137
+ const tokens = join(configDir(), "tokens");
138
+ await mkdir(tokens, {
132
139
  recursive: true,
133
140
  mode: 448
134
141
  });
135
- await writeFile(cacheFile(audience), JSON.stringify(cached), { mode: 384 });
142
+ await writeFile(cacheFile(audience, login), JSON.stringify(cached), { mode: 384 });
143
+ const dead = Date.now() - 864e5;
144
+ for (const name of await readdir(tokens).catch(() => [])) {
145
+ const file = join(tokens, name);
146
+ const written = (await stat(file).catch(() => null))?.mtimeMs;
147
+ if (written !== void 0 && written < dead) await unlink(file).catch(() => {});
148
+ }
149
+ }
150
+ /** The login a CLI that runs board's hands it to ask for (`vercel board`'s), while one does. */
151
+ let handed = null;
152
+ /**
153
+ * Hands board a login to ask for when a request needs one, as `vercel board` does with its own:
154
+ * nothing asks for it before then, so help, a typo, or misuse never refreshes or prompts. Null
155
+ * takes it back.
156
+ */
157
+ function handLogin(login) {
158
+ handed = login;
136
159
  }
160
+ /** Whether the login is one a CLI handed over (`createApp`'s `login`, or `VERCEL_AUTH_TOKEN`). */
161
+ const loginHanded = () => handed !== null || !!process.env.VERCEL_AUTH_TOKEN;
137
162
  /**
138
- * Your Vercel login: what the Vercel CLI hands a CLI it runs, as `vercel board` (`VERCEL_AUTH_TOKEN`,
139
- * from its `--token` or its own login), or else what `vercel login` saved.
163
+ * Your Vercel login: what the CLI that runs board's hands it, as `vercel board` does (its `login`,
164
+ * or `VERCEL_AUTH_TOKEN` from its `--token`), or else what `vercel login` saved.
140
165
  */
141
- const vercelLogin = () => process.env.VERCEL_AUTH_TOKEN ? Promise.resolve(process.env.VERCEL_AUTH_TOKEN) : getVercelToken();
166
+ const vercelLogin = () => handed ? handed() : process.env.VERCEL_AUTH_TOKEN ? Promise.resolve(process.env.VERCEL_AUTH_TOKEN) : getVercelToken();
167
+ /**
168
+ * A handed login's token, which its board tokens are cached by: null for `vercel login`'s own. One
169
+ * that fails fails the request: `vercel login`'s, cached or in the `vercel` on PATH, can be someone
170
+ * else's.
171
+ */
172
+ async function handedToken() {
173
+ if (!loginHanded()) return null;
174
+ try {
175
+ return await vercelLogin();
176
+ } catch {
177
+ throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
178
+ }
179
+ }
180
+ /** The login to call Vercel's API with: a handed one, or `vercel login`'s, or none it can read (the keychain's). */
181
+ const apiLogin = async (handed) => handed ?? await getVercelToken().catch(() => null);
142
182
  /**
143
183
  * A person's token: their Vercel CLI login mints a development token for the
144
184
  * project the board's host is a deployment of, exchanged so its audience is the
145
185
  * board's URL. Cached on disk for its 12-hour life.
146
186
  */
147
187
  async function personToken(audience, discovery) {
148
- const cached = await readCache(audience);
188
+ const login = await handedToken();
189
+ const cached = await readCache(audience, login);
149
190
  if (cached && cached.name === void 0) {
150
- const user = await vercelLogin().then(vercelUser).catch(() => null);
191
+ const authToken = await apiLogin(login);
192
+ const user = authToken ? await vercelUser(authToken) : null;
151
193
  const filled = {
152
194
  ...cached,
153
195
  name: user?.name ?? null,
154
196
  avatar: user?.avatar ?? null
155
197
  };
156
- await writeCache(audience, filled).catch(() => {});
198
+ await writeCache(audience, login, filled).catch(() => {});
157
199
  return filled;
158
200
  }
159
201
  if (cached) return cached;
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
- const authToken = await vercelLogin().catch(() => null);
202
+ if (!discovery.project_id) throw new BoardError("no_project", `The board at ${audience} 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." });
203
+ const authToken = await apiLogin(login);
162
204
  const project = await boardProject(audience, discovery, authToken);
163
205
  let development;
164
206
  try {
@@ -171,7 +213,7 @@ async function personToken(audience, discovery) {
171
213
  if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
172
214
  development = (await response.json()).token;
173
215
  } catch (error) {
174
- if (error instanceof BoardError) throw error;
216
+ if (error instanceof BoardError || login) throw error;
175
217
  development = await tokenFromVercelCli(project);
176
218
  }
177
219
  const token = await exchangeVercelOidcToken({
@@ -186,7 +228,7 @@ async function personToken(audience, discovery) {
186
228
  name: user?.name ?? null,
187
229
  avatar: user?.avatar ?? null
188
230
  };
189
- await writeCache(audience, fresh).catch(() => {});
231
+ await writeCache(audience, login, fresh).catch(() => {});
190
232
  return fresh;
191
233
  }
192
234
  /**
@@ -211,7 +253,7 @@ async function boardProject(url, discovery, authToken) {
211
253
  return response.ok ? await response.json() : null;
212
254
  } : vercelCliApi, host);
213
255
  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);
256
+ if (found.projectId !== discovery.project_id) throw new BoardError("wrong_project", `The board at ${url} says its project is ${shown(discovery.project_id, PROJECT_ID) ?? "another one"}, but it's a deployment of ${found.projectId}, so no token is minted for it.`, 0);
215
257
  return {
216
258
  id: found.projectId,
217
259
  team: found.ownerId.startsWith("team_") ? found.ownerId : null
@@ -274,7 +316,7 @@ function connect(url, options = {}) {
274
316
  const discover = () => discovery ??= doFetch(url).then(async (response) => {
275
317
  if (!response.ok) throw new BoardError("no_board", `There's no board at ${url}: it answered ${response.status}.`, response.status);
276
318
  const found = await response.json();
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}.` });
319
+ if (found.audience !== url) throw new BoardError("wrong_audience", `The board at ${url} says its URL is ${shown(found.audience, URL_SHAPE) ?? "another one"}.`, 0, { hint: shown(found.audience, URL_SHAPE) ? `Use ${found.audience}.` : "Use the URL its discovery names." });
278
320
  return found;
279
321
  });
280
322
  let cached = null;
@@ -381,6 +423,19 @@ var Wall = class extends Error {
381
423
  }
382
424
  };
383
425
  /**
426
+ * A wall reached after a redirect to another origin, which none of a request's credentials follow,
427
+ * by design: no bypass or token gets through it from here, so none is looked for.
428
+ */
429
+ var Moved = class extends Error {
430
+ origin;
431
+ status;
432
+ constructor(origin, status) {
433
+ super("moved");
434
+ this.origin = origin;
435
+ this.status = status;
436
+ }
437
+ };
438
+ /**
384
439
  * This deployment's Vercel OIDC token, with `url` as its audience: what a hosted client sends a
385
440
  * board, for Deployment Protection's Trusted Sources. Its `cause` says why it couldn't.
386
441
  */
@@ -403,7 +458,12 @@ function guarded(url, doFetch, suppliedToken) {
403
458
  let secret;
404
459
  let looked;
405
460
  let envRejected = false;
406
- return async (input, init = {}) => {
461
+ return (input, init) => attempt(input, init).catch((error) => {
462
+ if (!(error instanceof Moved)) throw error;
463
+ const there = shown(`${error.origin}${new URL(url).pathname}`, URL_SHAPE);
464
+ throw new BoardError("redirected", `${url} redirects to ${there ? new URL(there).origin : "another origin"}, and its credentials don't follow it there, so Deployment Protection turns it away.`, error.status, { hint: there ? `Use ${there}.` : "Use the URL it redirects to." });
465
+ });
466
+ async function attempt(input, init = {}) {
407
467
  const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : void 0));
408
468
  init = {
409
469
  ...init,
@@ -447,7 +507,7 @@ function guarded(url, doFetch, suppliedToken) {
447
507
  } catch (error) {
448
508
  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
509
  }
450
- };
510
+ }
451
511
  }
452
512
  /**
453
513
  * One request that recognizes the wall: a redirect to `/sso-api`, an HTML
@@ -464,6 +524,8 @@ async function through(doFetch, input, init, bypass) {
464
524
  const method = (init.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
465
525
  let headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : void 0));
466
526
  if (bypass) headers.set("x-vercel-protection-bypass", bypass);
527
+ let moved = null;
528
+ const wall = (status) => moved ? new Moved(moved, status) : new Wall(status);
467
529
  for (let hops = 0;; hops++) {
468
530
  const response = await doFetch(hops === 0 ? input : target, {
469
531
  ...init,
@@ -473,10 +535,11 @@ async function through(doFetch, input, init, bypass) {
473
535
  const location = response.status >= 300 && response.status < 400 ? response.headers.get("location") : null;
474
536
  if (location) {
475
537
  const next = new URL(location, target);
476
- if (next.pathname.startsWith("/sso-api")) throw new Wall(response.status);
538
+ if (next.pathname.startsWith("/sso-api")) throw wall(response.status);
477
539
  if (method === "GET" || method === "HEAD") {
478
540
  if (hops >= 20) throw new BoardError("too_many_redirects", `${target} redirected more than 20 times.`, response.status);
479
541
  if (next.origin !== new URL(target).origin) {
542
+ moved = next.origin;
480
543
  headers = new Headers(headers);
481
544
  headers.delete("authorization");
482
545
  headers.delete("x-vercel-trusted-oidc-idp-token");
@@ -487,8 +550,8 @@ async function through(doFetch, input, init, bypass) {
487
550
  }
488
551
  }
489
552
  const { status } = response;
490
- 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);
491
- if (status === 401 && response.headers.has("x-vercel-error")) throw new Wall(status);
553
+ 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 wall(status);
554
+ if (status === 401 && response.headers.has("x-vercel-error")) throw wall(status);
492
555
  return response;
493
556
  }
494
557
  }
@@ -518,23 +581,18 @@ const automationBypass = (bypasses) => Object.entries(bypasses ?? {}).find(([, v
518
581
  */
519
582
  async function projectBypass(url, host, doFetch) {
520
583
  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
- let api;
522
- try {
523
- const token = await vercelLogin();
524
- api = async (method, path) => {
525
- const response = await doFetch(`https://api.vercel.com${path}`, {
526
- method,
527
- headers: {
528
- authorization: `Bearer ${token}`,
529
- ...method === "PATCH" ? { "content-type": "application/json" } : {}
530
- },
531
- ...method === "PATCH" ? { body: "{}" } : {}
532
- });
533
- return response.ok ? await response.json() : null;
534
- };
535
- } catch {
536
- api = vercelCliApi;
537
- }
584
+ const token = await apiLogin(await handedToken());
585
+ const api = token ? async (method, path) => {
586
+ const response = await doFetch(`https://api.vercel.com${path}`, {
587
+ method,
588
+ headers: {
589
+ authorization: `Bearer ${token}`,
590
+ ...method === "PATCH" ? { "content-type": "application/json" } : {}
591
+ },
592
+ ...method === "PATCH" ? { body: "{}" } : {}
593
+ });
594
+ return response.ok ? await response.json() : null;
595
+ } : vercelCliApi;
538
596
  const project = await findProject(api, host);
539
597
  if (!project) throw blocked("can't see its project", "Log in as someone on its project's team (vercel login), ask for access");
540
598
  const scope = project.ownerId.startsWith("team_") ? `?teamId=${project.ownerId}` : "";
@@ -600,4 +658,4 @@ const vercelCliApi = async (method, path) => {
600
658
  }
601
659
  };
602
660
  //#endregion
603
- export { projectDir as a, localTokenFile as i, connect as n, vercelLogin as o, environmentProfile as r, vercelProjectFile as s, boardUrl as t };
661
+ export { localTokenFile as a, vercelLogin as c, handLogin as i, vercelProjectFile as l, connect as n, loginHanded as o, environmentProfile as r, projectDir as s, boardUrl as t };