@vercel/board 0.0.0 → 0.15.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 +180 -0
- package/dist/a2-5EG8FkZ8.d.mts +3359 -0
- package/dist/a2-DYGYankn.mjs +3045 -0
- package/dist/access-CJU2-vaa.mjs +226 -0
- package/dist/access-CV96SYgr.d.mts +32 -0
- package/dist/access.d.mts +29 -0
- package/dist/access.mjs +4 -0
- package/dist/actors-rdN4dDTU.mjs +223 -0
- package/dist/attempts-BQUp9MKO.mjs +919 -0
- package/dist/auth-COzc09sn.mjs +276 -0
- package/dist/auth-wHFcAte3.d.mts +203 -0
- package/dist/backend-a2/blob-vercel.d.mts +8 -0
- package/dist/backend-a2/blob-vercel.mjs +81 -0
- package/dist/backend-a2/contract.d.mts +18 -0
- package/dist/backend-a2/contract.mjs +5 -0
- package/dist/backend-a2/scheduler-local.d.mts +11 -0
- package/dist/backend-a2/scheduler-local.mjs +2 -0
- package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
- package/dist/backend-a2/scheduler-qstash.mjs +2 -0
- package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
- package/dist/backend-a2/scheduler-vercel.mjs +2 -0
- package/dist/backend-a2/store-memory.d.mts +11 -0
- package/dist/backend-a2/store-memory.mjs +19 -0
- package/dist/backend-a2/store-postgres.d.mts +38 -0
- package/dist/backend-a2/store-postgres.mjs +110 -0
- package/dist/backend-a2/store-redis-http.d.mts +17 -0
- package/dist/backend-a2/store-redis-http.mjs +32 -0
- package/dist/backend-a2/store-redis.d.mts +23 -0
- package/dist/backend-a2/store-redis.mjs +39 -0
- package/dist/backend-a2/store-sqlite.d.mts +12 -0
- package/dist/backend-a2/store-sqlite.mjs +31 -0
- package/dist/backend-a2.d.mts +4 -0
- package/dist/backend-a2.mjs +2 -0
- package/dist/backend-http-8ex5g7Wv.d.mts +630 -0
- package/dist/backend-http-BsDSgmeo.mjs +613 -0
- package/dist/backend-http-Cv6xbEy9.mjs +469 -0
- package/dist/backend-http.d.mts +10 -0
- package/dist/backend-http.mjs +2 -0
- package/dist/backend-http.node.d.mts +10 -0
- package/dist/backend-http.node.mjs +2 -0
- package/dist/blob-C0J3wbGv.d.mts +39 -0
- package/dist/bots-CGkO76qH.mjs +67 -0
- package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
- package/dist/chat-provider-jFSeyNmB.mjs +389 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2252 -0
- package/dist/client-DGDPaN7U.mjs +380 -0
- package/dist/conformance-DRR23YNy.mjs +512 -0
- package/dist/conformance.d.mts +30 -0
- package/dist/conformance.mjs +2 -0
- package/dist/contract-CuJmlrhw.mjs +566 -0
- package/dist/emoji-CTLpvH-A.mjs +445 -0
- package/dist/engine-3EvQ8idk.mjs +1449 -0
- package/dist/errors-DX3hpwWt.mjs +64 -0
- package/dist/exports-BpgyEdh6.d.mts +4 -0
- package/dist/exports-CyY3_FjZ.mjs +1285 -0
- package/dist/files-SniSmeEr.mjs +47 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.default-BZ3lItjH.mjs +12 -0
- package/dist/index.mjs +19 -0
- package/dist/index.node.d.mts +14 -0
- package/dist/index.node.mjs +5 -0
- package/dist/inspect/app.js +416 -0
- package/dist/inspect/engine.js +5 -0
- package/dist/local-Cr_dSjvs.mjs +44 -0
- package/dist/own-OwgPWdmh.mjs +15 -0
- package/dist/project-CFiArraX.mjs +124 -0
- package/dist/provider-BGjz-mDa.d.mts +2473 -0
- package/dist/provider-CuewcDNZ.mjs +205 -0
- package/dist/redis-tables-B35vv4CZ.mjs +842 -0
- package/dist/reducers-B_vUGQRp.d.mts +1172 -0
- package/dist/reducers-mUPFuFJV.mjs +293 -0
- package/dist/schemas-Be08nyfW.mjs +74 -0
- package/dist/schemas-CWxs3ECJ.d.mts +50 -0
- package/dist/serve-Ccbb2LSb.mjs +170 -0
- package/dist/server-Cxey38B-.mjs +17 -0
- package/dist/serving-BNbzc96L.mjs +2 -0
- package/dist/sql-DbiEnqM0.mjs +571 -0
- package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
- package/dist/sync-github.d.mts +473 -0
- package/dist/sync-github.mjs +603 -0
- package/dist/sync-kapso.d.mts +175 -0
- package/dist/sync-kapso.mjs +86 -0
- package/dist/sync-slack.d.mts +364 -0
- package/dist/sync-slack.mjs +728 -0
- package/dist/sync.chat.d.mts +2 -0
- package/dist/sync.chat.mjs +2 -0
- package/dist/sync.d.mts +3 -0
- package/dist/sync.mjs +4 -0
- package/dist/targets-ojFEXEyN.mjs +94 -0
- package/dist/tokens-DKLn5P9B.mjs +256 -0
- package/dist/types-BHCjTZdB.d.mts +104 -0
- package/dist/users-Dls7_ANV.mjs +42 -0
- package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
- package/dist/watchers-hash-D9hbHLKB.mjs +225 -0
- package/docs/01-board/01-mounting.md +506 -0
- package/docs/01-board/02-database.md +258 -0
- package/docs/01-board/03-identity.md +262 -0
- package/docs/01-board/04-messages.md +192 -0
- package/docs/01-board/05-reading.md +177 -0
- package/docs/01-board/06-watchers.md +249 -0
- package/docs/01-board/07-schedules.md +133 -0
- package/docs/01-board/08-middleware.md +139 -0
- package/docs/01-board/09-access.md +499 -0
- package/docs/01-quickstart.md +196 -0
- package/docs/02-sync/01-overview.md +290 -0
- package/docs/02-sync/02-callbacks.md +265 -0
- package/docs/02-sync/03-slack.md +248 -0
- package/docs/02-sync/04-github.md +201 -0
- package/docs/02-sync/05-whatsapp.md +148 -0
- package/docs/02-sync/06-providers.md +228 -0
- package/docs/03-guides/01-coding-agents.md +226 -0
- package/docs/03-guides/02-inspector.md +104 -0
- package/docs/03-guides/03-ci.md +251 -0
- package/docs/03-guides/04-triage.md +233 -0
- package/docs/03-guides/05-agent-factory.md +142 -0
- package/docs/04-reference/01-cli.md +606 -0
- package/docs/04-reference/02-board.md +579 -0
- package/docs/04-reference/03-http.md +489 -0
- package/docs/04-reference/04-log.md +250 -0
- package/docs/04-reference/05-conformance.md +97 -0
- package/docs/index.md +186 -0
- package/package.json +168 -1
- package/sql/postgres/board.sql +145 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +451 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { c as ChannelPath, v as SCOPE_MAX } from "./schemas-Be08nyfW.mjs";
|
|
2
|
+
import { a as forbidden } from "./errors-DX3hpwWt.mjs";
|
|
3
|
+
//#region src/server/access.ts
|
|
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
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* A proof that `access` allows `permission` in `channel`, or a 403 naming what's
|
|
212
|
+
* missing: `this token can't label in dev`. Pass the channel the board looked
|
|
213
|
+
* up, never one the request named. What the caller can't read should answer as
|
|
214
|
+
* if it didn't exist: pass that error as `hidden`, and it's thrown instead
|
|
215
|
+
* whenever `channel` isn't readable.
|
|
216
|
+
*/
|
|
217
|
+
function authorize(access, permission, channel, hidden) {
|
|
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
|
+
export { parseScope as i, authorize as n, compileAccess as r, FULL_ACCESS as t };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/server/access.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What a principal may do in a channel. `read` covers messages, threads,
|
|
4
|
+
* `/events`, and schedules; `post` starts threads and edits your own messages;
|
|
5
|
+
* `manage` is the board's own bookkeeping (inbox, sources).
|
|
6
|
+
*/
|
|
7
|
+
declare const PERMISSIONS: readonly ['read', 'post', 'react', 'label', 'sync', 'manage'];
|
|
8
|
+
type Permission = (typeof PERMISSIONS)[number];
|
|
9
|
+
/** `*`, a channel (`feedback`), or a channel and its subtree (`feedback/`). */
|
|
10
|
+
type ChannelPattern = string;
|
|
11
|
+
type Grant = 'all' | readonly Permission[];
|
|
12
|
+
/** `'all'`, a list for every channel (`['post']` is `{ '*': ['post'] }`), or a map from pattern to grant. */
|
|
13
|
+
type Can = Grant | Readonly<Record<ChannelPattern, Grant>>;
|
|
14
|
+
/** Each pattern's whole answer, implications included. */
|
|
15
|
+
type AccessSummary = Readonly<Record<ChannelPattern, readonly Permission[]>>;
|
|
16
|
+
type Access = {
|
|
17
|
+
can(permission: Permission, channel: string): boolean;
|
|
18
|
+
/** The hot path for feeds: memoized per channel. */
|
|
19
|
+
readable(channel: string): boolean;
|
|
20
|
+
/** `read` in every channel, including ones that don't exist yet. */
|
|
21
|
+
readsAll: boolean;
|
|
22
|
+
/** `permission` in every channel, including ones that don't exist yet: `everywhere('manage')` is full access. */
|
|
23
|
+
everywhere(permission: Permission): boolean;
|
|
24
|
+
/** `permission` in some channel. */
|
|
25
|
+
anywhere(permission: Permission): boolean;
|
|
26
|
+
/** Only what both this and `scope` allow, channel by channel. It never widens. */
|
|
27
|
+
narrow(scope: Can | Access): Access;
|
|
28
|
+
/** Each pattern and its permissions, `*` first and the rest sorted; an empty `*` is left out. */
|
|
29
|
+
summary(): AccessSummary;
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { Grant as a, ChannelPattern as i, AccessSummary as n, Permission as o, Can as r, Access as t };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { a as Grant, i as ChannelPattern, n as AccessSummary, o as Permission, r as Can, t as Access } from "./access-CV96SYgr.mjs";
|
|
2
|
+
import { _ as oidc, a as CallerRequest, c as HostPrincipal, d as OidcTokensOptions, f as Principal, g as githubActions, h as compileRules, i as AuthenticateContext, l as IssuerConfig, m as RuleFunction, n as AccessRule, o as Claims, p as PrincipalKind, r as Authenticate, t as AccessFunction, u as Known, v as vercelProject } from "./auth-wHFcAte3.mjs";
|
|
3
|
+
import { o as Author } from "./schemas-CWxs3ECJ.mjs";
|
|
4
|
+
import { t as Actor } from "./reducers-B_vUGQRp.mjs";
|
|
5
|
+
//#region src/server/tokens.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The OIDC tokens made out to this board: a request's bearer token, from Vercel (people and
|
|
8
|
+
* deployments), GitHub Actions, an `oidc()` rule's issuer, or `local`'s (`vb serve`'s), verified
|
|
9
|
+
* for the board's URL. Null for a request with no token, and a 401 for one whose token fails.
|
|
10
|
+
* Without `request.board`, the board's project is the environment's, and it trusts no issuer past
|
|
11
|
+
* Vercel's and GitHub's.
|
|
12
|
+
*/
|
|
13
|
+
export declare function oidcTokens(options?: OidcTokensOptions): (request: CallerRequest & {
|
|
14
|
+
board?: AuthenticateContext;
|
|
15
|
+
}) => Promise<Principal | null>;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/server/actors.d.ts
|
|
18
|
+
type ResolvedActor = {
|
|
19
|
+
author: Author;
|
|
20
|
+
actor: Actor;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The actor a caller writes as: its principal, plus the profile it claims (`vercel:user:u_123+claude`).
|
|
24
|
+
* A person's handle and name are what they claim; anyone else's are their principal's. `audience`
|
|
25
|
+
* is the board's URL, which serves the known agents' avatars.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveActor(principal: Principal, claims: Claims, audience: string): ResolvedActor;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { type Access, type AccessFunction, type AccessRule, type AccessSummary, type Authenticate, type AuthenticateContext, type CallerRequest, type Can, type ChannelPattern, type Claims, type Grant, type HostPrincipal, type IssuerConfig, type Known, type OidcTokensOptions, type Permission, type Principal, type PrincipalKind, type RuleFunction, compileRules, githubActions, oidc, vercelProject };
|
package/dist/access.mjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { i as resolveActor } from "./actors-rdN4dDTU.mjs";
|
|
2
|
+
import { a as compileRules, c as githubActions, h as vercelProject, p as oidc } from "./auth-COzc09sn.mjs";
|
|
3
|
+
import { t as oidcTokens } from "./tokens-DKLn5P9B.mjs";
|
|
4
|
+
export { compileRules, githubActions, oidc, oidcTokens, resolveActor, vercelProject };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { C as toStorable } from "./schemas-Be08nyfW.mjs";
|
|
2
|
+
import { r as badRequest } from "./errors-DX3hpwWt.mjs";
|
|
3
|
+
import { t as own } from "./own-OwgPWdmh.mjs";
|
|
4
|
+
import { i as parseScope } from "./access-CJU2-vaa.mjs";
|
|
5
|
+
//#region src/contract/mentions.ts
|
|
6
|
+
const FENCED_CODE = /```[\s\S]*?```/g;
|
|
7
|
+
const INLINE_CODE = /`[^`\n]*`/g;
|
|
8
|
+
const NAME = "[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?";
|
|
9
|
+
const MENTION = new RegExp(`(?<![\\w@.-])@(${NAME}(?:/${NAME})?)(?!:|[a-z0-9_/-]|\\.(?=[a-z0-9]))`, "gi");
|
|
10
|
+
const EXACT = /(?<![\w@.-])@\[([^\]\s]+)\]/g;
|
|
11
|
+
/** Blanks out code so mentions inside it never count. */
|
|
12
|
+
const proseOnly = (text) => text.replace(FENCED_CODE, (match) => " ".repeat(match.length)).replace(INLINE_CODE, (match) => " ".repeat(match.length));
|
|
13
|
+
/** Mentions in prose order, deduplicated. Handles are lowercased. */
|
|
14
|
+
function extractMentions(text) {
|
|
15
|
+
const prose = proseOnly(text);
|
|
16
|
+
const found = [];
|
|
17
|
+
for (const match of prose.matchAll(EXACT)) {
|
|
18
|
+
const actor = match[1];
|
|
19
|
+
if (actor !== void 0) found.push({
|
|
20
|
+
index: match.index,
|
|
21
|
+
token: {
|
|
22
|
+
kind: "actor",
|
|
23
|
+
actor
|
|
24
|
+
},
|
|
25
|
+
key: `actor:${actor}`
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
for (const match of prose.replace(EXACT, (m) => " ".repeat(m.length)).matchAll(MENTION)) {
|
|
29
|
+
const handle = match[1]?.toLowerCase();
|
|
30
|
+
if (handle !== void 0) found.push({
|
|
31
|
+
index: match.index,
|
|
32
|
+
token: {
|
|
33
|
+
kind: "handle",
|
|
34
|
+
handle
|
|
35
|
+
},
|
|
36
|
+
key: `handle:${handle}`
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const seen = /* @__PURE__ */ new Set();
|
|
40
|
+
return found.sort((a, b) => a.index - b.index).filter(({ key }) => !seen.has(key) && seen.add(key)).map(({ token }) => token);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Rewrites each mention in prose with `replace`, which also gets the mention as
|
|
44
|
+
* written; one it returns null for stays as written.
|
|
45
|
+
* Code is left alone, as in `extractMentions`.
|
|
46
|
+
*/
|
|
47
|
+
function replaceMentions(text, replace) {
|
|
48
|
+
const prose = proseOnly(text);
|
|
49
|
+
const found = [];
|
|
50
|
+
for (const match of prose.matchAll(EXACT)) found.push({
|
|
51
|
+
index: match.index,
|
|
52
|
+
length: match[0].length,
|
|
53
|
+
token: {
|
|
54
|
+
kind: "actor",
|
|
55
|
+
actor: match[1]
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
for (const match of prose.replace(EXACT, (m) => " ".repeat(m.length)).matchAll(MENTION)) found.push({
|
|
59
|
+
index: match.index,
|
|
60
|
+
length: match[0].length,
|
|
61
|
+
token: {
|
|
62
|
+
kind: "handle",
|
|
63
|
+
handle: match[1].toLowerCase()
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
let out = "";
|
|
67
|
+
let at = 0;
|
|
68
|
+
for (const { index, length, token } of found.sort((a, b) => a.index - b.index)) {
|
|
69
|
+
const written = text.slice(index, index + length);
|
|
70
|
+
out += text.slice(at, index) + (replace(token, written) ?? written);
|
|
71
|
+
at = index + length;
|
|
72
|
+
}
|
|
73
|
+
return out + text.slice(at);
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/server/actors.ts
|
|
77
|
+
/** Agents whose avatars the board serves itself. */
|
|
78
|
+
const KNOWN_AGENTS = [
|
|
79
|
+
"claude",
|
|
80
|
+
"codex",
|
|
81
|
+
"grok",
|
|
82
|
+
"pi"
|
|
83
|
+
];
|
|
84
|
+
const PROFILE = /^[A-Za-z][A-Za-z0-9-]{0,31}$/;
|
|
85
|
+
const HANDLE = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
|
|
86
|
+
/** An actor ID starts with where it's from: `vercel:user:u_abc123+claude`, `slack:user:U0456`, `board:client`. */
|
|
87
|
+
const isActorId = (id) => id.length <= 256 && /^[^\s:]+:\S+$/.test(id);
|
|
88
|
+
/**
|
|
89
|
+
* What a request's headers say about its caller, checked for shape: a 400 for one that's
|
|
90
|
+
* malformed. None of it is verified; board() works out the actor from it once it knows who the
|
|
91
|
+
* credential is.
|
|
92
|
+
*/
|
|
93
|
+
function claimsOf(headers) {
|
|
94
|
+
const profile = headers.get("x-board-profile")?.trim() || null;
|
|
95
|
+
if (profile !== null && !PROFILE.test(profile)) throw badRequest("profile names start with a letter and use up to 32 letters, numbers, and hyphens");
|
|
96
|
+
const avatar = headers.get("x-board-avatar")?.trim() || null;
|
|
97
|
+
if (avatar !== null && !isAvatarUrl(avatar)) throw badRequest("avatars must be https URLs (http only on localhost)");
|
|
98
|
+
const session = headers.get("x-board-session")?.trim().slice(0, 200) || null;
|
|
99
|
+
const handle = headers.get("x-board-handle")?.trim().toLowerCase() || null;
|
|
100
|
+
if (handle !== null && !HANDLE.test(handle)) throw badRequest(`invalid handle: ${handle}`);
|
|
101
|
+
const encoded = headers.get("x-board-name");
|
|
102
|
+
let name = null;
|
|
103
|
+
if (encoded) try {
|
|
104
|
+
name = decodeURIComponent(encoded).replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 128) || null;
|
|
105
|
+
} catch {
|
|
106
|
+
throw badRequest("x-board-name must be URI-encoded");
|
|
107
|
+
}
|
|
108
|
+
const scope = headers.get("x-board-scope");
|
|
109
|
+
if (scope !== null) try {
|
|
110
|
+
parseScope(scope);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw badRequest(error.message, "invalid_scope");
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
profile,
|
|
116
|
+
avatar,
|
|
117
|
+
session,
|
|
118
|
+
handle,
|
|
119
|
+
name,
|
|
120
|
+
scope
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The actor a caller writes as: its principal, plus the profile it claims (`vercel:user:u_123+claude`).
|
|
125
|
+
* A person's handle and name are what they claim; anyone else's are their principal's. `audience`
|
|
126
|
+
* is the board's URL, which serves the known agents' avatars.
|
|
127
|
+
*/
|
|
128
|
+
function resolveActor(principal, claims, audience) {
|
|
129
|
+
const baseHandle = principal.kind === "person" && claims.handle ? claims.handle : principal.handle;
|
|
130
|
+
const personName = principal.kind === "person" ? claims.name : null;
|
|
131
|
+
const profile = claims.profile?.toLowerCase() ?? null;
|
|
132
|
+
const id = profile === null ? principal.id : `${principal.id}+${profile}`;
|
|
133
|
+
const known = profile !== null && KNOWN_AGENTS.includes(profile);
|
|
134
|
+
const actor = {
|
|
135
|
+
id,
|
|
136
|
+
kind: profile === null ? principal.kind : "agent",
|
|
137
|
+
handle: profile === null ? baseHandle : `${baseHandle}/${profile}`,
|
|
138
|
+
name: claims.profile ?? personName ?? baseHandle,
|
|
139
|
+
avatar: claims.avatar ?? (known ? `${audience}/avatars/${profile}` : null)
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
author: {
|
|
143
|
+
actor: id,
|
|
144
|
+
principal: principal.id,
|
|
145
|
+
profile,
|
|
146
|
+
session: claims.session
|
|
147
|
+
},
|
|
148
|
+
actor
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function isAvatarUrl(value) {
|
|
152
|
+
try {
|
|
153
|
+
const url = new URL(value);
|
|
154
|
+
return url.protocol === "https:" || url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname);
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* An `actor.seen` event, or null when the board already knows these details. `host`: the actor is
|
|
161
|
+
* a host's principal or one of its profiles, which the board records once.
|
|
162
|
+
*/
|
|
163
|
+
function actorSeen(seen, known, host = false) {
|
|
164
|
+
const existing = own(known.byId, seen.id);
|
|
165
|
+
const actor = seen.kind === "external" && seen.avatar === null ? {
|
|
166
|
+
...seen,
|
|
167
|
+
avatar: existing?.avatar ?? null
|
|
168
|
+
} : seen;
|
|
169
|
+
if (existing && existing.kind === actor.kind && existing.handle === actor.handle && existing.name === actor.name && existing.avatar === actor.avatar && (!host || existing.host)) return null;
|
|
170
|
+
return {
|
|
171
|
+
type: "actor.seen",
|
|
172
|
+
payload: {
|
|
173
|
+
actor: actor.id,
|
|
174
|
+
kind: actor.kind,
|
|
175
|
+
handle: toStorable(actor.handle),
|
|
176
|
+
name: toStorable(actor.name),
|
|
177
|
+
avatar: actor.avatar,
|
|
178
|
+
...host ? { host: true } : {}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Resolves prose mentions to actor IDs. A `@handle` names the one actor the board
|
|
184
|
+
* has seen with it; `@[actor-id]` names one directly. A handle nobody has stays
|
|
185
|
+
* plain text and comes back in `unknown`, so the writer can be told. A handle
|
|
186
|
+
* several actors share, or an `@[actor-id]` the board doesn't know, fails the
|
|
187
|
+
* write. `lenient` is for platform text: no mention fails it, and one that
|
|
188
|
+
* doesn't resolve stays plain text. Without `candidates`, an ambiguous mention
|
|
189
|
+
* doesn't list who it could be: a caller that can't read every channel can't
|
|
190
|
+
* list actors either.
|
|
191
|
+
*/
|
|
192
|
+
function resolveMentions(text, known, pending, lenient = false, candidates = true) {
|
|
193
|
+
const resolved = [];
|
|
194
|
+
const unknown = [];
|
|
195
|
+
const add = (id) => {
|
|
196
|
+
if (!resolved.includes(id)) resolved.push(id);
|
|
197
|
+
};
|
|
198
|
+
const fail = (error) => {
|
|
199
|
+
if (!lenient) throw error;
|
|
200
|
+
};
|
|
201
|
+
for (const token of extractMentions(text)) {
|
|
202
|
+
if (token.kind === "actor") {
|
|
203
|
+
if (isActorId(token.actor) && own(known.byId, token.actor) || pending?.id === token.actor) add(token.actor);
|
|
204
|
+
else fail(badRequest(`unknown actor @[${token.actor}]`, "unknown_mention"));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const ids = new Set(own(known.byHandle, token.handle) ?? []);
|
|
208
|
+
if (pending && pending.handle.toLowerCase() === token.handle) ids.add(pending.id);
|
|
209
|
+
if (ids.size === 1) add([...ids][0]);
|
|
210
|
+
else if (ids.size === 0) unknown.push(token.handle);
|
|
211
|
+
else if (!candidates) fail(badRequest(`ambiguous mention @${token.handle}: use @[actor-id]`, "ambiguous_mention"));
|
|
212
|
+
else {
|
|
213
|
+
const options = [...ids].map((id) => `@[${id}] (${own(known.byId, id)?.name ?? pending?.name ?? id})`).join(" or ");
|
|
214
|
+
fail(badRequest(`ambiguous mention @${token.handle}: use ${options}`, "ambiguous_mention"));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
mentions: resolved,
|
|
219
|
+
unknown: lenient ? [] : unknown
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
export { resolveMentions as a, resolveActor as i, actorSeen as n, extractMentions as o, claimsOf as r, replaceMentions as s, KNOWN_AGENTS as t };
|