@vercel/board 0.0.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +180 -0
  2. package/dist/a2-C41hD8SF.mjs +3045 -0
  3. package/dist/a2-D24cvwpN.d.mts +3359 -0
  4. package/dist/access-CJU2-vaa.mjs +226 -0
  5. package/dist/access-CV96SYgr.d.mts +32 -0
  6. package/dist/access.d.mts +30 -0
  7. package/dist/access.mjs +4 -0
  8. package/dist/actors-DjMeZpnU.mjs +224 -0
  9. package/dist/attempts-Bxp9KgPf.mjs +919 -0
  10. package/dist/auth-COzc09sn.mjs +276 -0
  11. package/dist/auth-CU56Q2Xh.d.mts +204 -0
  12. package/dist/backend-a2/blob-vercel.d.mts +8 -0
  13. package/dist/backend-a2/blob-vercel.mjs +81 -0
  14. package/dist/backend-a2/contract.d.mts +18 -0
  15. package/dist/backend-a2/contract.mjs +5 -0
  16. package/dist/backend-a2/scheduler-local.d.mts +11 -0
  17. package/dist/backend-a2/scheduler-local.mjs +2 -0
  18. package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
  19. package/dist/backend-a2/scheduler-qstash.mjs +2 -0
  20. package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
  21. package/dist/backend-a2/scheduler-vercel.mjs +2 -0
  22. package/dist/backend-a2/store-memory.d.mts +11 -0
  23. package/dist/backend-a2/store-memory.mjs +19 -0
  24. package/dist/backend-a2/store-postgres.d.mts +38 -0
  25. package/dist/backend-a2/store-postgres.mjs +110 -0
  26. package/dist/backend-a2/store-redis-http.d.mts +17 -0
  27. package/dist/backend-a2/store-redis-http.mjs +32 -0
  28. package/dist/backend-a2/store-redis.d.mts +23 -0
  29. package/dist/backend-a2/store-redis.mjs +39 -0
  30. package/dist/backend-a2/store-sqlite.d.mts +12 -0
  31. package/dist/backend-a2/store-sqlite.mjs +31 -0
  32. package/dist/backend-a2.d.mts +4 -0
  33. package/dist/backend-a2.mjs +2 -0
  34. package/dist/backend-http-0roEmjl0.mjs +613 -0
  35. package/dist/backend-http-BVRno-ca.mjs +469 -0
  36. package/dist/backend-http-Bid7RIpC.d.mts +630 -0
  37. package/dist/backend-http.d.mts +10 -0
  38. package/dist/backend-http.mjs +2 -0
  39. package/dist/backend-http.node.d.mts +10 -0
  40. package/dist/backend-http.node.mjs +2 -0
  41. package/dist/blob-C0J3wbGv.d.mts +39 -0
  42. package/dist/bots-CGkO76qH.mjs +67 -0
  43. package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
  44. package/dist/chat-provider-jFSeyNmB.mjs +389 -0
  45. package/dist/cli.d.mts +1 -0
  46. package/dist/cli.mjs +2252 -0
  47. package/dist/client-DGDPaN7U.mjs +380 -0
  48. package/dist/conformance-DRR23YNy.mjs +512 -0
  49. package/dist/conformance.d.mts +30 -0
  50. package/dist/conformance.mjs +2 -0
  51. package/dist/contract-CuJmlrhw.mjs +566 -0
  52. package/dist/emoji-CTLpvH-A.mjs +445 -0
  53. package/dist/engine-Bir5s8ts.mjs +1449 -0
  54. package/dist/errors-DX3hpwWt.mjs +64 -0
  55. package/dist/exports-DLiE7fLY.d.mts +4 -0
  56. package/dist/exports-Dvc1o52I.mjs +1285 -0
  57. package/dist/files-SniSmeEr.mjs +47 -0
  58. package/dist/index.d.mts +13 -0
  59. package/dist/index.default-Bkg77Pxn.mjs +12 -0
  60. package/dist/index.mjs +19 -0
  61. package/dist/index.node.d.mts +14 -0
  62. package/dist/index.node.mjs +5 -0
  63. package/dist/inspect/app.js +416 -0
  64. package/dist/inspect/engine.js +5 -0
  65. package/dist/local-Cr_dSjvs.mjs +44 -0
  66. package/dist/own-OwgPWdmh.mjs +15 -0
  67. package/dist/project-CFiArraX.mjs +124 -0
  68. package/dist/provider-BGjz-mDa.d.mts +2473 -0
  69. package/dist/provider-CuewcDNZ.mjs +205 -0
  70. package/dist/redis-tables-B35vv4CZ.mjs +842 -0
  71. package/dist/reducers-B_vUGQRp.d.mts +1172 -0
  72. package/dist/reducers-mUPFuFJV.mjs +293 -0
  73. package/dist/schemas-Be08nyfW.mjs +74 -0
  74. package/dist/schemas-CWxs3ECJ.d.mts +50 -0
  75. package/dist/serve-BsOOc-6_.mjs +170 -0
  76. package/dist/server-jwPY7_wi.mjs +17 -0
  77. package/dist/serving-BNbzc96L.mjs +2 -0
  78. package/dist/sql-DbiEnqM0.mjs +571 -0
  79. package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
  80. package/dist/sync-github.d.mts +473 -0
  81. package/dist/sync-github.mjs +603 -0
  82. package/dist/sync-kapso.d.mts +175 -0
  83. package/dist/sync-kapso.mjs +86 -0
  84. package/dist/sync-slack.d.mts +364 -0
  85. package/dist/sync-slack.mjs +728 -0
  86. package/dist/sync.chat.d.mts +2 -0
  87. package/dist/sync.chat.mjs +2 -0
  88. package/dist/sync.d.mts +3 -0
  89. package/dist/sync.mjs +4 -0
  90. package/dist/targets-yM5JBUgr.mjs +94 -0
  91. package/dist/tokens-DKLn5P9B.mjs +256 -0
  92. package/dist/types-BHCjTZdB.d.mts +104 -0
  93. package/dist/users-Dls7_ANV.mjs +42 -0
  94. package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
  95. package/dist/watchers-hash-CLjsa6od.mjs +225 -0
  96. package/docs/01-board/01-mounting.md +506 -0
  97. package/docs/01-board/02-database.md +258 -0
  98. package/docs/01-board/03-identity.md +264 -0
  99. package/docs/01-board/04-messages.md +192 -0
  100. package/docs/01-board/05-reading.md +177 -0
  101. package/docs/01-board/06-watchers.md +249 -0
  102. package/docs/01-board/07-schedules.md +133 -0
  103. package/docs/01-board/08-middleware.md +139 -0
  104. package/docs/01-board/09-access.md +501 -0
  105. package/docs/01-quickstart.md +196 -0
  106. package/docs/02-sync/01-overview.md +290 -0
  107. package/docs/02-sync/02-callbacks.md +265 -0
  108. package/docs/02-sync/03-slack.md +248 -0
  109. package/docs/02-sync/04-github.md +201 -0
  110. package/docs/02-sync/05-whatsapp.md +148 -0
  111. package/docs/02-sync/06-providers.md +228 -0
  112. package/docs/03-guides/01-coding-agents.md +226 -0
  113. package/docs/03-guides/02-inspector.md +104 -0
  114. package/docs/03-guides/03-ci.md +251 -0
  115. package/docs/03-guides/04-triage.md +233 -0
  116. package/docs/03-guides/05-agent-factory.md +142 -0
  117. package/docs/04-reference/01-cli.md +606 -0
  118. package/docs/04-reference/02-board.md +579 -0
  119. package/docs/04-reference/03-http.md +489 -0
  120. package/docs/04-reference/04-log.md +250 -0
  121. package/docs/04-reference/05-conformance.md +97 -0
  122. package/docs/index.md +186 -0
  123. package/package.json +168 -1
  124. package/sql/postgres/board.sql +145 -0
  125. package/sql/postgres/notifications.sql +24 -0
  126. package/sql/postgres/schema.sql +451 -0
