@vercel/board 0.15.0 → 0.17.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 +27 -27
- package/dist/{a2-DYGYankn.mjs → a2-Bck5x1Kh.mjs} +969 -25
- package/dist/access.d.mts +5 -4
- package/dist/access.mjs +3 -3
- package/dist/{actors-rdN4dDTU.mjs → actors-BOamkEuz.mjs} +7 -76
- package/dist/attempts-CLG840Oj.mjs +20 -0
- package/dist/{auth-wHFcAte3.d.mts → auth-CU56Q2Xh.d.mts} +2 -1
- package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
- package/dist/backend-B-SJ7p6p.d.mts +680 -0
- package/dist/backend-a2/contract.d.mts +873 -2
- package/dist/backend-a2/contract.mjs +2 -2
- package/dist/backend-a2/scheduler-local.d.mts +1 -1
- package/dist/backend-a2/scheduler-local.mjs +1 -1
- package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
- package/dist/backend-a2/scheduler-qstash.mjs +521 -1
- package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
- package/dist/backend-a2/scheduler-vercel.mjs +203 -1
- package/dist/backend-a2/store-memory.d.mts +2 -2
- package/dist/backend-a2/store-memory.mjs +3 -3
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +2850 -2
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +490 -2
- package/dist/backend-a2/store-redis.d.mts +10 -2
- package/dist/backend-a2/store-redis.mjs +51 -2
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +3 -3
- package/dist/backend-a2.d.mts +2706 -3
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-8ex5g7Wv.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
- package/dist/{backend-http-BsDSgmeo.mjs → backend-http-C5I7Wfdw.mjs} +2 -2
- package/dist/{backend-http-Cv6xbEy9.mjs → backend-http-CuUIACCs.mjs} +32 -3
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
- package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
- package/dist/cli.mjs +30 -10
- package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
- package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-3EvQ8idk.mjs → engine-D0oBUBEK.mjs} +249 -163
- package/dist/events-CewD4kw7.mjs +75 -0
- package/dist/{exports-CyY3_FjZ.mjs → exports-DOdA7KhF.mjs} +35 -32
- package/dist/exports-ZsgjzEnM.d.mts +4 -0
- package/dist/external-BV_cUncs.mjs +151 -0
- package/dist/folds-CRYY14kc.d.mts +534 -0
- package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
- package/dist/ids-C6AYo1bu.mjs +31 -0
- package/dist/index.d.mts +5 -5
- package/dist/{index.default-BZ3lItjH.mjs → index.default-n4qKN7EL.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.node.d.mts +5 -5
- package/dist/index.node.mjs +3 -3
- package/dist/inspect/app.js +31 -31
- package/dist/inspect/engine.js +5 -5
- package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
- package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
- package/dist/mentions-CB6ShY4R.mjs +72 -0
- package/dist/{targets-ojFEXEyN.mjs → middleware-CqaCwucP.mjs} +1 -33
- package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
- package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
- package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
- package/dist/provider-B3DpYSDq.mjs +220 -0
- package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
- package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
- package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
- package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
- package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
- package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
- package/dist/{serve-Ccbb2LSb.mjs → serve-CNYvA4pT.mjs} +5 -5
- package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
- package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
- package/dist/{server-Cxey38B-.mjs → server-snYFkITb.mjs} +4 -4
- package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
- package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
- package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
- package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
- package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
- package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
- package/dist/store-memory-Bas9Uq5C.mjs +872 -0
- package/dist/store-memory-ZLiZbH3R.mjs +2 -0
- package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
- package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
- package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
- package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
- package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
- package/dist/sync-github.d.mts +24 -17
- package/dist/sync-github.mjs +89 -22
- package/dist/sync-kapso.d.mts +15 -13
- package/dist/sync-kapso.mjs +17 -14
- package/dist/sync-slack.d.mts +12 -17
- package/dist/sync-slack.mjs +54 -42
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.chat.mjs +1 -1
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +5 -4
- package/dist/targets-C-wdR5wP.mjs +33 -0
- package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
- package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
- package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
- package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-DojIuoFg.mjs} +233 -28
- package/docs/01-board/01-mounting.md +1 -1
- package/docs/01-board/03-identity.md +3 -1
- package/docs/01-board/09-access.md +4 -2
- package/docs/02-sync/01-overview.md +69 -47
- package/docs/02-sync/02-functions.md +395 -0
- package/docs/02-sync/03-slack.md +30 -25
- package/docs/02-sync/04-github.md +62 -26
- package/docs/02-sync/05-whatsapp.md +23 -19
- package/docs/02-sync/06-providers.md +58 -26
- package/docs/03-guides/04-triage.md +1 -1
- package/docs/04-reference/01-cli.md +26 -6
- package/docs/04-reference/02-board.md +18 -5
- package/docs/04-reference/03-http.md +6 -3
- package/docs/04-reference/05-conformance.md +1 -1
- package/docs/index.md +5 -10
- package/package.json +22 -6
- package/dist/a2-5EG8FkZ8.d.mts +0 -3359
- package/dist/access-CJU2-vaa.mjs +0 -226
- package/dist/attempts-BQUp9MKO.mjs +0 -919
- package/dist/exports-BpgyEdh6.d.mts +0 -4
- package/dist/provider-CuewcDNZ.mjs +0 -205
- package/dist/redis-tables-B35vv4CZ.mjs +0 -842
- package/dist/reducers-B_vUGQRp.d.mts +0 -1172
- package/dist/watchers-hash-D9hbHLKB.mjs +0 -225
- package/docs/02-sync/02-callbacks.md +0 -265
package/dist/access.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
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-
|
|
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
3
|
import { o as Author } from "./schemas-CWxs3ECJ.mjs";
|
|
4
|
-
import { t as Actor } from "./
|
|
4
|
+
import { t as Actor } from "./folds-CRYY14kc.mjs";
|
|
5
5
|
//#region src/server/tokens.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* The OIDC tokens made out to this board: a request's bearer token, from Vercel (people and
|
|
@@ -21,8 +21,9 @@ type ResolvedActor = {
|
|
|
21
21
|
};
|
|
22
22
|
/**
|
|
23
23
|
* The actor a caller writes as: its principal, plus the profile it claims (`vercel:user:u_123+claude`).
|
|
24
|
-
* A person's
|
|
25
|
-
* is the board's URL, which serves the known
|
|
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.
|
|
26
27
|
*/
|
|
27
28
|
export declare function resolveActor(principal: Principal, claims: Claims, audience: string): ResolvedActor;
|
|
28
29
|
//#endregion
|
package/dist/access.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as oidcTokens } from "./tokens-
|
|
1
|
+
import { a as compileRules, c as githubActions, h as vercelProject, p as oidc } from "./auth-Drg9FiC2.mjs";
|
|
2
|
+
import { i as resolveActor } from "./actors-BOamkEuz.mjs";
|
|
3
|
+
import { t as oidcTokens } from "./tokens-DhuzKzAl.mjs";
|
|
4
4
|
export { compileRules, githubActions, oidc, oidcTokens, resolveActor, vercelProject };
|
|
@@ -1,78 +1,8 @@
|
|
|
1
1
|
import { C as toStorable } from "./schemas-Be08nyfW.mjs";
|
|
2
2
|
import { r as badRequest } from "./errors-DX3hpwWt.mjs";
|
|
3
3
|
import { t as own } from "./own-OwgPWdmh.mjs";
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
|
4
|
+
import { t as extractMentions } from "./mentions-CB6ShY4R.mjs";
|
|
5
|
+
import { y as parseScope } from "./auth-Drg9FiC2.mjs";
|
|
76
6
|
//#region src/server/actors.ts
|
|
77
7
|
/** Agents whose avatars the board serves itself. */
|
|
78
8
|
const KNOWN_AGENTS = [
|
|
@@ -122,11 +52,12 @@ function claimsOf(headers) {
|
|
|
122
52
|
}
|
|
123
53
|
/**
|
|
124
54
|
* The actor a caller writes as: its principal, plus the profile it claims (`vercel:user:u_123+claude`).
|
|
125
|
-
* A person's
|
|
126
|
-
* is the board's URL, which serves the known
|
|
55
|
+
* A person's name is what they claim, and so is their handle unless a host's verifier named them;
|
|
56
|
+
* anyone else's are their principal's. `audience` is the board's URL, which serves the known
|
|
57
|
+
* agents' avatars.
|
|
127
58
|
*/
|
|
128
59
|
function resolveActor(principal, claims, audience) {
|
|
129
|
-
const baseHandle = principal.kind === "person" && claims.handle ? claims.handle : principal.handle;
|
|
60
|
+
const baseHandle = principal.kind === "person" && principal.facts?.issuer !== "host" && claims.handle ? claims.handle : principal.handle;
|
|
130
61
|
const personName = principal.kind === "person" ? claims.name : null;
|
|
131
62
|
const profile = claims.profile?.toLowerCase() ?? null;
|
|
132
63
|
const id = profile === null ? principal.id : `${principal.id}+${profile}`;
|
|
@@ -220,4 +151,4 @@ function resolveMentions(text, known, pending, lenient = false, candidates = tru
|
|
|
220
151
|
};
|
|
221
152
|
}
|
|
222
153
|
//#endregion
|
|
223
|
-
export { resolveMentions as a, resolveActor as i, actorSeen as n,
|
|
154
|
+
export { resolveMentions as a, resolveActor as i, actorSeen as n, claimsOf as r, KNOWN_AGENTS as t };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/server/wait-until.ts
|
|
2
|
+
/**
|
|
3
|
+
* Keeps `task` running after the response, where the platform offers it: Vercel's request context, as A2
|
|
4
|
+
* reads it too. Elsewhere it runs on in the process.
|
|
5
|
+
*/
|
|
6
|
+
function waitUntil(task) {
|
|
7
|
+
try {
|
|
8
|
+
Reflect.get(globalThis, Symbol.for("@vercel/request-context"))?.get?.()?.waitUntil?.(task);
|
|
9
|
+
} catch {}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Whether a run comes after a retry from a dead letter. A2 keeps counting
|
|
13
|
+
* `ctx.attempt` across one, and no earlier run sees an attempt past A2_TRIES:
|
|
14
|
+
* the event is dead by then. board gives up early, and records why, only in an
|
|
15
|
+
* event's first tries; a retried one gets A2's fresh tries, and dead-letters
|
|
16
|
+
* again if they all fail.
|
|
17
|
+
*/
|
|
18
|
+
const afterRetry = (attempt) => attempt > 10;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { waitUntil as n, afterRetry as t };
|
|
@@ -39,7 +39,7 @@ type Principal = {
|
|
|
39
39
|
/** `vercel:user:<id>`, `vercel:project:<id>:<env>`, `github:repo:<id>`, `oidc:<issuer>:<sub>`. */
|
|
40
40
|
id: string;
|
|
41
41
|
kind: PrincipalKind;
|
|
42
|
-
/** The default handle; a person's is replaced by the handle their CLI sends. */
|
|
42
|
+
/** The default handle; a person's is replaced by the handle their CLI sends, unless a host's verifier named them. */
|
|
43
43
|
handle: string;
|
|
44
44
|
expiresAt: Date | null;
|
|
45
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. */
|
|
@@ -196,6 +196,7 @@ type HostPrincipal = {
|
|
|
196
196
|
/** `vercel:user:<id>`, `vercel:project:<id>:<env>`, …: records keep their shape. */
|
|
197
197
|
id: string;
|
|
198
198
|
kind: PrincipalKind;
|
|
199
|
+
/** Kept as it is: `x-board-handle` doesn't replace it. Return `request.claims.handle` to take the caller's. */
|
|
199
200
|
handle: string;
|
|
200
201
|
expiresAt?: Date | null;
|
|
201
202
|
};
|
|
@@ -1,5 +1,228 @@
|
|
|
1
|
+
import { c as ChannelPath, v as SCOPE_MAX } from "./schemas-Be08nyfW.mjs";
|
|
1
2
|
import { a as forbidden, l as unauthorized } from "./errors-DX3hpwWt.mjs";
|
|
2
|
-
|
|
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
|
|
3
226
|
//#region src/server/auth.ts
|
|
4
227
|
/**
|
|
5
228
|
* Principal IDs a board authenticates: its people, deployments, workflows, and its own code. No
|
|
@@ -273,4 +496,4 @@ function hostPrincipal(value) {
|
|
|
273
496
|
};
|
|
274
497
|
}
|
|
275
498
|
//#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 };
|
|
499
|
+
export { FULL_ACCESS as _, 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, authorize as v, parseScope as y };
|