@vercel/board 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -17
- package/dist/{a2-HUbQJ0RD.mjs → a2-DngWY2Eb.mjs} +741 -415
- package/dist/access-CY5xVMIK.mjs +242 -0
- package/dist/{access-CV96SYgr.d.mts → access-DPcrKIH_.d.mts} +6 -1
- package/dist/access.d.mts +4 -4
- package/dist/access.mjs +5 -4
- package/dist/{actors-DcJhFbwJ.mjs → actors-Bm-ojWWT.mjs} +2 -2
- package/dist/{auth-JO22iIwr.mjs → auth-CyO2aw5n.mjs} +126 -236
- package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
- package/dist/{backend-CPC_vyOo.d.mts → backend-Bob-5CM7.d.mts} +93 -42
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +38 -98
- package/dist/backend-a2/contract.mjs +1 -1
- package/dist/backend-a2/scheduler-local.d.mts +1 -1
- package/dist/backend-a2/scheduler-local.mjs +1 -1
- package/dist/backend-a2/scheduler-qstash.d.mts +3 -3
- package/dist/backend-a2/scheduler-qstash.mjs +8 -8
- package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
- package/dist/backend-a2/scheduler-vercel.mjs +6 -6
- package/dist/backend-a2/store-memory.d.mts +2 -2
- package/dist/backend-a2/store-memory.mjs +3 -3
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +34 -15
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +6 -6
- package/dist/backend-a2/store-redis.d.mts +2 -2
- package/dist/backend-a2/store-redis.mjs +5 -5
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +3 -3
- package/dist/backend-a2.d.mts +143 -242
- package/dist/backend-a2.mjs +2 -2
- package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-BRrRbjRx.d.mts} +59 -45
- package/dist/{backend-http-CzIJMm58.mjs → backend-http-CvUKOn-m.mjs} +11 -10
- package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-D8mUzuKQ.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-DQc_TaHp.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
- package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-BaSEUdwg.d.mts} +62 -47
- package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-CmY9yZ9m.mjs} +279 -42
- package/dist/backend-vercel.d.mts +1 -1
- package/dist/backend-vercel.mjs +1 -1
- package/dist/backend-vercel.node.d.mts +1 -1
- package/dist/backend-vercel.node.mjs +1 -1
- package/dist/{blob-DmiQjrYy.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
- package/dist/{blob-DdbpJqN1.mjs → blob-WbB2DJKn.mjs} +30 -11
- package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-BChYrrA7.d.mts} +32 -28
- package/dist/{chat-provider-COtsU5jB.mjs → chat-provider-DkFp2hrh.mjs} +105 -60
- package/dist/cli-commands.d.mts +1 -1
- package/dist/cli-commands.mjs +1 -1
- package/dist/cli-embedded.d.mts +2 -2
- package/dist/cli-embedded.mjs +1 -1
- package/dist/cli.mjs +4 -4
- package/dist/{client-BqAjk0Q4.d.mts → client-COfYrgfV.d.mts} +240 -227
- package/dist/{commands-D6gvrOh3.d.mts → commands-DK4W3HCJ.d.mts} +47 -10
- package/dist/{commands-C3R1AWgm.mjs → commands-L1JSGz2Z.mjs} +212 -54
- package/dist/conformance-DK43HiHf.mjs +1000 -0
- package/dist/conformance.d.mts +8 -2
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-DQxXk7dX.mjs → engine-DurYIRt8.mjs} +29 -210
- package/dist/{errors-DDktVeUa.d.mts → errors-2vGs49Db.d.mts} +1 -1
- package/dist/{client-D0Px7KzF.mjs → errors-CJXhhzvT.mjs} +132 -27
- package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
- package/dist/{exports-BG-0Wyn-.mjs → exports-CWIdN0Ub.mjs} +195 -103
- package/dist/exports-FBOQWT7O.d.mts +5 -0
- package/dist/{folds-BDMSxBUf.mjs → folds-Cu9Q9zyQ.mjs} +3 -22
- package/dist/{folds-ZkHM4M-v.d.mts → folds-DLUzfibJ.d.mts} +15 -40
- package/dist/{index-D3VvNfh_.d.mts → index-Ho-ayyQL.d.mts} +2 -2
- package/dist/index.d.mts +7 -7
- package/dist/{index.default-rB8sJ9RX.mjs → index.default-BUcwDaAe.mjs} +1 -1
- package/dist/index.mjs +5 -6
- package/dist/index.node.d.mts +7 -7
- package/dist/index.node.mjs +3 -4
- package/dist/inspect/app.js +62 -61
- package/dist/inspect/engine.js +3 -3
- package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
- package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
- package/dist/{main-CVBqe3VO.mjs → main-Dz-8komG.mjs} +251 -127
- package/dist/{middleware-CXjEeef4.mjs → middleware-BavYSDUa.mjs} +1 -1
- package/dist/{node-Zd_C-gxP.mjs → node-BgeGcwX0.mjs} +7 -3
- package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
- package/dist/{project-CBXlLftU.mjs → project-CQ4VUibA.mjs} +1 -1
- package/dist/{provider-DmSTp9lI.d.mts → provider-gynDUoyW.d.mts} +13 -43
- package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-DHf85ixI.mjs} +40 -42
- package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
- package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
- package/dist/{reducers-DJzVpXwM.mjs → reducers-DcENwtwZ.mjs} +74 -77
- package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
- package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
- package/dist/{serve-G9RuaDSr.mjs → serve-CTmow-YH.mjs} +7 -7
- package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
- package/dist/{server-BixV_byP.mjs → server-DG8D6VV0.mjs} +4 -4
- package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
- package/dist/{serving-B75Y4BF-.mjs → serving-DBFRo6U_.mjs} +1 -1
- package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
- package/dist/{sql-D4Z_efJX.mjs → sql-CT5xVuGr.mjs} +2 -8
- package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
- package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
- package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
- package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
- package/dist/store-memory-4IBl0Ztu.mjs +2 -0
- package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
- package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
- package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
- package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
- package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
- package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
- package/dist/{sync-kapso.d.mts → sync-chat-sdk.d.mts} +33 -37
- package/dist/sync-chat-sdk.mjs +44 -0
- package/dist/sync-github.d.mts +5 -18
- package/dist/sync-github.mjs +4 -70
- package/dist/sync-linear.d.mts +3 -3
- package/dist/sync-linear.mjs +6 -5
- package/dist/sync-slack.d.mts +7 -7
- package/dist/sync-slack.mjs +33 -54
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +2 -4
- package/dist/{tokens-CC3izSNr.mjs → tokens-Dnyd_y1n.mjs} +15 -6
- package/dist/{types-No0WdhE5.d.mts → types-CmrFjpml.d.mts} +2 -4
- package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-BjwlnGD9.mjs} +124 -115
- package/docs/01-board/01-mounting.md +2 -2
- package/docs/01-board/03-identity.md +5 -6
- package/docs/01-board/04-messages.md +96 -2
- package/docs/01-board/05-reading.md +2 -1
- package/docs/01-board/06-watchers.md +8 -6
- package/docs/01-board/08-middleware.md +6 -3
- package/docs/01-board/09-access.md +145 -14
- package/docs/02-sync/01-overview.md +19 -26
- package/docs/02-sync/02-functions.md +8 -9
- package/docs/02-sync/03-slack.md +5 -5
- package/docs/02-sync/04-github.md +2 -2
- package/docs/02-sync/06-providers.md +91 -62
- package/docs/03-guides/01-coding-agents.md +1 -1
- package/docs/03-guides/03-ci.md +7 -4
- package/docs/04-reference/01-cli.md +109 -51
- package/docs/04-reference/02-board.md +76 -28
- package/docs/04-reference/03-http.md +76 -38
- package/docs/04-reference/04-log.md +8 -11
- package/docs/04-reference/05-conformance.md +47 -7
- package/docs/index.md +2 -2
- package/package.json +7 -10
- package/sql/postgres/board.sql +1 -1
- package/sql/postgres/schema.sql +1 -1
- package/dist/conformance-DlnfuzEB.mjs +0 -556
- package/dist/errors-CusN4x2l.mjs +0 -88
- package/dist/events-qf5XG5VQ.mjs +0 -75
- package/dist/exports-mhAYiU4i.d.mts +0 -5
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
- package/dist/store-memory-ZLiZbH3R.mjs +0 -2
- package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
- package/dist/sync-kapso.mjs +0 -89
- package/dist/sync.chat.d.mts +0 -2
- package/dist/sync.chat.mjs +0 -2
- package/dist/users-yryHKFMo.mjs +0 -42
- package/docs/02-sync/05-whatsapp.md +0 -152
|
@@ -1,229 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
//#region src/server/
|
|
4
|
-
/**
|
|
5
|
-
* What a principal may do in a channel. `read` covers messages, threads,
|
|
6
|
-
* `/events`, and schedules; `post` starts threads and edits your own messages;
|
|
7
|
-
* `manage` is the board's own bookkeeping (inbox, sources).
|
|
8
|
-
*/
|
|
9
|
-
const PERMISSIONS = [
|
|
10
|
-
"read",
|
|
11
|
-
"post",
|
|
12
|
-
"react",
|
|
13
|
-
"label",
|
|
14
|
-
"sync",
|
|
15
|
-
"manage"
|
|
16
|
-
];
|
|
17
|
-
/**
|
|
18
|
-
* One hop of implication, kept as data like Vercel's git broker
|
|
19
|
-
* (`ACTION_IMPLICATIONS`). Each verb that addresses a message by ID implies
|
|
20
|
-
* `read`, so none of them is a blind existence oracle. `post` implies nothing:
|
|
21
|
-
* write-only is `['post']`.
|
|
22
|
-
*/
|
|
23
|
-
const IMPLIES = {
|
|
24
|
-
read: [],
|
|
25
|
-
post: [],
|
|
26
|
-
react: ["read"],
|
|
27
|
-
label: ["read"],
|
|
28
|
-
sync: ["read"],
|
|
29
|
-
manage: [
|
|
30
|
-
"read",
|
|
31
|
-
"post",
|
|
32
|
-
"react",
|
|
33
|
-
"label",
|
|
34
|
-
"sync"
|
|
35
|
-
]
|
|
36
|
-
};
|
|
37
|
-
const ORDER = new Map(PERMISSIONS.map((permission, index) => [permission, index]));
|
|
38
|
-
const SHAPE = "write *, a channel, or a channel and its subtree (feedback/)";
|
|
39
|
-
/** A pattern's parts; any other `*` throws, and says what to type instead. */
|
|
40
|
-
function parsePattern(pattern) {
|
|
41
|
-
if (pattern === "*") return { kind: "all" };
|
|
42
|
-
if (pattern.includes("*")) {
|
|
43
|
-
const before = pattern.slice(0, pattern.indexOf("*")).replace(/\/+$/, "");
|
|
44
|
-
const hint = before === "" ? "write * for every channel" : ChannelPath.safeParse(before).success ? `write ${before}/ for ${before} and its subtree` : SHAPE;
|
|
45
|
-
throw new Error(`invalid channel pattern ${pattern}: ${hint}`);
|
|
46
|
-
}
|
|
47
|
-
const subtree = pattern.endsWith("/");
|
|
48
|
-
const path = subtree ? pattern.slice(0, -1) : pattern;
|
|
49
|
-
if (!ChannelPath.safeParse(path).success) throw new Error(`invalid channel pattern ${pattern || "(empty)"}: ${SHAPE}`);
|
|
50
|
-
return subtree ? {
|
|
51
|
-
kind: "subtree",
|
|
52
|
-
path
|
|
53
|
-
} : {
|
|
54
|
-
kind: "exact",
|
|
55
|
-
path
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
const key = (pattern) => pattern.kind === "all" ? "*" : pattern.kind === "subtree" ? `${pattern.path}/` : pattern.path;
|
|
59
|
-
/** A grant with its implications, closed: `['label']` is `read` and `label`. */
|
|
60
|
-
function expand(grant) {
|
|
61
|
-
const out = /* @__PURE__ */ new Set();
|
|
62
|
-
const add = (permission) => {
|
|
63
|
-
if (!ORDER.has(permission)) throw new Error(`unknown permission ${String(permission)}: use ${PERMISSIONS.join(", ")}`);
|
|
64
|
-
if (out.has(permission)) return;
|
|
65
|
-
out.add(permission);
|
|
66
|
-
for (const implied of IMPLIES[permission]) add(implied);
|
|
67
|
-
};
|
|
68
|
-
for (const permission of grant === "all" ? PERMISSIONS : grant) add(permission);
|
|
69
|
-
return out;
|
|
70
|
-
}
|
|
71
|
-
const NOTHING = /* @__PURE__ */ new Set();
|
|
72
|
-
function compile(entries) {
|
|
73
|
-
let all = null;
|
|
74
|
-
const exact = /* @__PURE__ */ new Map();
|
|
75
|
-
const subtrees = [];
|
|
76
|
-
for (const [pattern, permissions] of entries) if (pattern.kind === "all") all = permissions;
|
|
77
|
-
else if (pattern.kind === "exact") exact.set(pattern.path, permissions);
|
|
78
|
-
else subtrees.push([pattern.path, permissions]);
|
|
79
|
-
subtrees.sort((a, b) => b[0].length - a[0].length);
|
|
80
|
-
return {
|
|
81
|
-
all,
|
|
82
|
-
exact,
|
|
83
|
-
subtrees
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
const under = (channel, path) => channel === path || channel.startsWith(`${path}/`);
|
|
87
|
-
/** What the most specific pattern covering `channel` gives; with `subtreeOnly`, ignoring an exact rule for it. */
|
|
88
|
-
function resolve(rules, channel, subtreeOnly = false) {
|
|
89
|
-
if (!subtreeOnly) {
|
|
90
|
-
const exact = rules.exact.get(channel);
|
|
91
|
-
if (exact) return exact;
|
|
92
|
-
}
|
|
93
|
-
for (const [path, permissions] of rules.subtrees) if (under(channel, path)) return permissions;
|
|
94
|
-
return rules.all ?? NOTHING;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* What `rules` gives every channel `pattern` governs where nothing more
|
|
98
|
-
* specific does. The patterns matching one channel form a chain, so that's one
|
|
99
|
-
* answer: an exact channel resolves as itself, a subtree as whatever covers the
|
|
100
|
-
* subtree's root, ignoring an exact rule for the root.
|
|
101
|
-
*/
|
|
102
|
-
const at = (rules, pattern) => pattern.kind === "all" ? rules.all ?? NOTHING : resolve(rules, pattern.path, pattern.kind === "subtree");
|
|
103
|
-
const equal = (a, b) => a.size === b.size && [...a].every((permission) => b.has(permission));
|
|
104
|
-
/** The answer of the pattern next less specific than `pattern` in `rules`. */
|
|
105
|
-
function parent(rules, pattern) {
|
|
106
|
-
if (pattern.kind === "exact") return resolve(rules, pattern.path, true);
|
|
107
|
-
if (pattern.kind === "subtree") {
|
|
108
|
-
for (const [path, permissions] of rules.subtrees) if (path !== pattern.path && under(pattern.path, path)) return permissions;
|
|
109
|
-
}
|
|
110
|
-
return rules.all ?? NOTHING;
|
|
111
|
-
}
|
|
112
|
-
/** Drops rules that say what their parent says, and an empty `*`, so equal access has one summary. */
|
|
113
|
-
function canonical(entries) {
|
|
114
|
-
const rules = compile(entries);
|
|
115
|
-
return compile(entries.filter(([pattern, permissions]) => pattern.kind === "all" ? permissions.size > 0 : !equal(permissions, parent(rules, pattern))));
|
|
116
|
-
}
|
|
117
|
-
function entriesOf(can) {
|
|
118
|
-
if (can === "all" || Array.isArray(can)) return [[{ kind: "all" }, expand(can)]];
|
|
119
|
-
return Object.entries(can).map(([pattern, grant]) => [parsePattern(pattern), expand(grant)]);
|
|
120
|
-
}
|
|
121
|
-
const entriesIn = (rules) => [
|
|
122
|
-
...rules.all ? [[{ kind: "all" }, rules.all]] : [],
|
|
123
|
-
...rules.subtrees.map(([path, permissions]) => [{
|
|
124
|
-
kind: "subtree",
|
|
125
|
-
path
|
|
126
|
-
}, permissions]),
|
|
127
|
-
...[...rules.exact].map(([path, permissions]) => [{
|
|
128
|
-
kind: "exact",
|
|
129
|
-
path
|
|
130
|
-
}, permissions])
|
|
131
|
-
];
|
|
132
|
-
const RULES = Symbol("board.access");
|
|
133
|
-
const isAccess = (value) => typeof value === "object" && RULES in value;
|
|
134
|
-
function fromRules(rules) {
|
|
135
|
-
const entries = entriesIn(rules);
|
|
136
|
-
const everywhere = (permission) => (rules.all?.has(permission) ?? false) && entries.every(([, permissions]) => permissions.has(permission));
|
|
137
|
-
const readable = /* @__PURE__ */ new Map();
|
|
138
|
-
return {
|
|
139
|
-
[RULES]: rules,
|
|
140
|
-
can: (permission, channel) => resolve(rules, channel).has(permission),
|
|
141
|
-
readable(channel) {
|
|
142
|
-
let found = readable.get(channel);
|
|
143
|
-
if (found === void 0) readable.set(channel, found = resolve(rules, channel).has("read"));
|
|
144
|
-
return found;
|
|
145
|
-
},
|
|
146
|
-
readsAll: everywhere("read"),
|
|
147
|
-
everywhere,
|
|
148
|
-
anywhere: (permission) => entries.some(([, permissions]) => permissions.has(permission)),
|
|
149
|
-
narrow(scope) {
|
|
150
|
-
const other = isAccess(scope) ? scope[RULES] : canonical(entriesOf(scope));
|
|
151
|
-
const patterns = /* @__PURE__ */ new Map([["*", { kind: "all" }]]);
|
|
152
|
-
for (const [pattern] of [...entries, ...entriesIn(other)]) patterns.set(key(pattern), pattern);
|
|
153
|
-
return fromRules(canonical([...patterns.values()].map((pattern) => {
|
|
154
|
-
const theirs = at(other, pattern);
|
|
155
|
-
return [pattern, new Set([...at(rules, pattern)].filter((permission) => theirs.has(permission)))];
|
|
156
|
-
})));
|
|
157
|
-
},
|
|
158
|
-
summary() {
|
|
159
|
-
const named = entries.map(([pattern, permissions]) => [key(pattern), permissions]);
|
|
160
|
-
named.sort(([a], [b]) => a === "*" ? -1 : b === "*" ? 1 : a.localeCompare(b));
|
|
161
|
-
const out = {};
|
|
162
|
-
for (const [name, permissions] of named) out[name] = [...permissions].sort((a, b) => ORDER.get(a) - ORDER.get(b));
|
|
163
|
-
return out;
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/** Access from config: `'all'`, a list for every channel, or a map from pattern to grant. Throws on a bad pattern or permission. */
|
|
168
|
-
function compileAccess(can) {
|
|
169
|
-
return fromRules(canonical(entriesOf(can)));
|
|
170
|
-
}
|
|
171
|
-
const FULL_ACCESS = compileAccess("all");
|
|
172
|
-
/**
|
|
173
|
-
* `x-board-scope`: `dev/=read,post,react,label;ci/=read`. Each entry is a
|
|
174
|
-
* pattern and its permissions (or `all`). It's meant for a proxy that holds the
|
|
175
|
-
* credential, like Pi's sandbox: the board intersects it with the principal's
|
|
176
|
-
* access, so it can only narrow, and a caller holding its own token gains
|
|
177
|
-
* nothing by sending it.
|
|
178
|
-
*/
|
|
179
|
-
function parseScope(header) {
|
|
180
|
-
if (header.length > 4e3) throw new Error(`x-board-scope is longer than ${SCOPE_MAX} characters`);
|
|
181
|
-
const out = {};
|
|
182
|
-
const entries = header.split(";").map((entry) => entry.trim());
|
|
183
|
-
if (entries.every((entry) => entry === "")) throw new Error("x-board-scope is empty: write pattern=permissions, like dev/=read,post");
|
|
184
|
-
for (const entry of entries) {
|
|
185
|
-
if (entry === "") continue;
|
|
186
|
-
const split = entry.indexOf("=");
|
|
187
|
-
if (split < 0) throw new Error(`x-board-scope entry ${entry} needs =: write pattern=permissions, like dev/=read,post`);
|
|
188
|
-
const pattern = entry.slice(0, split).trim();
|
|
189
|
-
const list = entry.slice(split + 1).trim();
|
|
190
|
-
if (Object.hasOwn(out, pattern)) throw new Error(`x-board-scope lists ${pattern} twice`);
|
|
191
|
-
parsePattern(pattern);
|
|
192
|
-
const permissions = list === "" ? [] : list.split(",").map((permission) => permission.trim());
|
|
193
|
-
if (permissions.length === 1 && permissions[0] === "all") {
|
|
194
|
-
out[pattern] = "all";
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
for (const permission of permissions) if (!ORDER.has(permission)) throw new Error(`x-board-scope: unknown permission ${permission || "(empty)"}: use ${PERMISSIONS.join(", ")}, or all`);
|
|
198
|
-
out[pattern] = permissions;
|
|
199
|
-
}
|
|
200
|
-
return out;
|
|
201
|
-
}
|
|
202
|
-
const PHRASE = {
|
|
203
|
-
read: (channel) => `read ${channel}`,
|
|
204
|
-
post: (channel) => `post in ${channel}`,
|
|
205
|
-
react: (channel) => `react in ${channel}`,
|
|
206
|
-
label: (channel) => `label in ${channel}`,
|
|
207
|
-
sync: (channel) => `sync in ${channel}`,
|
|
208
|
-
manage: (channel) => `manage ${channel}`
|
|
209
|
-
};
|
|
1
|
+
import { a as forbidden, l as unauthorized, r as badRequest } from "./errors-CJXhhzvT.mjs";
|
|
2
|
+
import { i as parseScope, o as union, r as compileAccess, t as FULL_ACCESS } from "./access-CY5xVMIK.mjs";
|
|
3
|
+
//#region src/server/auth.ts
|
|
210
4
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
5
|
+
* Whether a caller wrote what `by` says, as far as changing it goes (editing, deleting, canceling a
|
|
6
|
+
* schedule): its principal did, and when a board token pins the profile it writes as, as that
|
|
7
|
+
* profile. A token acts as its minter, so the principal alone would let one agent's token change
|
|
8
|
+
* what its minter or another agent wrote. Another token that pins the same profile writes as the
|
|
9
|
+
* same author, so a renewed one picks up where the last left off.
|
|
216
10
|
*/
|
|
217
|
-
|
|
218
|
-
if (hidden && !access.readable(channel)) throw hidden();
|
|
219
|
-
if (!access.can(permission, channel)) throw forbidden(`this token can't ${PHRASE[permission](channel)}`);
|
|
220
|
-
return Object.freeze({
|
|
221
|
-
permission,
|
|
222
|
-
channel
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
//#endregion
|
|
226
|
-
//#region src/server/auth.ts
|
|
11
|
+
const wroteAs = (caller, by) => by.principal === caller.principal.id && ((caller.principal.pinned?.profile ?? null) === null || by.actor === caller.actor.id);
|
|
227
12
|
/**
|
|
228
13
|
* Principal IDs a board authenticates: its people, deployments, workflows, and its own code. No
|
|
229
14
|
* platform actor sync writes as may have one, or an import could pass for someone signed in.
|
|
@@ -322,11 +107,101 @@ function trustedIssuers(issuers) {
|
|
|
322
107
|
}
|
|
323
108
|
/** The OIDC issuers rules name: the only ones whose tokens are verified for them. */
|
|
324
109
|
const issuersOf = (rules) => [...new Set(rules.flatMap((rule) => typeof rule !== "function" && rule.issuer === "oidc" ? [rule.url] : []))];
|
|
110
|
+
/** What a board token starts with, so board() knows its own before it tries any other verifier. */
|
|
111
|
+
const BOARD_TOKEN_PREFIX = "board_";
|
|
112
|
+
/**
|
|
113
|
+
* A board token's SHA-256, hex: what the board keeps instead of the secret, both when it mints one
|
|
114
|
+
* and when a request carries one. A token is 32 random bytes, so no salt or key is needed.
|
|
115
|
+
*/
|
|
116
|
+
async function tokenHash(secret) {
|
|
117
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret)));
|
|
118
|
+
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
119
|
+
}
|
|
120
|
+
/** What a grant or a token left without `can` gives: what Vercel's API gives a project it lets use a board. */
|
|
121
|
+
const DEFAULT_CAN = { "*": [
|
|
122
|
+
"read",
|
|
123
|
+
"post",
|
|
124
|
+
"react",
|
|
125
|
+
"label"
|
|
126
|
+
] };
|
|
127
|
+
const GRANT_CLAIMS_MAX = 32;
|
|
128
|
+
const GRANT_VALUES_MAX = 32;
|
|
129
|
+
const GRANT_VALUE_MAX = 1e3;
|
|
130
|
+
const invalidGrant = (message) => badRequest(message, "invalid_grant");
|
|
131
|
+
/** Whether `host` names something on the board's own network rather than on the internet. */
|
|
132
|
+
function privateHost(host) {
|
|
133
|
+
const name = host.toLowerCase().replace(/\.$/, "");
|
|
134
|
+
if (name.startsWith("[") || /^\d+(\.\d+){3}$/.test(name) || /^\d+$/.test(name)) return true;
|
|
135
|
+
if (!name.includes(".") || name === "localhost") return true;
|
|
136
|
+
return /\.(localhost|local|internal|home|lan|corp|intranet)$/.test(name);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A grant checked and put in one form: its issuer, its claims with keys and lists sorted, its
|
|
140
|
+
* `can`, and the key that says it's the same grant as another. A 400 `invalid_grant` that says
|
|
141
|
+
* what to write. `trusted`: the issuers the board's rules and other grants name, which no two may
|
|
142
|
+
* share principals between.
|
|
143
|
+
*/
|
|
144
|
+
function checkGrant(spec, trusted) {
|
|
145
|
+
const { issuer } = spec;
|
|
146
|
+
if (typeof issuer !== "string" || issuer === "") throw invalidGrant("a grant needs an issuer: vercel, github, or an https URL");
|
|
147
|
+
if (issuer !== "vercel" && issuer !== "github") {
|
|
148
|
+
let url;
|
|
149
|
+
try {
|
|
150
|
+
url = new URL(issuer);
|
|
151
|
+
} catch {
|
|
152
|
+
throw invalidGrant(`a grant's issuer is vercel, github, or an https URL, got ${issuer}`);
|
|
153
|
+
}
|
|
154
|
+
if (VERCEL_ISSUER.test(issuer)) throw invalidGrant("write the issuer as vercel, and another team's projects with owner_id");
|
|
155
|
+
if (issuer === "https://token.actions.githubusercontent.com") throw invalidGrant("write the issuer as github");
|
|
156
|
+
if (url.protocol !== "https:" || url.search || url.hash || url.username || url.password) throw invalidGrant(`a grant's issuer is an https URL with no query or credentials, got ${issuer}`);
|
|
157
|
+
if (privateHost(url.hostname)) throw invalidGrant(`a grant's issuer is on the internet, not ${url.hostname}`);
|
|
158
|
+
const name = issuerName(issuer);
|
|
159
|
+
const other = trusted.find((candidate) => candidate !== issuer && /^https:/.test(candidate) && issuerName(candidate) === name);
|
|
160
|
+
if (other !== void 0) throw invalidGrant(`issuers ${other} and ${issuer} name the same principals, oidc:${name}:<sub>: use ${other}`);
|
|
161
|
+
}
|
|
162
|
+
const entries = typeof spec.claims === "object" && spec.claims !== null ? Object.entries(spec.claims) : [];
|
|
163
|
+
if (entries.length === 0) throw invalidGrant(`name at least one claim for ${issuer}: its issuer signs tokens for everyone it serves`);
|
|
164
|
+
if (entries.length > GRANT_CLAIMS_MAX) throw invalidGrant(`a grant names at most ${GRANT_CLAIMS_MAX} claims`);
|
|
165
|
+
const claims = {};
|
|
166
|
+
for (const [name, value] of entries.sort(([a], [b]) => a.localeCompare(b))) {
|
|
167
|
+
const values = typeof value === "string" ? [value] : Array.isArray(value) ? [...new Set(value)] : [];
|
|
168
|
+
if (values.length === 0 || values.some((one) => typeof one !== "string" || one === "")) throw invalidGrant(`claim ${name} has no value to match: give a string, or a list of them`);
|
|
169
|
+
if (values.length > GRANT_VALUES_MAX || values.some((one) => one.length > GRANT_VALUE_MAX)) throw invalidGrant(`claim ${name} lists at most ${GRANT_VALUES_MAX} values of up to ${GRANT_VALUE_MAX} characters`);
|
|
170
|
+
const sorted = values.sort();
|
|
171
|
+
claims[name] = sorted.length === 1 ? sorted[0] : sorted;
|
|
172
|
+
}
|
|
173
|
+
if (issuer === "vercel" && (claims.project_id === void 0 || claims.environment === void 0)) throw invalidGrant("a Vercel grant names project_id and environment: a preview runs any branch, so it says outright whether they get in");
|
|
174
|
+
if (issuer === "github" && claims.repository === void 0 && claims.repository_id === void 0) throw invalidGrant("a GitHub grant names repository or repository_id: GitHub signs tokens for every repository");
|
|
175
|
+
const can = spec.can ?? DEFAULT_CAN;
|
|
176
|
+
try {
|
|
177
|
+
compileAccess(can);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw invalidGrant(error.message);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
issuer,
|
|
183
|
+
claims,
|
|
184
|
+
can,
|
|
185
|
+
key: JSON.stringify([issuer, claims])
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Whether a verified token's facts are a grant's: its issuer, and every claim it names. */
|
|
189
|
+
function grantMatches(grant, facts) {
|
|
190
|
+
if (!(grant.issuer === "vercel" || grant.issuer === "github" ? facts.issuer === grant.issuer : facts.issuer === "oidc" && facts.iss === grant.issuer)) return false;
|
|
191
|
+
return Object.entries(grant.claims).every(([name, allowed]) => {
|
|
192
|
+
const value = facts.claims[name];
|
|
193
|
+
if (value === void 0) return false;
|
|
194
|
+
const text = String(value);
|
|
195
|
+
return typeof allowed === "string" ? text === allowed : allowed.includes(text);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/** The OIDC issuers grants name past Vercel and GitHub: their tokens are verified too. */
|
|
199
|
+
const grantIssuers = (grants) => [...new Set(grants.flatMap((grant) => grant.issuer === "vercel" || grant.issuer === "github" ? [] : [grant.issuer]))];
|
|
325
200
|
/**
|
|
326
201
|
* What principals may do, under a board's rules: `vercelProject()`, `githubActions()`, `oidc()`,
|
|
327
|
-
* and functions of your own, the first that matches deciding,
|
|
328
|
-
* board's own deployments in its environment, may do
|
|
329
|
-
* principal.
|
|
202
|
+
* and functions of your own, the first that matches deciding, with the board's grants adding to it,
|
|
203
|
+
* past the defaults (people, and the board's own deployments in its environment, may do
|
|
204
|
+
* everything). Or a host's own function of the principal.
|
|
330
205
|
*/
|
|
331
206
|
function createAuthority(options) {
|
|
332
207
|
const rules = typeof options.access === "function" ? [] : options.access;
|
|
@@ -334,13 +209,14 @@ function createAuthority(options) {
|
|
|
334
209
|
const oidcIssuers = trustedIssuers(issuersOf(rules));
|
|
335
210
|
/**
|
|
336
211
|
* What the rules give a principal, in order: a token's rule when its facts match, a function when
|
|
337
|
-
* it answers.
|
|
338
|
-
*
|
|
339
|
-
*
|
|
212
|
+
* it answers. Every grant that matches its token adds to that, so a grant never goes unused
|
|
213
|
+
* because a rule matched first. The board's own deployments in its environment get everything
|
|
214
|
+
* unless a token's rule says otherwise, no function is asked about them, and no grant is theirs;
|
|
215
|
+
* a function that answers null refuses, grants or not. A 401 or 403 that says which rule would
|
|
216
|
+
* let it in, when nothing does.
|
|
340
217
|
*/
|
|
341
218
|
async function byRules(principal, facts) {
|
|
342
219
|
if (facts?.issuer === "host") throw forbidden("this token can't use this board: its board no longer takes a host's principals");
|
|
343
|
-
if (facts?.issuer === "oidc" && !oidcIssuers.has(facts.iss)) throw unauthorized(`untrusted issuer: ${facts.iss}`);
|
|
344
220
|
const claims = facts?.claims ?? {};
|
|
345
221
|
const own = facts?.issuer === "vercel" ? await options.project() : null;
|
|
346
222
|
const context = own ? {
|
|
@@ -354,18 +230,32 @@ function createAuthority(options) {
|
|
|
354
230
|
const here = boardEnvironment();
|
|
355
231
|
const ownDeployment = own !== null && own.id !== null && claims.project_id === own.id;
|
|
356
232
|
const ownHere = ownDeployment && here !== null && environment === here;
|
|
233
|
+
let ruled = null;
|
|
357
234
|
for (const rule of rules) {
|
|
358
235
|
if (typeof rule === "function") {
|
|
359
236
|
if (ownHere) continue;
|
|
360
237
|
const can = await rule(principal);
|
|
361
238
|
if (can === void 0) continue;
|
|
362
239
|
if (can === null) throw forbidden("this token can't use this board");
|
|
363
|
-
|
|
240
|
+
ruled = compileAccess(can);
|
|
241
|
+
break;
|
|
364
242
|
}
|
|
365
|
-
if (facts && rule.issuer === facts.issuer && (rule.issuer !== "oidc" || rule.url === facts.iss) && rule.matches(claims, context))
|
|
243
|
+
if (facts && rule.issuer === facts.issuer && (rule.issuer !== "oidc" || rule.url === facts.iss) && rule.matches(claims, context)) {
|
|
244
|
+
ruled = rule.access;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (ownHere) return ruled ?? FULL_ACCESS;
|
|
249
|
+
if (!facts) {
|
|
250
|
+
if (ruled) return ruled;
|
|
251
|
+
throw forbidden("this token can't use this board");
|
|
366
252
|
}
|
|
367
|
-
|
|
368
|
-
|
|
253
|
+
const grants = options.grants ? await options.grants() : [];
|
|
254
|
+
const granted = grants.filter((grant) => grantMatches(grant, facts)).map((grant) => compileAccess(grant.can));
|
|
255
|
+
if (ruled) return granted.length > 0 ? union([ruled, ...granted]) : ruled;
|
|
256
|
+
if (granted.length > 0) return union(granted);
|
|
257
|
+
if (facts.issuer === "oidc" && !oidcIssuers.has(facts.iss) && !grants.some((grant) => grant.issuer === facts.iss)) throw unauthorized(`untrusted issuer: ${facts.iss}`);
|
|
258
|
+
if (facts.issuer === "oidc" && !oidcIssuers.has(facts.iss)) throw forbidden("this token can't use this board: no grant lets in its claims");
|
|
369
259
|
if (facts.issuer === "oidc") throw notInAccess(`oidc({ issuer: '${facts.iss}', claims, can })`);
|
|
370
260
|
if (facts.issuer === "github") throw notInAccess(`githubActions({ repository: '${String(claims.repository)}', can })`);
|
|
371
261
|
if (ownDeployment) throw forbidden(`this token can't use this board: it runs in ${here ?? "no Vercel environment"}, so its ${environment || "unnamed"} deployments need vercelProject({ project: 'self', environments: ['${environment}'], can })`);
|
|
@@ -496,4 +386,4 @@ function hostPrincipal(value) {
|
|
|
496
386
|
};
|
|
497
387
|
}
|
|
498
388
|
//#endregion
|
|
499
|
-
export {
|
|
389
|
+
export { wroteAs as C, verifiesTokens as S, oidc as _, VERCEL_ISSUER as a, trustedIssuers as b, compileRules as c, githubActions as d, grantIssuers as f, markVerified as g, issuersOf as h, TOKENS as i, createAuthority as l, issuerName as m, DEFAULT_CAN as n, authenticates as o, hostPrincipal as p, GITHUB_ISSUER as r, checkGrant as s, BOARD_TOKEN_PREFIX as t, fromTokens as u, privateHost as v, vercelProject as x, tokenHash as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as Can, t as Access } from "./access-
|
|
1
|
+
import { r as Can, t as Access } from "./access-DPcrKIH_.mjs";
|
|
2
2
|
import { JWTPayload, JWTVerifyGetKey } from "jose";
|
|
3
3
|
//#region src/server/auth.d.ts
|
|
4
4
|
/**
|
|
@@ -46,6 +46,22 @@ type Principal = {
|
|
|
46
46
|
facts?: TokenFacts;
|
|
47
47
|
/** The `x-board-scope` a proxy sent, which narrows whatever it may do. */
|
|
48
48
|
scope?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The board token it came with (its ID), kept with what it stores: a schedule or a registration
|
|
51
|
+
* made with one stops when the token does.
|
|
52
|
+
*/
|
|
53
|
+
token?: string;
|
|
54
|
+
/** What a board token pins for every request with it: the profile, avatar, and session it writes as, and a person's handle and name. */
|
|
55
|
+
pinned?: Pinned;
|
|
56
|
+
};
|
|
57
|
+
/** What a board token says about who writes, beside its principal: labels, but the board's, so a request can't change them. */
|
|
58
|
+
type Pinned = {
|
|
59
|
+
profile: string | null;
|
|
60
|
+
avatar: string | null;
|
|
61
|
+
session: string | null;
|
|
62
|
+
/** A person's handle and display name as they were when they minted it: whoever holds the token sends none. */
|
|
63
|
+
handle: string | null;
|
|
64
|
+
name: string | null;
|
|
49
65
|
};
|
|
50
66
|
/** A principal and what it may do in each channel, as board()'s `access` worked it out. */
|
|
51
67
|
type Granted = Principal & {
|
|
@@ -154,8 +170,20 @@ type AccessFunction = (principal: Known) => Can | null | undefined | Promise<Can
|
|
|
154
170
|
* person or the board's own deployments in its environment, which the defaults decide first.
|
|
155
171
|
*/
|
|
156
172
|
type RuleFunction = (principal: Known) => Can | null | undefined | Promise<Can | null | undefined>;
|
|
157
|
-
/** A principal as the board keeps it to work its access out again: who it is, its token's facts, and
|
|
158
|
-
type Known = Pick<Principal, 'id' | 'kind' | 'handle' | 'facts' | 'scope'>;
|
|
173
|
+
/** A principal as the board keeps it to work its access out again: who it is, its token's facts, its scope, and the board token it came with. */
|
|
174
|
+
type Known = Pick<Principal, 'id' | 'kind' | 'handle' | 'facts' | 'scope' | 'token'>;
|
|
175
|
+
/** A grant the board keeps: its ID, its issuer and claims as the board checked them, and who set it when. */
|
|
176
|
+
type StoredGrant = {
|
|
177
|
+
id: string;
|
|
178
|
+
issuer: string;
|
|
179
|
+
claims: Readonly<Record<string, string | readonly string[]>>;
|
|
180
|
+
can: Can;
|
|
181
|
+
/** The actor that last set it, and when. */
|
|
182
|
+
by: string;
|
|
183
|
+
at: string;
|
|
184
|
+
};
|
|
185
|
+
/** What a grant or a token left without `can` gives: what Vercel's API gives a project it lets use a board. */
|
|
186
|
+
declare const DEFAULT_CAN: Can;
|
|
159
187
|
/**
|
|
160
188
|
* What a principal may do under `rules` (`vercelProject()`, `githubActions()`, `oidc()`, and
|
|
161
189
|
* functions of your own), the first that matches deciding, past the defaults: people, and the
|
|
@@ -201,4 +229,4 @@ type HostPrincipal = {
|
|
|
201
229
|
expiresAt?: Date | null;
|
|
202
230
|
};
|
|
203
231
|
//#endregion
|
|
204
|
-
export {
|
|
232
|
+
export { StoredGrant as _, CallerRequest as a, oidc as b, Granted as c, Known as d, OidcTokensOptions as f, RuleFunction as g, PrincipalKind as h, AuthenticateContext as i, HostPrincipal as l, Principal as m, AccessRule as n, Claims as o, Pinned as p, Authenticate as r, DEFAULT_CAN as s, AccessFunction as t, IssuerConfig as u, compileRules as v, vercelProject as x, githubActions as y };
|