@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.
- package/dist/{a2-CQ3WcqMv.mjs → a2-Qzj0kCWU.mjs} +6 -6
- package/dist/access.d.mts +1 -2
- package/dist/access.mjs +3 -3
- package/dist/{actors-CDDhhFNY.mjs → actors-BLXVp4oZ.mjs} +2 -2
- package/dist/{auth-BwWH3Twu.mjs → auth-lTkOgvcE.mjs} +1 -1
- package/dist/{backend-DmXjVnil.d.mts → backend-ZKYTe3xa.d.mts} +5 -15
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +1 -2
- package/dist/backend-a2/store-memory.d.mts +1 -1
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2.d.mts +5 -6
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-BJ7AdK0c.mjs → backend-http-BB0Gld6Q.mjs} +8 -5
- package/dist/{backend-http-BsP0Xq9e.d.mts → backend-http-E7YU0qhn.d.mts} +3 -4
- package/dist/{backend-http-Cs-UKpYp.mjs → backend-http-F0W6e36G.mjs} +2 -2
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-vercel-CED673bx.d.mts → backend-vercel-CIRObA0t.d.mts} +3 -4
- package/dist/{backend-vercel-CG2SYa03.mjs → backend-vercel-CtB46vMQ.mjs} +3 -3
- package/dist/{backend-vercel-B0Q0K0O5.mjs → backend-vercel-DUWIcFi9.mjs} +24 -8
- package/dist/backend-vercel.d.mts +1 -1
- package/dist/backend-vercel.mjs +1 -1
- package/dist/backend-vercel.node.d.mts +6 -1
- package/dist/backend-vercel.node.mjs +2 -2
- package/dist/{blob-C0J3wbGv.d.mts → blob-B-9AvYis.d.mts} +2 -1
- package/dist/{chat-provider-6eYkL2ef.d.mts → chat-provider-DsNEBQiA.d.mts} +3 -2
- package/dist/cli-commands.d.mts +1 -1
- package/dist/cli-commands.mjs +1 -1
- package/dist/cli-embedded.d.mts +7 -3
- package/dist/cli-embedded.mjs +8 -4
- package/dist/cli.mjs +4 -4
- package/dist/{client-BY-BCggB.mjs → client-D0Px7KzF.mjs} +16 -4
- package/dist/{provider-C_sj4-GL.d.mts → client-xLxeDz0f.d.mts} +13 -689
- package/dist/{commands-D0wXq6CY.d.mts → commands-D6gvrOh3.d.mts} +4 -4
- package/dist/{commands-BtL4jFq8.mjs → commands-nEG47RM_.mjs} +7 -7
- package/dist/{conformance-BZ53bxrG.mjs → conformance-DlnfuzEB.mjs} +1 -1
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-SeaJ_CIY.mjs → engine-ROa7-IQY.mjs} +2 -2
- package/dist/errors-CB4tnZGz.d.mts +16 -0
- package/dist/{errors-Bven0k29.mjs → errors-CusN4x2l.mjs} +14 -6
- package/dist/exports-D22M68rk.d.mts +5 -0
- package/dist/{exports-ByvYtxFT.mjs → exports-NFjBgIT2.mjs} +6 -6
- package/dist/{folds-CRYY14kc.d.mts → folds-jS9wlh9_.d.mts} +49 -2
- package/dist/{index-k2LfZxl-.d.mts → index-C05it0mC.d.mts} +2 -2
- package/dist/index.d.mts +5 -4
- package/dist/{index.default-DmfhBZZi.mjs → index.default-Boe7x-mK.mjs} +1 -1
- package/dist/index.mjs +6 -6
- package/dist/index.node.d.mts +5 -4
- package/dist/index.node.mjs +4 -4
- package/dist/inspect/app.js +108 -108
- package/dist/{main-BNkywL2P.mjs → main-BuahKR16.mjs} +84 -69
- package/dist/{middleware-B0J7QBq7.mjs → middleware-ChKM1zuu.mjs} +1 -1
- package/dist/{node-C51_xzCb.mjs → node-Ybi9E6bO.mjs} +104 -46
- package/dist/provider-Cdg1qadh.d.mts +688 -0
- package/dist/{serve-DYQ64Q5R.mjs → serve-BbHEQk_Y.mjs} +5 -5
- package/dist/{server-BgK1sgj7.mjs → server-Bphio_82.mjs} +4 -4
- package/dist/{serving-rXQcEgTO.mjs → serving-CvOWEym2.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 +3 -3
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.d.mts +2 -2
- package/dist/{tokens-U-XRlEfq.mjs → tokens-BuCU-61R.mjs} +2 -2
- package/dist/{types-8yq2VnKI.d.mts → types-aCAf6lGU.d.mts} +1 -1
- package/dist/{watchers-hash-C4CJg7K1.mjs → watchers-hash-BfDPW95j.mjs} +5 -5
- package/docs/01-board/04-messages.md +1 -1
- package/docs/01-board/05-reading.md +2 -2
- package/docs/01-board/06-watchers.md +3 -3
- package/docs/02-sync/01-overview.md +2 -2
- package/docs/03-guides/03-ci.md +2 -2
- package/docs/03-guides/04-triage.md +5 -5
- package/docs/04-reference/01-cli.md +9 -4
- package/docs/04-reference/02-board.md +6 -2
- package/docs/index.md +2 -2
- package/package.json +1 -1
- package/dist/exports-CPrIkpz3.d.mts +0 -4
- 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-
|
|
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-
|
|
6
|
-
import { i as groups, n as filters$1, o as options, r as globalOptions, s as usage$1, t as commands } from "./commands-
|
|
7
|
-
import {
|
|
8
|
-
import { t as http } from "./backend-http-
|
|
9
|
-
import { t as board } from "./server-
|
|
10
|
-
import { t as conformance } from "./conformance-
|
|
11
|
-
import { n as vercel, t as credentialSource } from "./backend-vercel-
|
|
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.
|
|
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.
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
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
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
115
|
-
*
|
|
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
|
-
|
|
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
|
|
139
|
-
* from its `--token`
|
|
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
|
|
188
|
+
const login = await handedToken();
|
|
189
|
+
const cached = await readCache(audience, login);
|
|
149
190
|
if (cached && cached.name === void 0) {
|
|
150
|
-
const
|
|
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 ${
|
|
161
|
-
const authToken = await
|
|
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
|
|
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
|
|
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
|
|
491
|
-
if (status === 401 && response.headers.has("x-vercel-error")) throw
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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 {
|
|
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 };
|