@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,276 @@
1
+ import { a as forbidden, l as unauthorized } from "./errors-DX3hpwWt.mjs";
2
+ import { i as parseScope, r as compileAccess, t as FULL_ACCESS } from "./access-CJU2-vaa.mjs";
3
+ //#region src/server/auth.ts
4
+ /**
5
+ * Principal IDs a board authenticates: its people, deployments, workflows, and its own code. No
6
+ * platform actor sync writes as may have one, or an import could pass for someone signed in.
7
+ */
8
+ const authenticates = (id) => /^(vercel|oidc|local|board):/.test(id) || id.startsWith("github:repo:");
9
+ /** A token no rule matches, and the rule that would let it in. */
10
+ const notInAccess = (rule) => forbidden(`this token can't use this board: add ${rule} to access`);
11
+ /** A rule's `can`, compiled. A rule without one says which rule and what to write. */
12
+ function accessOf(rule, can) {
13
+ if (can === void 0 || can === null) throw new Error(`${rule} needs can: say what it may do, like can: { 'ci/': ['read', 'post'] }, or can: 'all'`);
14
+ return compileAccess(can);
15
+ }
16
+ const oneOf = (value, allowed) => allowed === void 0 || typeof value === "string" && (typeof allowed === "string" ? [allowed] : allowed).includes(value);
17
+ /**
18
+ * A Vercel project's deployments, by name or ID. A name is only unique within
19
+ * a team, so without `team` it means the board's own team, and matches nothing
20
+ * when that isn't known. An ID is unique on Vercel, so it matches on any team.
21
+ * `'self'` is the board's own project: its deployments in the environment the
22
+ * board runs in have full access unless a rule says otherwise, and its other
23
+ * environments have none unless one lists them. Its development tokens are
24
+ * people, which no rule changes.
25
+ */
26
+ function vercelProject(options) {
27
+ if (options.project === "self" && options.team !== void 0) throw new Error("vercelProject({ project: 'self' }) is the board's own project: drop team");
28
+ return {
29
+ issuer: "vercel",
30
+ access: accessOf(`vercelProject({ project: '${options.project}' })`, options.can),
31
+ matches: (claims, { teamId, projectId }) => (options.project === "self" ? projectId !== null && claims.project_id === projectId : claims.project_id === options.project || claims.project === options.project && (options.team === void 0 ? teamId !== null && claims.owner_id === teamId : claims.owner === options.team || claims.owner_id === options.team)) && oneOf(claims.environment, options.environments)
32
+ };
33
+ }
34
+ /**
35
+ * A GitHub Actions workflow. A `pull_request` run's token carries
36
+ * `ref: refs/pull/<n>/merge`, so match those on `event` and `workflow`, not `ref`.
37
+ */
38
+ function githubActions(options) {
39
+ return {
40
+ issuer: "github",
41
+ access: accessOf(`githubActions({ repository: '${options.repository}' })`, options.can),
42
+ matches: (claims) => claims.repository === options.repository && oneOf(claims.event_name, options.event) && (options.ref === void 0 || claims.ref === options.ref) && (options.workflow === void 0 || typeof claims.workflow_ref === "string" && claims.workflow_ref.startsWith(`${options.repository}/${options.workflow}@`))
43
+ };
44
+ }
45
+ /**
46
+ * Any OIDC issuer's tokens: GitLab, Buildkite, a cloud's workload identity.
47
+ * The issuer is pinned here, and only issuers some rule names are trusted or
48
+ * have their keys fetched. Every listed claim must match exactly (a list is
49
+ * any of its values), and there must be at least one, since an issuer signs
50
+ * tokens for everyone it serves. The principal is `oidc:<issuer host and path>:<sub>`,
51
+ * with any `:` or `%` in the issuer part percent-encoded, so it ends at the first `:`.
52
+ */
53
+ function oidc(options) {
54
+ let url;
55
+ try {
56
+ url = new URL(options.issuer);
57
+ } catch {
58
+ throw new Error(`oidc: issuer must be a URL, got ${options.issuer}`);
59
+ }
60
+ if (url.protocol !== "https:" || url.search || url.hash || url.username || url.password) throw new Error(`oidc: issuer must be an https URL with no query or credentials, got ${options.issuer}`);
61
+ if (VERCEL_ISSUER.test(options.issuer)) throw new Error("oidc: use vercelProject() for Vercel tokens");
62
+ if (options.issuer === "https://token.actions.githubusercontent.com") throw new Error("oidc: use githubActions() for GitHub Actions tokens");
63
+ const required = Object.entries(options.claims);
64
+ if (required.length === 0) throw new Error(`oidc: name at least one claim to match for ${options.issuer}`);
65
+ for (const [name, allowed] of required) {
66
+ const values = typeof allowed === "string" ? [allowed] : Array.isArray(allowed) ? allowed : [];
67
+ if (values.length === 0 || values.some((value) => typeof value !== "string" || value === "")) throw new Error(`oidc: claim ${name} has no value to match for ${options.issuer}`);
68
+ }
69
+ return {
70
+ issuer: "oidc",
71
+ url: options.issuer,
72
+ access: accessOf(`oidc({ issuer: '${options.issuer}' })`, options.can),
73
+ matches: (claims) => required.every(([name, allowed]) => oneOf(claims[name], allowed))
74
+ };
75
+ }
76
+ const VERCEL_ISSUER = /^https:\/\/oidc\.vercel\.com(?:\/[a-z0-9-]+)?$/;
77
+ const GITHUB_ISSUER = "https://token.actions.githubusercontent.com";
78
+ /**
79
+ * `gitlab.com`, `example.com/oidc`, `ci.test%3A8443`: an issuer as a principal
80
+ * names it. A `:` (a port) and a `%` are percent-encoded, so the name has no `:`
81
+ * and the `sub` after it can hold anything. Issuers that differ only in what the
82
+ * URL normalizes (a trailing slash, the host's case) share a name, so the board
83
+ * refuses to trust two of them.
84
+ */
85
+ const issuerName = (iss) => {
86
+ const url = new URL(iss);
87
+ return `${url.host}${url.pathname.replace(/\/+$/, "")}`.replace(/[%:]/g, (char) => char === "%" ? "%25" : "%3A");
88
+ };
89
+ /** OIDC issuers to trust, checked: two that name the same principals can't both be. */
90
+ function trustedIssuers(issuers) {
91
+ const named = /* @__PURE__ */ new Map();
92
+ for (const iss of issuers) {
93
+ const name = issuerName(iss);
94
+ const other = named.get(name);
95
+ if (other !== void 0 && other !== iss) throw new Error(`access: oidc issuers ${other} and ${iss} name the same principals, oidc:${name}:<sub>: pin one of them`);
96
+ named.set(name, iss);
97
+ }
98
+ return new Set(issuers);
99
+ }
100
+ /** The OIDC issuers rules name: the only ones whose tokens are verified for them. */
101
+ const issuersOf = (rules) => [...new Set(rules.flatMap((rule) => typeof rule !== "function" && rule.issuer === "oidc" ? [rule.url] : []))];
102
+ /**
103
+ * What principals may do, under a board's rules: `vercelProject()`, `githubActions()`, `oidc()`,
104
+ * and functions of your own, the first that matches deciding, past the defaults (people, and the
105
+ * board's own deployments in its environment, may do everything). Or a host's own function of the
106
+ * principal.
107
+ */
108
+ function createAuthority(options) {
109
+ const rules = typeof options.access === "function" ? [] : options.access;
110
+ const host = typeof options.access === "function" ? options.access : null;
111
+ const oidcIssuers = trustedIssuers(issuersOf(rules));
112
+ /**
113
+ * What the rules give a principal, in order: a token's rule when its facts match, a function when
114
+ * it answers. The board's own deployments in its environment get everything unless a token's rule
115
+ * says otherwise, and no function is asked about them. A 401 or 403 that says which rule would let
116
+ * it in, when none does.
117
+ */
118
+ async function byRules(principal, facts) {
119
+ if (facts?.issuer === "host") throw forbidden("this token can't use this board: its board no longer takes a host's principals");
120
+ if (facts?.issuer === "oidc" && !oidcIssuers.has(facts.iss)) throw unauthorized(`untrusted issuer: ${facts.iss}`);
121
+ const claims = facts?.claims ?? {};
122
+ const own = facts?.issuer === "vercel" ? await options.project() : null;
123
+ const context = own ? {
124
+ teamId: facts.team ?? own.teamId,
125
+ projectId: own.id
126
+ } : {
127
+ teamId: null,
128
+ projectId: null
129
+ };
130
+ const environment = String(claims.environment ?? "");
131
+ const here = boardEnvironment();
132
+ const ownDeployment = own !== null && own.id !== null && claims.project_id === own.id;
133
+ const ownHere = ownDeployment && here !== null && environment === here;
134
+ for (const rule of rules) {
135
+ if (typeof rule === "function") {
136
+ if (ownHere) continue;
137
+ const can = await rule(principal);
138
+ if (can === void 0) continue;
139
+ if (can === null) throw forbidden("this token can't use this board");
140
+ return compileAccess(can);
141
+ }
142
+ if (facts && rule.issuer === facts.issuer && (rule.issuer !== "oidc" || rule.url === facts.iss) && rule.matches(claims, context)) return rule.access;
143
+ }
144
+ if (ownHere) return FULL_ACCESS;
145
+ if (!facts) throw forbidden("this token can't use this board");
146
+ if (facts.issuer === "oidc") throw notInAccess(`oidc({ issuer: '${facts.iss}', claims, can })`);
147
+ if (facts.issuer === "github") throw notInAccess(`githubActions({ repository: '${String(claims.repository)}', can })`);
148
+ 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 })`);
149
+ throw notInAccess(`vercelProject({ project: '${String(claims.project ?? "") || String(claims.project_id)}', can })`);
150
+ }
151
+ /**
152
+ * What a principal may do now, narrowed by its scope: the board's own code and the platform users
153
+ * sync writes as everything; anyone else what a host's function says, or under the rules, a person
154
+ * everything and anyone else whatever the first rule that matches gives. A host's principal is
155
+ * what its kind is, until it expires. A 401 or 403 when it may do nothing.
156
+ */
157
+ async function grant(principal) {
158
+ let access = FULL_ACCESS;
159
+ const hosted = principal.facts?.issuer === "host";
160
+ if (principal.id.startsWith("board:") || principal.kind === "external" && !hosted) access = FULL_ACCESS;
161
+ else {
162
+ if (hosted) {
163
+ if (!options.hosts) throw forbidden("this token can't use this board: its board no longer takes a host's principals");
164
+ const { exp } = principal.facts.claims;
165
+ if (typeof exp === "number" && exp * 1e3 <= Date.now()) throw unauthorized("token expired");
166
+ }
167
+ if (host) {
168
+ const can = await host(principal);
169
+ if (can === null || can === void 0) throw forbidden("this token can't use this board");
170
+ access = compileAccess(can);
171
+ } else if (principal.kind !== "person") access = await byRules(principal, (hosted ? void 0 : principal.facts) ?? factsFromId(principal.id));
172
+ }
173
+ return principal.scope === void 0 ? access : access.narrow(parseScope(principal.scope));
174
+ }
175
+ /** What a stored principal (a schedule's creator, a watcher's registrant) may do now: null once nothing. */
176
+ async function accessNow(principal) {
177
+ try {
178
+ return await grant(principal);
179
+ } catch (error) {
180
+ const status = error.status;
181
+ if (status === 401 || status === 403) return null;
182
+ throw error;
183
+ }
184
+ }
185
+ return {
186
+ grant,
187
+ accessNow
188
+ };
189
+ }
190
+ /**
191
+ * What a principal may do under `rules` (`vercelProject()`, `githubActions()`, `oidc()`, and
192
+ * functions of your own), the first that matches deciding, past the defaults: people, and the
193
+ * board's own deployments in the environment it runs in, may do everything. Its scope narrows what
194
+ * it gets. A 401 or 403 that names the rule that would let it in, when it may do nothing.
195
+ */
196
+ function compileRules(rules, board) {
197
+ return createAuthority({
198
+ access: rules,
199
+ project: () => board.project(),
200
+ hosts: true
201
+ }).grant;
202
+ }
203
+ /**
204
+ * The Vercel environment the board runs in: `production`, `preview`, or a
205
+ * custom environment's name. `VERCEL_TARGET_ENV` names custom environments,
206
+ * where `VERCEL_ENV` says `preview`. Null outside Vercel, where only people
207
+ * get in without a rule.
208
+ */
209
+ const boardEnvironment = () => process.env.VERCEL_TARGET_ENV || process.env.VERCEL_ENV || null;
210
+ /**
211
+ * What a principal's ID still says, for one stored before the board kept
212
+ * claims: a deployment's project ID and environment. A workflow's ID names
213
+ * neither its repository nor its run, so it says nothing, and a rule that
214
+ * needs more than the ID no longer matches.
215
+ */
216
+ function factsFromId(id) {
217
+ const deployment = id.match(/^vercel:project:([^:]+):([^:]+)$/);
218
+ return deployment ? {
219
+ issuer: "vercel",
220
+ iss: "",
221
+ claims: {
222
+ project_id: deployment[1],
223
+ environment: deployment[2]
224
+ }
225
+ } : null;
226
+ }
227
+ /** What marks `oidcTokens()`, so board() can tell it makes no host's principals. */
228
+ const TOKENS = Symbol("board.oidcTokens");
229
+ /** The principals `oidcTokens()` made: board() takes them as they are, and checks the rest as a host's. */
230
+ const verifiedTokens = /* @__PURE__ */ new WeakSet();
231
+ /** Marks a principal `oidcTokens()` made. */
232
+ function markVerified(principal) {
233
+ verifiedTokens.add(principal);
234
+ return principal;
235
+ }
236
+ /** Whether `authenticate` is `oidcTokens()`'s alone: it makes no host's principals. */
237
+ const verifiesTokens = (authenticate) => TOKENS in authenticate;
238
+ /** Whether `oidcTokens()` made `principal`, as opposed to a host's own verifier. */
239
+ const fromTokens = (principal) => verifiedTokens.has(principal);
240
+ const PRINCIPAL_ID = /^[a-z][a-z0-9-]*:[a-z][a-z0-9-]*:[^\s+\]]{1,200}$/;
241
+ const KINDS = [
242
+ "person",
243
+ "deployment",
244
+ "workflow",
245
+ "external"
246
+ ];
247
+ /**
248
+ * Checks what a host's own verifier returned. A malformed principal is the host's bug, so it
249
+ * fails the request rather than letting a half-identity write.
250
+ */
251
+ function hostPrincipal(value) {
252
+ const fail = (why) => {
253
+ throw new TypeError(`authenticate returned an invalid principal: ${why}`);
254
+ };
255
+ if (typeof value !== "object" || value === null) fail("not an object");
256
+ if (typeof value.id !== "string" || !PRINCIPAL_ID.test(value.id) || value.id.startsWith("board:")) fail(`id must look like \`vercel:user:<id>\`, got ${JSON.stringify(value.id)}`);
257
+ if (!KINDS.includes(value.kind)) fail(`kind must be one of ${KINDS.join(", ")}`);
258
+ if (typeof value.handle !== "string" || !/^\S{1,64}$/.test(value.handle)) fail("handle must be 1-64 characters, no spaces");
259
+ const expiresAt = value.expiresAt ?? null;
260
+ if (expiresAt !== null && (!(expiresAt instanceof Date) || Number.isNaN(expiresAt.getTime()))) fail("expiresAt must be a Date or null");
261
+ if (expiresAt !== null && expiresAt.getTime() <= Date.now()) throw unauthorized("token expired");
262
+ const claims = expiresAt === null ? {} : { exp: Math.floor(expiresAt.getTime() / 1e3) };
263
+ return {
264
+ id: value.id,
265
+ kind: value.kind,
266
+ handle: value.handle,
267
+ expiresAt,
268
+ facts: {
269
+ issuer: "host",
270
+ iss: "",
271
+ claims
272
+ }
273
+ };
274
+ }
275
+ //#endregion
276
+ export { compileRules as a, githubActions as c, issuersOf as d, markVerified as f, verifiesTokens as g, vercelProject as h, authenticates as i, hostPrincipal as l, trustedIssuers as m, TOKENS as n, createAuthority as o, oidc as p, VERCEL_ISSUER as r, fromTokens as s, GITHUB_ISSUER as t, issuerName as u };
@@ -0,0 +1,204 @@
1
+ import { r as Can, t as Access } from "./access-CV96SYgr.mjs";
2
+ import { JWTPayload, JWTVerifyGetKey } from "jose";
3
+ //#region src/server/auth.d.ts
4
+ /**
5
+ * What a request says about who's calling, beside its credential: labels the caller picks for
6
+ * itself, under whoever the credential turns out to be. They're never security: board() uses them
7
+ * to work out the actor a verified caller writes as.
8
+ */
9
+ type Claims = {
10
+ /** `x-board-profile`: the agent or tool it writes as, `claude`, say. */
11
+ profile: string | null;
12
+ /** `x-board-avatar`: an https URL for its picture. */
13
+ avatar: string | null;
14
+ /** `x-board-session`: the agent session it writes from. */
15
+ session: string | null;
16
+ /** `x-board-handle` and `x-board-name`: a person's handle and display name. */
17
+ handle: string | null;
18
+ name: string | null;
19
+ /** `x-board-scope`: what it narrows itself to, `dev/=read,post`. It only narrows. */
20
+ scope: string | null;
21
+ };
22
+ /** A request, as board() hands it to `authenticate`: what it said, none of it verified yet. */
23
+ type CallerRequest = {
24
+ /** The bearer token, or null when it sent none. */
25
+ credential: string | null;
26
+ claims: Claims;
27
+ /** The board's URL, which a token must be minted for. */
28
+ audience: string;
29
+ /** Its headers, for a verifier that recognizes a session of its own: a same-site page's cookie, say. */
30
+ headers: Headers;
31
+ };
32
+ /** `external`: a platform user writing through sync (`slack:user:U0456`). A local board's people are `local:user:<name>`. */
33
+ type PrincipalKind = 'person' | 'deployment' | 'workflow' | 'external';
34
+ /**
35
+ * Who's calling, as board() verified it: an ID in the board's scheme and what its token said.
36
+ * What it may do is board()'s `access` to say.
37
+ */
38
+ type Principal = {
39
+ /** `vercel:user:<id>`, `vercel:project:<id>:<env>`, `github:repo:<id>`, `oidc:<issuer>:<sub>`. */
40
+ id: string;
41
+ kind: PrincipalKind;
42
+ /** The default handle; a person's is replaced by the handle their CLI sends, unless a host's verifier named them. */
43
+ handle: string;
44
+ expiresAt: Date | null;
45
+ /** The verified token's claims, so its access can be worked out again later, as a schedule's sends do. Absent for people and the board's own code. */
46
+ facts?: TokenFacts;
47
+ /** The `x-board-scope` a proxy sent, which narrows whatever it may do. */
48
+ scope?: string;
49
+ };
50
+ /** A principal and what it may do in each channel, as board()'s `access` worked it out. */
51
+ type Granted = Principal & {
52
+ access: Access;
53
+ };
54
+ /** A verified token's claims, as rules match them. */
55
+ type TokenClaims = JWTPayload & Record<string, unknown>;
56
+ /** What a verified token said: its issuer and its claims, never the token itself. */
57
+ type TokenFacts = {
58
+ /** `host`: a host's own verifier recognized the caller; its claims are when that runs out. */
59
+ issuer: 'vercel' | 'github' | 'oidc' | 'host';
60
+ iss: string;
61
+ claims: Readonly<Record<string, string | number | boolean>>;
62
+ /**
63
+ * The board's own Vercel team, as the board knew it when it verified the
64
+ * token. A schedule's send may run where the board can't work it out (the
65
+ * scheduler's own function, with no request to learn it from), and a rule
66
+ * naming a project without its team needs it. Only the board writes it.
67
+ */
68
+ team?: string;
69
+ };
70
+ /** What a rule may compare claims to: the board's own Vercel team and project, when they're known. */
71
+ type MatchContext = {
72
+ teamId: string | null;
73
+ projectId: string | null;
74
+ };
75
+ /** One entry in `board({ access })`: who it matches, and what they may do. The first entry that matches decides. */
76
+ type AccessRule = {
77
+ /** `vercel` and `github` are their platforms' issuers; `oidc` is the exact `url`. */
78
+ issuer: 'vercel' | 'github' | 'oidc';
79
+ /** For `oidc`: the issuer URL, pinned in code. */
80
+ url?: string;
81
+ matches(claims: TokenClaims, context: MatchContext): boolean;
82
+ access: Access;
83
+ };
84
+ /**
85
+ * A Vercel project's deployments, by name or ID. A name is only unique within
86
+ * a team, so without `team` it means the board's own team, and matches nothing
87
+ * when that isn't known. An ID is unique on Vercel, so it matches on any team.
88
+ * `'self'` is the board's own project: its deployments in the environment the
89
+ * board runs in have full access unless a rule says otherwise, and its other
90
+ * environments have none unless one lists them. Its development tokens are
91
+ * people, which no rule changes.
92
+ */
93
+ declare function vercelProject(options: {
94
+ project: string;
95
+ team?: string;
96
+ environments?: readonly string[];
97
+ can: Can;
98
+ }): AccessRule;
99
+ /**
100
+ * A GitHub Actions workflow. A `pull_request` run's token carries
101
+ * `ref: refs/pull/<n>/merge`, so match those on `event` and `workflow`, not `ref`.
102
+ */
103
+ declare function githubActions(options: {
104
+ repository: string;
105
+ event?: string | readonly string[];
106
+ workflow?: string;
107
+ ref?: string;
108
+ can: Can;
109
+ }): AccessRule;
110
+ /**
111
+ * Any OIDC issuer's tokens: GitLab, Buildkite, a cloud's workload identity.
112
+ * The issuer is pinned here, and only issuers some rule names are trusted or
113
+ * have their keys fetched. Every listed claim must match exactly (a list is
114
+ * any of its values), and there must be at least one, since an issuer signs
115
+ * tokens for everyone it serves. The principal is `oidc:<issuer host and path>:<sub>`,
116
+ * with any `:` or `%` in the issuer part percent-encoded, so it ends at the first `:`.
117
+ */
118
+ declare function oidc(options: {
119
+ issuer: string;
120
+ claims: Readonly<Record<string, string | readonly string[]>>;
121
+ can: Can;
122
+ }): AccessRule;
123
+ type IssuerConfig = {
124
+ /** Accepted `iss` values. */
125
+ issuer: string | RegExp;
126
+ /** Key source; defaults to the issuer's published JWKS. */
127
+ jwks?: JWTVerifyGetKey;
128
+ };
129
+ /** `oidcTokens()`'s options: tests point these at a local issuer; production uses the defaults. */
130
+ type OidcTokensOptions = {
131
+ vercel?: IssuerConfig;
132
+ github?: IssuerConfig;
133
+ /** Keys for `oidc()` issuers, by issuer URL, instead of their published JWKS. It trusts no issuer by itself. */
134
+ oidc?: Readonly<Record<string, JWTVerifyGetKey>>;
135
+ /**
136
+ * A local board's own issuer (`vb serve`): its tokens' `sub` is a person on
137
+ * this machine, `local:user:<sub>`, with full access. Its keys are never
138
+ * fetched, so `jwks` is required.
139
+ */
140
+ local?: {
141
+ issuer: string;
142
+ jwks: JWTVerifyGetKey;
143
+ };
144
+ };
145
+ /**
146
+ * What a host's own model gives a principal: what it may do, or null for nothing. It sees only the
147
+ * principals its own `authenticate` returned.
148
+ */
149
+ type AccessFunction = (principal: Known) => Can | null | undefined | Promise<Can | null | undefined>;
150
+ /**
151
+ * A rule of your own, among `vercelProject()` and the rest: what it gives a principal (`'all'`,
152
+ * `['read']`, `{ 'support/': ['read', 'post'] }`), null for nothing, or undefined to leave it to the
153
+ * next rule. It's asked whenever a principal acts, a schedule's send included, and never about a
154
+ * person or the board's own deployments in its environment, which the defaults decide first.
155
+ */
156
+ 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 its scope. */
158
+ type Known = Pick<Principal, 'id' | 'kind' | 'handle' | 'facts' | 'scope'>;
159
+ /**
160
+ * What a principal may do under `rules` (`vercelProject()`, `githubActions()`, `oidc()`, and
161
+ * functions of your own), the first that matches deciding, past the defaults: people, and the
162
+ * board's own deployments in the environment it runs in, may do everything. Its scope narrows what
163
+ * it gets. A 401 or 403 that names the rule that would let it in, when it may do nothing.
164
+ */
165
+ declare function compileRules(rules: readonly (AccessRule | RuleFunction)[], board: {
166
+ project(): Promise<{
167
+ id: string | null;
168
+ teamId: string | null;
169
+ }>;
170
+ }): (principal: Known) => Promise<Access>;
171
+ /** What board() knows that verifying a request needs: its own Vercel project, and the OIDC issuers its rules trust. */
172
+ type AuthenticateContext = {
173
+ /** The board's own Vercel project and team: its people's development tokens are for it. */
174
+ project(): Promise<{
175
+ id: string | null;
176
+ teamId: string | null;
177
+ }>;
178
+ /** OIDC issuers past Vercel's and GitHub's whose tokens may be trusted: the ones `oidc()` rules name. */
179
+ issuers: readonly string[];
180
+ };
181
+ /** What a verifier gets: what the request said, and what the board knows that verifying it needs. */
182
+ type AuthenticateRequest = CallerRequest & {
183
+ board: AuthenticateContext;
184
+ };
185
+ /**
186
+ * Who a request is, in the board's ID scheme: `oidcTokens()` checks its bearer token for the
187
+ * board's URL, and a host's own verifier recognizes its own credentials (a session cookie in
188
+ * `request.headers`, say). Null when the request carries nothing it recognizes, so verifiers
189
+ * compose: `(request) => (await staff(request)) ?? (await tokens(request))`. To reject a
190
+ * credential that fails, throw an `HttpError` (a 401), or return or throw a `Response`; anything
191
+ * else thrown is a 500. What a principal may do is `board({ access })`'s to say.
192
+ */
193
+ type Authenticate = (request: AuthenticateRequest) => Principal | HostPrincipal | Response | null | undefined | Promise<Principal | HostPrincipal | Response | null | undefined>;
194
+ /** What a host's own verifier returns: a principal in the board's ID scheme. */
195
+ type HostPrincipal = {
196
+ /** `vercel:user:<id>`, `vercel:project:<id>:<env>`, …: records keep their shape. */
197
+ id: string;
198
+ kind: PrincipalKind;
199
+ /** Kept as it is: `x-board-handle` doesn't replace it. Return `request.claims.handle` to take the caller's. */
200
+ handle: string;
201
+ expiresAt?: Date | null;
202
+ };
203
+ //#endregion
204
+ export { oidc as _, CallerRequest as a, HostPrincipal as c, OidcTokensOptions as d, Principal as f, githubActions as g, compileRules as h, AuthenticateContext as i, IssuerConfig as l, RuleFunction as m, AccessRule as n, Claims as o, PrincipalKind as p, Authenticate as r, Granted as s, AccessFunction as t, Known as u, vercelProject as v };
@@ -0,0 +1,8 @@
1
+ import { t as BlobStore } from "../blob-C0J3wbGv.mjs";
2
+ //#region src/blob/vercel.d.ts
3
+ export declare function vercelBlob(options?: {
4
+ token?: string;
5
+ storeId?: string;
6
+ prefix?: string;
7
+ }): BlobStore;
8
+ //#endregion
@@ -0,0 +1,81 @@
1
+ import { BlobNotFoundError, get, head, issueSignedToken, presignUrl, put } from "@vercel/blob";
2
+ //#region src/blob/vercel.ts
3
+ /**
4
+ * Attachments in a private Vercel Blob store. Connect one to the board's
5
+ * project; the SDK finds it through `BLOB_READ_WRITE_TOKEN`, or the project's
6
+ * OIDC token and `BLOB_STORE_ID`. Files are private: the board serves them to
7
+ * callers it verified, and the store never hands them out. Direct uploads, for
8
+ * files over 4 MB, `PUT` to a URL the store presigns for one pathname, which
9
+ * takes at most the file's size and never overwrites.
10
+ */
11
+ /** The Blob API version `@vercel/blob` speaks, which a presigned `PUT` names. */
12
+ const BLOB_API_VERSION = "12";
13
+ function vercelBlob(options = {}) {
14
+ const auth = {
15
+ ...options.token ? { token: options.token } : {},
16
+ ...options.storeId ? { storeId: options.storeId } : {}
17
+ };
18
+ const prefix = options.prefix ? `${options.prefix.replace(/^\/+|\/+$/g, "")}/` : "";
19
+ return {
20
+ async put(key, data, contentType) {
21
+ await put(prefix + key, Buffer.from(data.buffer, data.byteOffset, data.byteLength), {
22
+ ...auth,
23
+ access: "private",
24
+ contentType,
25
+ addRandomSuffix: false
26
+ });
27
+ },
28
+ async get(key) {
29
+ const found = await get(prefix + key, {
30
+ ...auth,
31
+ access: "private",
32
+ ...key.endsWith(".json") ? { useCache: false } : {}
33
+ });
34
+ if (!found || found.statusCode !== 200) return null;
35
+ return {
36
+ data: found.stream,
37
+ contentType: found.blob.contentType
38
+ };
39
+ },
40
+ async uploadUrl(key, { contentType, maxBytes, expiresAt }) {
41
+ const pathname = prefix + key;
42
+ const validUntil = expiresAt.getTime();
43
+ const signed = await issueSignedToken({
44
+ ...auth,
45
+ pathname,
46
+ operations: ["put"],
47
+ validUntil,
48
+ allowedContentTypes: [contentType],
49
+ maximumSizeInBytes: maxBytes
50
+ });
51
+ const { presignedUrl } = await presignUrl(signed, {
52
+ operation: "put",
53
+ access: "private",
54
+ pathname,
55
+ validUntil,
56
+ allowedContentTypes: [contentType],
57
+ maximumSizeInBytes: maxBytes,
58
+ allowOverwrite: false,
59
+ addRandomSuffix: false
60
+ });
61
+ return {
62
+ url: presignedUrl,
63
+ headers: {
64
+ "x-api-version": BLOB_API_VERSION,
65
+ "x-vercel-blob-access": "private",
66
+ "x-content-type": contentType
67
+ }
68
+ };
69
+ },
70
+ async head(key) {
71
+ try {
72
+ return { size: (await head(prefix + key, auth)).size };
73
+ } catch (error) {
74
+ if (error instanceof BlobNotFoundError) return null;
75
+ throw error;
76
+ }
77
+ }
78
+ };
79
+ }
80
+ //#endregion
81
+ export { vercelBlob };
@@ -0,0 +1,18 @@
1
+ import { a as AttachmentId, c as ChannelPath, d as LabelKey, f as LabelValue, g as MessageText, h as MessageId, i as Attachment, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, o as Author, p as Labels, r as ActorKind, s as CHANNEL_PATH_MAX, t as ATTACHMENTS_PER_MESSAGE_MAX, u as Handle } from "../schemas-CWxs3ECJ.mjs";
2
+ import { b as Origin, d as messagesReducer, f as BOARD_SESSION, h as ExternalAuthor, i as ChannelsState, l as actorsReducer, m as BoardEvents, n as ActorsState, o as Message, p as BoardEvent, r as Channel, s as MessagesState, t as Actor, u as channelsReducer, x as boardContract } from "../reducers-B_vUGQRp.mjs";
3
+ //#region src/contract/files.d.ts
4
+ /** Under Vercel Functions' 4.5 MB request body limit, so an upload fits in one request. */
5
+ export declare const ATTACHMENT_BYTES_MAX: number;
6
+ /**
7
+ * The most a file sync brings over from a platform can be. Sync fetches it on the server and
8
+ * stores it in-process, so no request carries it and the request limit doesn't apply: this bounds
9
+ * what one import holds in memory.
10
+ */
11
+ export declare const IMPORT_BYTES_MAX: number;
12
+ /**
13
+ * The most a direct upload can be: the client sends the bytes straight to the blob store, on a URL
14
+ * the board signs, so no request to the board carries them. Needs a store with `uploadUrl`.
15
+ */
16
+ export declare const DIRECT_UPLOAD_BYTES_MAX: number;
17
+ //#endregion
18
+ export { ATTACHMENTS_PER_MESSAGE_MAX, type Actor, ActorId, ActorKind, type ActorsState, Attachment, AttachmentId, Author, BOARD_SESSION, type BoardEvent, type BoardEvents, CHANNEL_PATH_MAX, type Channel, ChannelPath, type ChannelsState, Emoji, type ExternalAuthor, Handle, LabelKey, LabelValue, Labels, MESSAGE_TEXT_MAX, type Message, MessageId, MessageText, type MessagesState, Origin, actorsReducer, boardContract, channelsReducer, messagesReducer };
@@ -0,0 +1,5 @@
1
+ import { n as DIRECT_UPLOAD_BYTES_MAX, r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "../files-SniSmeEr.mjs";
2
+ import { a as AttachmentId, c as ChannelPath, d as LabelKey, f as LabelValue, g as MessageText, h as MessageId, i as Attachment, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, o as Author, p as Labels, r as ActorKind, s as CHANNEL_PATH_MAX, t as ATTACHMENTS_PER_MESSAGE_MAX, u as Handle } from "../schemas-Be08nyfW.mjs";
3
+ import { f as boardContract, l as Origin, t as BOARD_SESSION } from "../contract-CuJmlrhw.mjs";
4
+ import { d as messagesReducer, n as channelsReducer, t as actorsReducer } from "../reducers-mUPFuFJV.mjs";
5
+ export { ATTACHMENTS_PER_MESSAGE_MAX, ATTACHMENT_BYTES_MAX, ActorId, ActorKind, Attachment, AttachmentId, Author, BOARD_SESSION, CHANNEL_PATH_MAX, ChannelPath, DIRECT_UPLOAD_BYTES_MAX, Emoji, Handle, IMPORT_BYTES_MAX, LabelKey, LabelValue, Labels, MESSAGE_TEXT_MAX, MessageId, MessageText, Origin, actorsReducer, boardContract, channelsReducer, messagesReducer };
@@ -0,0 +1,11 @@
1
+ import { A2Scheduler } from "experimental-a2/server";
2
+ //#region src/scheduler/local.d.ts
3
+ /**
4
+ * A scheduler in this process (`@vercel/board/backend-a2/scheduler-local`): each task runs at its time on
5
+ * a timer, so retries, leases, wake-ups, and scheduled messages work with nothing else running.
6
+ * Its timers live in the process, and a restart drops the pending ones, so it's for development,
7
+ * `vb serve`, tests, and long-running servers, never serverless functions, which stop between
8
+ * requests: there, use `vercelQueues()` or `qstash()`.
9
+ */
10
+ export declare function localScheduler(): A2Scheduler;
11
+ //#endregion
@@ -0,0 +1,2 @@
1
+ import { t as localScheduler } from "../local-Cr_dSjvs.mjs";
2
+ export { localScheduler };
@@ -0,0 +1,2 @@
1
+ import { QStashSchedulerOptions, qstash } from "experimental-a2/scheduler-qstash";
2
+ export { type QStashSchedulerOptions, qstash };
@@ -0,0 +1,2 @@
1
+ import { qstash } from "experimental-a2/scheduler-qstash";
2
+ export { qstash };
@@ -0,0 +1,2 @@
1
+ import { VercelQueuesOptions, vercelQueues } from "experimental-a2/scheduler-vercel";
2
+ export { type VercelQueuesOptions, vercelQueues };
@@ -0,0 +1,2 @@
1
+ import { vercelQueues } from "experimental-a2/scheduler-vercel";
2
+ export { vercelQueues };
@@ -0,0 +1,11 @@
1
+ import { t as BoardStore } from "../types-BHCjTZdB.mjs";
2
+ import { Clock } from "experimental-a2/server";
3
+ //#region src/store/memory.d.ts
4
+ /**
5
+ * A board that lives in memory, for tests: its log in A2's memory store, its
6
+ * tables in an in-memory SQLite. `clock` sets every stored timestamp.
7
+ */
8
+ export declare function memory(options?: {
9
+ clock?: Clock;
10
+ }): BoardStore;
11
+ //#endregion
@@ -0,0 +1,19 @@
1
+ import { r as sqlTables } from "../sql-DbiEnqM0.mjs";
2
+ import { t as sqliteDb } from "../sqlite-db-Cbj03vmp.mjs";
3
+ import { memory as memory$1 } from "experimental-a2/store-memory";
4
+ //#region src/store/memory.ts
5
+ /**
6
+ * A board that lives in memory, for tests: its log in A2's memory store, its
7
+ * tables in an in-memory SQLite. `clock` sets every stored timestamp.
8
+ */
9
+ function memory(options = {}) {
10
+ const log = memory$1(options.clock ? { clock: options.clock } : {});
11
+ const tables = sqlTables(sqliteDb(":memory:"));
12
+ return {
13
+ log,
14
+ tables,
15
+ close: () => tables.close()
16
+ };
17
+ }
18
+ //#endregion
19
+ export { memory };