@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.
- package/README.md +180 -0
- package/dist/a2-C41hD8SF.mjs +3045 -0
- package/dist/a2-D24cvwpN.d.mts +3359 -0
- package/dist/access-CJU2-vaa.mjs +226 -0
- package/dist/access-CV96SYgr.d.mts +32 -0
- package/dist/access.d.mts +30 -0
- package/dist/access.mjs +4 -0
- package/dist/actors-DjMeZpnU.mjs +224 -0
- package/dist/attempts-Bxp9KgPf.mjs +919 -0
- package/dist/auth-COzc09sn.mjs +276 -0
- package/dist/auth-CU56Q2Xh.d.mts +204 -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-0roEmjl0.mjs +613 -0
- package/dist/backend-http-BVRno-ca.mjs +469 -0
- package/dist/backend-http-Bid7RIpC.d.mts +630 -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-Bir5s8ts.mjs +1449 -0
- package/dist/errors-DX3hpwWt.mjs +64 -0
- package/dist/exports-DLiE7fLY.d.mts +4 -0
- package/dist/exports-Dvc1o52I.mjs +1285 -0
- package/dist/files-SniSmeEr.mjs +47 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.default-Bkg77Pxn.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-BsOOc-6_.mjs +170 -0
- package/dist/server-jwPY7_wi.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-yM5JBUgr.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-CLjsa6od.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 +264 -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 +501 -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,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,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,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 };
|