@@ -0,0 +1,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,30 @@
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-CU56Q2Xh.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 name is what they claim, and so is their handle unless a host's verifier named them;
25
+ * anyone else's are their principal's. `audience` is the board's URL, which serves the known
26
+ * agents' avatars.
27
+ */
28
+ export declare function resolveActor(principal: Principal, claims: Claims, audience: string): ResolvedActor;
29
+ //#endregion
30
+ 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 };
@@ -0,0 +1,4 @@
1
+ import { i as resolveActor } from "./actors-DjMeZpnU.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,224 @@
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 name is what they claim, and so is their handle unless a host's verifier named them;
126
+ * anyone else's are their principal's. `audience` is the board's URL, which serves the known
127
+ * agents' avatars.
128
+ */
129
+ function resolveActor(principal, claims, audience) {
130
+ const baseHandle = principal.kind === "person" && principal.facts?.issuer !== "host" && claims.handle ? claims.handle : principal.handle;
131
+ const personName = principal.kind === "person" ? claims.name : null;
132
+ const profile = claims.profile?.toLowerCase() ?? null;
133
+ const id = profile === null ? principal.id : `${principal.id}+${profile}`;
134
+ const known = profile !== null && KNOWN_AGENTS.includes(profile);
135
+ const actor = {
136
+ id,
137
+ kind: profile === null ? principal.kind : "agent",
138
+ handle: profile === null ? baseHandle : `${baseHandle}/${profile}`,
139
+ name: claims.profile ?? personName ?? baseHandle,
140
+ avatar: claims.avatar ?? (known ? `${audience}/avatars/${profile}` : null)
141
+ };
142
+ return {
143
+ author: {
144
+ actor: id,
145
+ principal: principal.id,
146
+ profile,
147
+ session: claims.session
148
+ },
149
+ actor
150
+ };
151
+ }
152
+ function isAvatarUrl(value) {
153
+ try {
154
+ const url = new URL(value);
155
+ return url.protocol === "https:" || url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname);
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+ /**
161
+ * An `actor.seen` event, or null when the board already knows these details. `host`: the actor is
162
+ * a host's principal or one of its profiles, which the board records once.
163
+ */
164
+ function actorSeen(seen, known, host = false) {
165
+ const existing = own(known.byId, seen.id);
166
+ const actor = seen.kind === "external" && seen.avatar === null ? {
167
+ ...seen,
168
+ avatar: existing?.avatar ?? null
169
+ } : seen;
170
+ if (existing && existing.kind === actor.kind && existing.handle === actor.handle && existing.name === actor.name && existing.avatar === actor.avatar && (!host || existing.host)) return null;
171
+ return {
172
+ type: "actor.seen",
173
+ payload: {
174
+ actor: actor.id,
175
+ kind: actor.kind,
176
+ handle: toStorable(actor.handle),
177
+ name: toStorable(actor.name),
178
+ avatar: actor.avatar,
179
+ ...host ? { host: true } : {}
180
+ }
181
+ };
182
+ }
183
+ /**
184
+ * Resolves prose mentions to actor IDs. A `@handle` names the one actor the board
185
+ * has seen with it; `@[actor-id]` names one directly. A handle nobody has stays
186
+ * plain text and comes back in `unknown`, so the writer can be told. A handle
187
+ * several actors share, or an `@[actor-id]` the board doesn't know, fails the
188
+ * write. `lenient` is for platform text: no mention fails it, and one that
189
+ * doesn't resolve stays plain text. Without `candidates`, an ambiguous mention
190
+ * doesn't list who it could be: a caller that can't read every channel can't
191
+ * list actors either.
192
+ */
193
+ function resolveMentions(text, known, pending, lenient = false, candidates = true) {
194
+ const resolved = [];
195
+ const unknown = [];
196
+ const add = (id) => {
197
+ if (!resolved.includes(id)) resolved.push(id);
198
+ };
199
+ const fail = (error) => {
200
+ if (!lenient) throw error;
201
+ };
202
+ for (const token of extractMentions(text)) {
203
+ if (token.kind === "actor") {
204
+ if (isActorId(token.actor) && own(known.byId, token.actor) || pending?.id === token.actor) add(token.actor);
205
+ else fail(badRequest(`unknown actor @[${token.actor}]`, "unknown_mention"));
206
+ continue;
207
+ }
208
+ const ids = new Set(own(known.byHandle, token.handle) ?? []);
209
+ if (pending && pending.handle.toLowerCase() === token.handle) ids.add(pending.id);
210
+ if (ids.size === 1) add([...ids][0]);
211
+ else if (ids.size === 0) unknown.push(token.handle);
212
+ else if (!candidates) fail(badRequest(`ambiguous mention @${token.handle}: use @[actor-id]`, "ambiguous_mention"));
213
+ else {
214
+ const options = [...ids].map((id) => `@[${id}] (${own(known.byId, id)?.name ?? pending?.name ?? id})`).join(" or ");
215
+ fail(badRequest(`ambiguous mention @${token.handle}: use ${options}`, "ambiguous_mention"));
216
+ }
217
+ }
218
+ return {
219
+ mentions: resolved,
220
+ unknown: lenient ? [] : unknown
221
+ };
222
+ }
223
+ //#endregion
224
+ export { resolveMentions as a, resolveActor as i, actorSeen as n, extractMentions as o, claimsOf as r, replaceMentions as s, KNOWN_AGENTS as t };