@vercel/board 0.30.0 → 0.31.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 (125) hide show
  1. package/README.md +8 -17
  2. package/dist/{a2-D_28Pdip.mjs → a2-DngWY2Eb.mjs} +573 -361
  3. package/dist/{access-CQ39gqEV.mjs → access-CY5xVMIK.mjs} +19 -3
  4. package/dist/{access-CV96SYgr.d.mts → access-DPcrKIH_.d.mts} +6 -1
  5. package/dist/access.d.mts +4 -4
  6. package/dist/access.mjs +5 -4
  7. package/dist/{actors-xPiamLWk.mjs → actors-Bm-ojWWT.mjs} +2 -2
  8. package/dist/{auth-BPinwfN0.mjs → auth-CyO2aw5n.mjs} +127 -14
  9. package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
  10. package/dist/{backend-n3hajfrD.d.mts → backend-Bob-5CM7.d.mts} +86 -41
  11. package/dist/backend-a2/blob-vercel.d.mts +1 -1
  12. package/dist/backend-a2/contract.d.mts +37 -97
  13. package/dist/backend-a2/contract.mjs +1 -1
  14. package/dist/backend-a2/store-memory.d.mts +1 -1
  15. package/dist/backend-a2/store-memory.mjs +2 -2
  16. package/dist/backend-a2/store-postgres.d.mts +1 -1
  17. package/dist/backend-a2/store-postgres.mjs +1 -1
  18. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  19. package/dist/backend-a2/store-redis-http.mjs +1 -1
  20. package/dist/backend-a2/store-redis.d.mts +1 -1
  21. package/dist/backend-a2/store-redis.mjs +1 -1
  22. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  23. package/dist/backend-a2/store-sqlite.mjs +2 -2
  24. package/dist/backend-a2.d.mts +131 -240
  25. package/dist/backend-a2.mjs +2 -2
  26. package/dist/{backend-http-3PoBYQoo.d.mts → backend-http-BRrRbjRx.d.mts} +49 -45
  27. package/dist/{backend-http-BPaNYLWj.mjs → backend-http-CvUKOn-m.mjs} +11 -9
  28. package/dist/{backend-http-16SbvnAK.mjs → backend-http-D8mUzuKQ.mjs} +2 -2
  29. package/dist/backend-http.d.mts +1 -1
  30. package/dist/backend-http.mjs +1 -1
  31. package/dist/backend-http.node.d.mts +1 -1
  32. package/dist/backend-http.node.mjs +1 -1
  33. package/dist/{backend-vercel-Kd8nvW5A.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
  34. package/dist/{backend-vercel-qznuDxnN.d.mts → backend-vercel-BaSEUdwg.d.mts} +49 -45
  35. package/dist/{backend-vercel-C4Kee2-K.mjs → backend-vercel-CmY9yZ9m.mjs} +80 -15
  36. package/dist/backend-vercel.d.mts +1 -1
  37. package/dist/backend-vercel.mjs +1 -1
  38. package/dist/backend-vercel.node.d.mts +1 -1
  39. package/dist/backend-vercel.node.mjs +1 -1
  40. package/dist/{blob-CqILvdmr.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
  41. package/dist/{blob-DQK5prXT.mjs → blob-WbB2DJKn.mjs} +30 -11
  42. package/dist/{chat-provider-D6jVT4wS.d.mts → chat-provider-BChYrrA7.d.mts} +32 -28
  43. package/dist/{chat-provider-COtsU5jB.mjs → chat-provider-DkFp2hrh.mjs} +105 -60
  44. package/dist/cli-commands.d.mts +1 -1
  45. package/dist/cli-commands.mjs +1 -1
  46. package/dist/cli-embedded.d.mts +2 -2
  47. package/dist/cli-embedded.mjs +1 -1
  48. package/dist/cli.mjs +4 -4
  49. package/dist/{client-CjAatpV4.d.mts → client-COfYrgfV.d.mts} +184 -221
  50. package/dist/{commands-BtH69yqt.d.mts → commands-DK4W3HCJ.d.mts} +30 -10
  51. package/dist/{commands-B5DHjZKK.mjs → commands-L1JSGz2Z.mjs} +162 -45
  52. package/dist/{conformance-C7fwIvmN.mjs → conformance-DK43HiHf.mjs} +224 -20
  53. package/dist/conformance.d.mts +8 -2
  54. package/dist/conformance.mjs +1 -1
  55. package/dist/{engine-DkdkXQEy.mjs → engine-DurYIRt8.mjs} +29 -210
  56. package/dist/{errors-87TVtXeb.d.mts → errors-2vGs49Db.d.mts} +1 -1
  57. package/dist/{errors-C_5rB8Iz.mjs → errors-CJXhhzvT.mjs} +28 -23
  58. package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
  59. package/dist/{exports-CG0P8Qi4.mjs → exports-CWIdN0Ub.mjs} +146 -80
  60. package/dist/exports-FBOQWT7O.d.mts +5 -0
  61. package/dist/{folds-DGYkMW19.mjs → folds-Cu9Q9zyQ.mjs} +2 -22
  62. package/dist/{folds-8wRRXjAt.d.mts → folds-DLUzfibJ.d.mts} +13 -40
  63. package/dist/{index-BygAXP7t.d.mts → index-Ho-ayyQL.d.mts} +2 -2
  64. package/dist/index.d.mts +7 -7
  65. package/dist/{index.default-DhkgpfR2.mjs → index.default-BUcwDaAe.mjs} +1 -1
  66. package/dist/index.mjs +5 -5
  67. package/dist/index.node.d.mts +7 -7
  68. package/dist/index.node.mjs +3 -3
  69. package/dist/inspect/app.js +62 -61
  70. package/dist/inspect/engine.js +3 -3
  71. package/dist/{main-D2s-QBOK.mjs → main-Dz-8komG.mjs} +189 -112
  72. package/dist/{middleware-DlJQYiwV.mjs → middleware-BavYSDUa.mjs} +1 -1
  73. package/dist/{node-D8Rtg1su.mjs → node-BgeGcwX0.mjs} +7 -3
  74. package/dist/{project-C_2xs57L.mjs → project-CQ4VUibA.mjs} +1 -1
  75. package/dist/{provider-Bm46LAdp.d.mts → provider-gynDUoyW.d.mts} +13 -43
  76. package/dist/{redis-tables-BWJZHUGg.mjs → redis-tables-DHf85ixI.mjs} +1 -19
  77. package/dist/{reducers-8jqLewgw.mjs → reducers-DcENwtwZ.mjs} +72 -75
  78. package/dist/{serve-lPoBDEds.mjs → serve-CTmow-YH.mjs} +6 -6
  79. package/dist/{server-D82a4uMe.mjs → server-DG8D6VV0.mjs} +4 -4
  80. package/dist/{serving-DKny38LK.mjs → serving-DBFRo6U_.mjs} +1 -1
  81. package/dist/{sql-Bj0fk6dn.mjs → sql-CT5xVuGr.mjs} +2 -8
  82. package/dist/{sqlite-db-Dc8G78XV.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
  83. package/dist/{sync-kapso.d.mts → sync-chat-sdk.d.mts} +33 -37
  84. package/dist/sync-chat-sdk.mjs +44 -0
  85. package/dist/sync-github.d.mts +5 -18
  86. package/dist/sync-github.mjs +4 -70
  87. package/dist/sync-linear.d.mts +3 -3
  88. package/dist/sync-linear.mjs +2 -3
  89. package/dist/sync-slack.d.mts +7 -7
  90. package/dist/sync-slack.mjs +33 -54
  91. package/dist/sync.d.mts +3 -3
  92. package/dist/sync.mjs +2 -4
  93. package/dist/{tokens-C5_yGRlL.mjs → tokens-Dnyd_y1n.mjs} +15 -6
  94. package/dist/{types-shZmGZ-q.d.mts → types-CmrFjpml.d.mts} +1 -3
  95. package/dist/{watchers-hash-B_5b0-ZG.mjs → watchers-hash-BjwlnGD9.mjs} +85 -105
  96. package/docs/01-board/01-mounting.md +2 -2
  97. package/docs/01-board/03-identity.md +5 -6
  98. package/docs/01-board/04-messages.md +3 -2
  99. package/docs/01-board/05-reading.md +2 -1
  100. package/docs/01-board/06-watchers.md +8 -6
  101. package/docs/01-board/08-middleware.md +1 -1
  102. package/docs/01-board/09-access.md +145 -14
  103. package/docs/02-sync/01-overview.md +19 -26
  104. package/docs/02-sync/02-functions.md +8 -9
  105. package/docs/02-sync/03-slack.md +5 -5
  106. package/docs/02-sync/04-github.md +2 -2
  107. package/docs/02-sync/06-providers.md +91 -62
  108. package/docs/03-guides/01-coding-agents.md +1 -1
  109. package/docs/03-guides/03-ci.md +7 -4
  110. package/docs/04-reference/01-cli.md +86 -41
  111. package/docs/04-reference/02-board.md +56 -23
  112. package/docs/04-reference/03-http.md +59 -32
  113. package/docs/04-reference/04-log.md +8 -11
  114. package/docs/04-reference/05-conformance.md +35 -6
  115. package/docs/index.md +2 -2
  116. package/package.json +6 -9
  117. package/sql/postgres/board.sql +1 -1
  118. package/sql/postgres/schema.sql +1 -1
  119. package/dist/events-qf5XG5VQ.mjs +0 -75
  120. package/dist/exports-CrGA5_yN.d.mts +0 -5
  121. package/dist/sync-kapso.mjs +0 -89
  122. package/dist/sync.chat.d.mts +0 -2
  123. package/dist/sync.chat.mjs +0 -2
  124. package/dist/users-yryHKFMo.mjs +0 -42
  125. package/docs/02-sync/05-whatsapp.md +0 -152
@@ -1,5 +1,5 @@
1
1
  import { c as ChannelPath, v as SCOPE_MAX } from "./schemas-DBmUO6pr.mjs";
2
- import { a as forbidden } from "./errors-C_5rB8Iz.mjs";
2
+ import { a as forbidden } from "./errors-CJXhhzvT.mjs";
3
3
  //#region src/server/access.ts
4
4
  /**
5
5
  * What a principal may do in a channel. `read` covers messages, threads,
@@ -135,7 +135,7 @@ function fromRules(rules) {
135
135
  const entries = entriesIn(rules);
136
136
  const everywhere = (permission) => (rules.all?.has(permission) ?? false) && entries.every(([, permissions]) => permissions.has(permission));
137
137
  const readable = /* @__PURE__ */ new Map();
138
- return {
138
+ const compiled = {
139
139
  [RULES]: rules,
140
140
  can: (permission, channel) => resolve(rules, channel).has(permission),
141
141
  readable(channel) {
@@ -163,12 +163,28 @@ function fromRules(rules) {
163
163
  return out;
164
164
  }
165
165
  };
166
+ return Object.freeze(compiled);
166
167
  }
167
168
  /** Access from config: `'all'`, a list for every channel, or a map from pattern to grant. Throws on a bad pattern or permission. */
168
169
  function compileAccess(can) {
169
170
  return fromRules(canonical(entriesOf(can)));
170
171
  }
171
172
  const FULL_ACCESS = compileAccess("all");
173
+ const NO_ACCESS = compileAccess([]);
174
+ /** What any of `accesses` allows, channel by channel: grants that match one principal add up. */
175
+ function union(accesses) {
176
+ const all = accesses.filter(isAccess);
177
+ if (all.length === 0) return NO_ACCESS;
178
+ const rules = all.map((access) => access[RULES]);
179
+ const patterns = /* @__PURE__ */ new Map([["*", { kind: "all" }]]);
180
+ for (const side of rules) for (const [pattern] of entriesIn(side)) patterns.set(key(pattern), pattern);
181
+ return fromRules(canonical([...patterns.values()].map((pattern) => [pattern, new Set(rules.flatMap((side) => [...at(side, pattern)]))])));
182
+ }
183
+ /** An access as `x-board-scope` writes it, `dev/=read,post;ci/=read`, or `*=` for nothing: what a stored principal keeps. */
184
+ function scopeOf(access) {
185
+ const entries = Object.entries(access.summary()).map(([pattern, permissions]) => `${pattern}=${permissions.join(",")}`);
186
+ return entries.length > 0 ? entries.join(";") : "*=";
187
+ }
172
188
  /**
173
189
  * `x-board-scope`: `dev/=read,post,react,label;ci/=read`. Each entry is a
174
190
  * pattern and its permissions (or `all`). It's meant for a proxy that holds the
@@ -223,4 +239,4 @@ function authorize(access, permission, channel, hidden) {
223
239
  });
224
240
  }
225
241
  //#endregion
226
- export { parseScope as i, authorize as n, compileAccess as r, FULL_ACCESS as t };
242
+ export { scopeOf as a, parseScope as i, authorize as n, union as o, compileAccess as r, FULL_ACCESS as t };
@@ -28,5 +28,10 @@ type Access = {
28
28
  /** Each pattern and its permissions, `*` first and the rest sorted; an empty `*` is left out. */
29
29
  summary(): AccessSummary;
30
30
  };
31
+ /** Access from config: `'all'`, a list for every channel, or a map from pattern to grant. Throws on a bad pattern or permission. */
32
+ declare function compileAccess(can: Can): Access;
33
+ declare const FULL_ACCESS: Access;
34
+ /** What any of `accesses` allows, channel by channel: grants that match one principal add up. */
35
+ declare function union(accesses: readonly Access[]): Access;
31
36
  //#endregion
32
- export { Grant as a, ChannelPattern as i, AccessSummary as n, Permission as o, Can as r, Access as t };
37
+ export { FULL_ACCESS as a, compileAccess as c, ChannelPattern as i, union as l, AccessSummary as n, Grant as o, Can as r, Permission as s, Access as t };
package/dist/access.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as Grant, i as ChannelPattern, n as AccessSummary, o as Permission, r as Can, t as Access } from "./access-CV96SYgr.mjs";
2
- import { _ as oidc, a as CallerRequest, c as HostPrincipal, d as OidcTokensOptions, f as Principal, g as githubActions, h as compileRules, i as AuthenticateContext, l as IssuerConfig, m as RuleFunction, n as AccessRule, o as Claims, p as PrincipalKind, r as Authenticate, t as AccessFunction, u as Known, v as vercelProject } from "./auth-CU56Q2Xh.mjs";
3
- import { C as Author, t as Actor } from "./folds-8wRRXjAt.mjs";
1
+ import { a as FULL_ACCESS, c as compileAccess, i as ChannelPattern, l as union, n as AccessSummary, o as Grant, r as Can, s as Permission, t as Access } from "./access-DPcrKIH_.mjs";
2
+ import { a as CallerRequest, b as oidc, d as Known, f as OidcTokensOptions, g as RuleFunction, h as PrincipalKind, i as AuthenticateContext, l as HostPrincipal, m as Principal, n as AccessRule, o as Claims, r as Authenticate, s as DEFAULT_CAN, t as AccessFunction, u as IssuerConfig, v as compileRules, x as vercelProject, y as githubActions } from "./auth-DLYSt_-A.mjs";
3
+ import { C as Author, t as Actor } from "./folds-DLUzfibJ.mjs";
4
4
  //#region src/server/tokens.d.ts
5
5
  /**
6
6
  * The OIDC tokens made out to this board: a request's bearer token, from Vercel (people and
@@ -26,4 +26,4 @@ type ResolvedActor = {
26
26
  */
27
27
  export declare function resolveActor(principal: Principal, claims: Claims, audience: string): ResolvedActor;
28
28
  //#endregion
29
- export { type Access, type AccessFunction, type AccessRule, type AccessSummary, type Authenticate, type AuthenticateContext, type CallerRequest, type Can, type ChannelPattern, type Claims, type Grant, type HostPrincipal, type IssuerConfig, type Known, type OidcTokensOptions, type Permission, type Principal, type PrincipalKind, type RuleFunction, compileRules, githubActions, oidc, vercelProject };
29
+ export { type Access, type AccessFunction, type AccessRule, type AccessSummary, type Authenticate, type AuthenticateContext, type CallerRequest, type Can, type ChannelPattern, type Claims, DEFAULT_CAN, FULL_ACCESS, type Grant, type HostPrincipal, type IssuerConfig, type Known, type OidcTokensOptions, type Permission, type Principal, type PrincipalKind, type RuleFunction, compileAccess, compileRules, githubActions, oidc, union, vercelProject };
package/dist/access.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { i as resolveActor } from "./actors-xPiamLWk.mjs";
2
- import { a as compileRules, c as githubActions, h as vercelProject, p as oidc } from "./auth-BPinwfN0.mjs";
3
- import { t as oidcTokens } from "./tokens-C5_yGRlL.mjs";
4
- export { compileRules, githubActions, oidc, oidcTokens, resolveActor, vercelProject };
1
+ import { o as union, r as compileAccess, t as FULL_ACCESS } from "./access-CY5xVMIK.mjs";
2
+ import { i as resolveActor } from "./actors-Bm-ojWWT.mjs";
3
+ import { _ as oidc, c as compileRules, d as githubActions, n as DEFAULT_CAN, x as vercelProject } from "./auth-CyO2aw5n.mjs";
4
+ import { t as oidcTokens } from "./tokens-Dnyd_y1n.mjs";
5
+ export { DEFAULT_CAN, FULL_ACCESS, compileAccess, compileRules, githubActions, oidc, oidcTokens, resolveActor, union, vercelProject };
@@ -1,8 +1,8 @@
1
1
  import { C as toStorable } from "./schemas-DBmUO6pr.mjs";
2
- import { r as badRequest } from "./errors-C_5rB8Iz.mjs";
2
+ import { r as badRequest } from "./errors-CJXhhzvT.mjs";
3
+ import { i as parseScope } from "./access-CY5xVMIK.mjs";
3
4
  import { t as own } from "./own-OwgPWdmh.mjs";
4
5
  import { t as extractMentions } from "./mentions-CB6ShY4R.mjs";
5
- import { i as parseScope } from "./access-CQ39gqEV.mjs";
6
6
  //#region src/server/actors.ts
7
7
  /** Agents whose avatars the board serves itself. */
8
8
  const KNOWN_AGENTS = [
@@ -1,7 +1,15 @@
1
- import { a as forbidden, l as unauthorized } from "./errors-C_5rB8Iz.mjs";
2
- import { i as parseScope, r as compileAccess, t as FULL_ACCESS } from "./access-CQ39gqEV.mjs";
1
+ import { a as forbidden, l as unauthorized, r as badRequest } from "./errors-CJXhhzvT.mjs";
2
+ import { i as parseScope, o as union, r as compileAccess, t as FULL_ACCESS } from "./access-CY5xVMIK.mjs";
3
3
  //#region src/server/auth.ts
4
4
  /**
5
+ * Whether a caller wrote what `by` says, as far as changing it goes (editing, deleting, canceling a
6
+ * schedule): its principal did, and when a board token pins the profile it writes as, as that
7
+ * profile. A token acts as its minter, so the principal alone would let one agent's token change
8
+ * what its minter or another agent wrote. Another token that pins the same profile writes as the
9
+ * same author, so a renewed one picks up where the last left off.
10
+ */
11
+ const wroteAs = (caller, by) => by.principal === caller.principal.id && ((caller.principal.pinned?.profile ?? null) === null || by.actor === caller.actor.id);
12
+ /**
5
13
  * Principal IDs a board authenticates: its people, deployments, workflows, and its own code. No
6
14
  * platform actor sync writes as may have one, or an import could pass for someone signed in.
7
15
  */
@@ -99,11 +107,101 @@ function trustedIssuers(issuers) {
99
107
  }
100
108
  /** The OIDC issuers rules name: the only ones whose tokens are verified for them. */
101
109
  const issuersOf = (rules) => [...new Set(rules.flatMap((rule) => typeof rule !== "function" && rule.issuer === "oidc" ? [rule.url] : []))];
110
+ /** What a board token starts with, so board() knows its own before it tries any other verifier. */
111
+ const BOARD_TOKEN_PREFIX = "board_";
112
+ /**
113
+ * A board token's SHA-256, hex: what the board keeps instead of the secret, both when it mints one
114
+ * and when a request carries one. A token is 32 random bytes, so no salt or key is needed.
115
+ */
116
+ async function tokenHash(secret) {
117
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret)));
118
+ return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
119
+ }
120
+ /** What a grant or a token left without `can` gives: what Vercel's API gives a project it lets use a board. */
121
+ const DEFAULT_CAN = { "*": [
122
+ "read",
123
+ "post",
124
+ "react",
125
+ "label"
126
+ ] };
127
+ const GRANT_CLAIMS_MAX = 32;
128
+ const GRANT_VALUES_MAX = 32;
129
+ const GRANT_VALUE_MAX = 1e3;
130
+ const invalidGrant = (message) => badRequest(message, "invalid_grant");
131
+ /** Whether `host` names something on the board's own network rather than on the internet. */
132
+ function privateHost(host) {
133
+ const name = host.toLowerCase().replace(/\.$/, "");
134
+ if (name.startsWith("[") || /^\d+(\.\d+){3}$/.test(name) || /^\d+$/.test(name)) return true;
135
+ if (!name.includes(".") || name === "localhost") return true;
136
+ return /\.(localhost|local|internal|home|lan|corp|intranet)$/.test(name);
137
+ }
138
+ /**
139
+ * A grant checked and put in one form: its issuer, its claims with keys and lists sorted, its
140
+ * `can`, and the key that says it's the same grant as another. A 400 `invalid_grant` that says
141
+ * what to write. `trusted`: the issuers the board's rules and other grants name, which no two may
142
+ * share principals between.
143
+ */
144
+ function checkGrant(spec, trusted) {
145
+ const { issuer } = spec;
146
+ if (typeof issuer !== "string" || issuer === "") throw invalidGrant("a grant needs an issuer: vercel, github, or an https URL");
147
+ if (issuer !== "vercel" && issuer !== "github") {
148
+ let url;
149
+ try {
150
+ url = new URL(issuer);
151
+ } catch {
152
+ throw invalidGrant(`a grant's issuer is vercel, github, or an https URL, got ${issuer}`);
153
+ }
154
+ if (VERCEL_ISSUER.test(issuer)) throw invalidGrant("write the issuer as vercel, and another team's projects with owner_id");
155
+ if (issuer === "https://token.actions.githubusercontent.com") throw invalidGrant("write the issuer as github");
156
+ if (url.protocol !== "https:" || url.search || url.hash || url.username || url.password) throw invalidGrant(`a grant's issuer is an https URL with no query or credentials, got ${issuer}`);
157
+ if (privateHost(url.hostname)) throw invalidGrant(`a grant's issuer is on the internet, not ${url.hostname}`);
158
+ const name = issuerName(issuer);
159
+ const other = trusted.find((candidate) => candidate !== issuer && /^https:/.test(candidate) && issuerName(candidate) === name);
160
+ if (other !== void 0) throw invalidGrant(`issuers ${other} and ${issuer} name the same principals, oidc:${name}:<sub>: use ${other}`);
161
+ }
162
+ const entries = typeof spec.claims === "object" && spec.claims !== null ? Object.entries(spec.claims) : [];
163
+ if (entries.length === 0) throw invalidGrant(`name at least one claim for ${issuer}: its issuer signs tokens for everyone it serves`);
164
+ if (entries.length > GRANT_CLAIMS_MAX) throw invalidGrant(`a grant names at most ${GRANT_CLAIMS_MAX} claims`);
165
+ const claims = {};
166
+ for (const [name, value] of entries.sort(([a], [b]) => a.localeCompare(b))) {
167
+ const values = typeof value === "string" ? [value] : Array.isArray(value) ? [...new Set(value)] : [];
168
+ if (values.length === 0 || values.some((one) => typeof one !== "string" || one === "")) throw invalidGrant(`claim ${name} has no value to match: give a string, or a list of them`);
169
+ if (values.length > GRANT_VALUES_MAX || values.some((one) => one.length > GRANT_VALUE_MAX)) throw invalidGrant(`claim ${name} lists at most ${GRANT_VALUES_MAX} values of up to ${GRANT_VALUE_MAX} characters`);
170
+ const sorted = values.sort();
171
+ claims[name] = sorted.length === 1 ? sorted[0] : sorted;
172
+ }
173
+ if (issuer === "vercel" && (claims.project_id === void 0 || claims.environment === void 0)) throw invalidGrant("a Vercel grant names project_id and environment: a preview runs any branch, so it says outright whether they get in");
174
+ if (issuer === "github" && claims.repository === void 0 && claims.repository_id === void 0) throw invalidGrant("a GitHub grant names repository or repository_id: GitHub signs tokens for every repository");
175
+ const can = spec.can ?? DEFAULT_CAN;
176
+ try {
177
+ compileAccess(can);
178
+ } catch (error) {
179
+ throw invalidGrant(error.message);
180
+ }
181
+ return {
182
+ issuer,
183
+ claims,
184
+ can,
185
+ key: JSON.stringify([issuer, claims])
186
+ };
187
+ }
188
+ /** Whether a verified token's facts are a grant's: its issuer, and every claim it names. */
189
+ function grantMatches(grant, facts) {
190
+ if (!(grant.issuer === "vercel" || grant.issuer === "github" ? facts.issuer === grant.issuer : facts.issuer === "oidc" && facts.iss === grant.issuer)) return false;
191
+ return Object.entries(grant.claims).every(([name, allowed]) => {
192
+ const value = facts.claims[name];
193
+ if (value === void 0) return false;
194
+ const text = String(value);
195
+ return typeof allowed === "string" ? text === allowed : allowed.includes(text);
196
+ });
197
+ }
198
+ /** The OIDC issuers grants name past Vercel and GitHub: their tokens are verified too. */
199
+ const grantIssuers = (grants) => [...new Set(grants.flatMap((grant) => grant.issuer === "vercel" || grant.issuer === "github" ? [] : [grant.issuer]))];
102
200
  /**
103
201
  * 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.
202
+ * and functions of your own, the first that matches deciding, with the board's grants adding to it,
203
+ * past the defaults (people, and the board's own deployments in its environment, may do
204
+ * everything). Or a host's own function of the principal.
107
205
  */
108
206
  function createAuthority(options) {
109
207
  const rules = typeof options.access === "function" ? [] : options.access;
@@ -111,13 +209,14 @@ function createAuthority(options) {
111
209
  const oidcIssuers = trustedIssuers(issuersOf(rules));
112
210
  /**
113
211
  * 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.
212
+ * it answers. Every grant that matches its token adds to that, so a grant never goes unused
213
+ * because a rule matched first. The board's own deployments in its environment get everything
214
+ * unless a token's rule says otherwise, no function is asked about them, and no grant is theirs;
215
+ * a function that answers null refuses, grants or not. A 401 or 403 that says which rule would
216
+ * let it in, when nothing does.
117
217
  */
118
218
  async function byRules(principal, facts) {
119
219
  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
220
  const claims = facts?.claims ?? {};
122
221
  const own = facts?.issuer === "vercel" ? await options.project() : null;
123
222
  const context = own ? {
@@ -131,18 +230,32 @@ function createAuthority(options) {
131
230
  const here = boardEnvironment();
132
231
  const ownDeployment = own !== null && own.id !== null && claims.project_id === own.id;
133
232
  const ownHere = ownDeployment && here !== null && environment === here;
233
+ let ruled = null;
134
234
  for (const rule of rules) {
135
235
  if (typeof rule === "function") {
136
236
  if (ownHere) continue;
137
237
  const can = await rule(principal);
138
238
  if (can === void 0) continue;
139
239
  if (can === null) throw forbidden("this token can't use this board");
140
- return compileAccess(can);
240
+ ruled = compileAccess(can);
241
+ break;
141
242
  }
142
- if (facts && rule.issuer === facts.issuer && (rule.issuer !== "oidc" || rule.url === facts.iss) && rule.matches(claims, context)) return rule.access;
243
+ if (facts && rule.issuer === facts.issuer && (rule.issuer !== "oidc" || rule.url === facts.iss) && rule.matches(claims, context)) {
244
+ ruled = rule.access;
245
+ break;
246
+ }
247
+ }
248
+ if (ownHere) return ruled ?? FULL_ACCESS;
249
+ if (!facts) {
250
+ if (ruled) return ruled;
251
+ throw forbidden("this token can't use this board");
143
252
  }
144
- if (ownHere) return FULL_ACCESS;
145
- if (!facts) throw forbidden("this token can't use this board");
253
+ const grants = options.grants ? await options.grants() : [];
254
+ const granted = grants.filter((grant) => grantMatches(grant, facts)).map((grant) => compileAccess(grant.can));
255
+ if (ruled) return granted.length > 0 ? union([ruled, ...granted]) : ruled;
256
+ if (granted.length > 0) return union(granted);
257
+ if (facts.issuer === "oidc" && !oidcIssuers.has(facts.iss) && !grants.some((grant) => grant.issuer === facts.iss)) throw unauthorized(`untrusted issuer: ${facts.iss}`);
258
+ if (facts.issuer === "oidc" && !oidcIssuers.has(facts.iss)) throw forbidden("this token can't use this board: no grant lets in its claims");
146
259
  if (facts.issuer === "oidc") throw notInAccess(`oidc({ issuer: '${facts.iss}', claims, can })`);
147
260
  if (facts.issuer === "github") throw notInAccess(`githubActions({ repository: '${String(claims.repository)}', can })`);
148
261
  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 })`);
@@ -273,4 +386,4 @@ function hostPrincipal(value) {
273
386
  };
274
387
  }
275
388
  //#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 };
389
+ export { wroteAs as C, verifiesTokens as S, oidc as _, VERCEL_ISSUER as a, trustedIssuers as b, compileRules as c, githubActions as d, grantIssuers as f, markVerified as g, issuersOf as h, TOKENS as i, createAuthority as l, issuerName as m, DEFAULT_CAN as n, authenticates as o, hostPrincipal as p, GITHUB_ISSUER as r, checkGrant as s, BOARD_TOKEN_PREFIX as t, fromTokens as u, privateHost as v, vercelProject as x, tokenHash as y };
@@ -1,4 +1,4 @@
1
- import { r as Can, t as Access } from "./access-CV96SYgr.mjs";
1
+ import { r as Can, t as Access } from "./access-DPcrKIH_.mjs";
2
2
  import { JWTPayload, JWTVerifyGetKey } from "jose";
3
3
  //#region src/server/auth.d.ts
4
4
  /**
@@ -46,6 +46,22 @@ type Principal = {
46
46
  facts?: TokenFacts;
47
47
  /** The `x-board-scope` a proxy sent, which narrows whatever it may do. */
48
48
  scope?: string;
49
+ /**
50
+ * The board token it came with (its ID), kept with what it stores: a schedule or a registration
51
+ * made with one stops when the token does.
52
+ */
53
+ token?: string;
54
+ /** What a board token pins for every request with it: the profile, avatar, and session it writes as, and a person's handle and name. */
55
+ pinned?: Pinned;
56
+ };
57
+ /** What a board token says about who writes, beside its principal: labels, but the board's, so a request can't change them. */
58
+ type Pinned = {
59
+ profile: string | null;
60
+ avatar: string | null;
61
+ session: string | null;
62
+ /** A person's handle and display name as they were when they minted it: whoever holds the token sends none. */
63
+ handle: string | null;
64
+ name: string | null;
49
65
  };
50
66
  /** A principal and what it may do in each channel, as board()'s `access` worked it out. */
51
67
  type Granted = Principal & {
@@ -154,8 +170,20 @@ type AccessFunction = (principal: Known) => Can | null | undefined | Promise<Can
154
170
  * person or the board's own deployments in its environment, which the defaults decide first.
155
171
  */
156
172
  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'>;
173
+ /** A principal as the board keeps it to work its access out again: who it is, its token's facts, its scope, and the board token it came with. */
174
+ type Known = Pick<Principal, 'id' | 'kind' | 'handle' | 'facts' | 'scope' | 'token'>;
175
+ /** A grant the board keeps: its ID, its issuer and claims as the board checked them, and who set it when. */
176
+ type StoredGrant = {
177
+ id: string;
178
+ issuer: string;
179
+ claims: Readonly<Record<string, string | readonly string[]>>;
180
+ can: Can;
181
+ /** The actor that last set it, and when. */
182
+ by: string;
183
+ at: string;
184
+ };
185
+ /** What a grant or a token left without `can` gives: what Vercel's API gives a project it lets use a board. */
186
+ declare const DEFAULT_CAN: Can;
159
187
  /**
160
188
  * What a principal may do under `rules` (`vercelProject()`, `githubActions()`, `oidc()`, and
161
189
  * functions of your own), the first that matches deciding, past the defaults: people, and the
@@ -201,4 +229,4 @@ type HostPrincipal = {
201
229
  expiresAt?: Date | null;
202
230
  };
203
231
  //#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 };
232
+ export { StoredGrant as _, CallerRequest as a, oidc as b, Granted as c, Known as d, OidcTokensOptions as f, RuleFunction as g, PrincipalKind as h, AuthenticateContext as i, HostPrincipal as l, Principal as m, AccessRule as n, Claims as o, Pinned as p, Authenticate as r, DEFAULT_CAN as s, AccessFunction as t, IssuerConfig as u, compileRules as v, vercelProject as x, githubActions as y };
@@ -1,9 +1,9 @@
1
- import { t as Access } from "./access-CV96SYgr.mjs";
2
- import { m as RuleFunction, n as AccessRule, r as Authenticate, s as Granted, t as AccessFunction, u as Known } from "./auth-CU56Q2Xh.mjs";
3
- import { A as Labels, C as Author, t as Actor, u as ExternalEvent, x as Attachment } from "./folds-8wRRXjAt.mjs";
4
- import { $ as RegisterBody, A as InboxItem, B as MatchesQuery, Bt as TypingBody, C as EventsQuery, Ct as SyncCopiesView, D as IdentityView, Dt as SyncImportBody, Et as SyncFindView, F as LabelBody, H as MessageView, Ht as UploadStartBody, It as ThreadView, J as PostBody, L as LinkedAccount, Lt as ThreadsPage, M as InboxPushBody, Mt as SyncThreadView, N as InboxQuery, Nt as SyncUnlinkBody, O as InboxAckBody, Pt as SyncedView, R as LinkedQuery, Rt as ThreadsQuery, S as EventsPage, St as SyncCopiesQuery, Tt as SyncFindQuery, U as MessagesPage, Ut as UploadTicket, V as Me, W as MessagesQuery, Z as ReactBody, _ as BoardEventView, _t as SyncAccountBody, b as Discovery, bt as SyncCodeBody, ct as SchedulesQuery, ft as SourceJoin, gt as SourceReadQuery, ht as SourceRead, it as ScheduleBody, j as InboxNextQuery, jt as SyncRootsQuery, k as InboxEntry, kt as SyncReceiptBody, m as AckBody, o as ProfileOption, pt as SourceJoinBody, q as PersonView, st as ScheduleView, t as BoardClient, tt as RegistrationView, v as ChannelView, x as EditBody, xt as SyncCodeView, y as DeleteBody, z as MatchView } from "./client-CjAatpV4.mjs";
5
- import { t as HttpError } from "./errors-87TVtXeb.mjs";
6
- import { p as SyncProvider } from "./provider-Bm46LAdp.mjs";
1
+ import { t as Access } from "./access-DPcrKIH_.mjs";
2
+ import { _ as StoredGrant, c as Granted, d as Known, g as RuleFunction, n as AccessRule, p as Pinned, r as Authenticate, t as AccessFunction } from "./auth-DLYSt_-A.mjs";
3
+ import { A as Labels, C as Author, t as Actor, u as ExternalEvent, x as Attachment } from "./folds-DLUzfibJ.mjs";
4
+ import { $ as RegisterBody, A as InboxAckBody, At as ThreadsPage, B as MatchView, C as EventsPage, D as GrantBody, Dt as SyncedView, Et as SyncThreadView, F as InboxQuery, Ft as TokenView, G as MessagesQuery, H as Me, It as TypingBody, J as PostBody, L as LabelBody, M as InboxItem, N as InboxNextQuery, Nt as TokenBody, P as InboxPushBody, Rt as UploadStartBody, S as EditBody, St as SyncImportBody, U as MessageView, V as MatchesQuery, W as MessagesPage, Z as ReactBody, _ as BoardEventView, _t as SyncCopiesQuery, b as DeleteBody, bt as SyncFindQuery, ct as SchedulesQuery, dt as SourceJoin, ft as SourceJoinBody, ht as SourceReadQuery, it as ScheduleBody, j as InboxEntry, jt as ThreadsQuery, k as GrantView, kt as ThreadView, m as AckBody, mt as SourceRead, o as ProfileOption, st as ScheduleView, t as BoardClient, tt as RegistrationView, v as ChannelView, vt as SyncCopiesView, w as EventsQuery, wt as SyncReceiptBody, x as Discovery, xt as SyncFindView, y as CreateChannelBody, z as LinkedQuery, zt as UploadTicket } from "./client-COfYrgfV.mjs";
5
+ import { t as HttpError } from "./errors-2vGs49Db.mjs";
6
+ import { p as SyncProvider } from "./provider-gynDUoyW.mjs";
7
7
  import { z } from "zod";
8
8
  //#region src/api/protocol.d.ts
9
9
  type Input<S extends z.ZodType> = NonNullable<z.output<S>>;
@@ -35,19 +35,21 @@ type Protocol = {
35
35
  cursor: string;
36
36
  };
37
37
  };
38
+ /** `created` is false for a channel that was there already, made by a post or another create. */
39
+ createChannel: {
40
+ input: Input<typeof CreateChannelBody>;
41
+ output: {
42
+ channel: ChannelView;
43
+ created: boolean;
44
+ };
45
+ };
38
46
  actors: {
39
47
  input: Nothing;
40
48
  output: {
41
- actors: (Actor & {
42
- person?: PersonView;
43
- })[];
49
+ actors: Actor[];
44
50
  cursor: string;
45
51
  };
46
52
  };
47
- identity: {
48
- input: Nothing;
49
- output: IdentityView;
50
- };
51
53
  messages: {
52
54
  input: Input<typeof MessagesQuery>;
53
55
  output: MessagesPage;
@@ -119,6 +121,50 @@ type Protocol = {
119
121
  canceled: boolean;
120
122
  };
121
123
  };
124
+ /** The board's grants: needs `manage` in every channel. 501 `no_grants` where the board keeps none. */
125
+ grants: {
126
+ input: Nothing;
127
+ output: {
128
+ grants: GrantView[];
129
+ };
130
+ };
131
+ /** Sets a grant, or the `can` of the one with its issuer and claims: `created` is false for that. */
132
+ createGrant: {
133
+ input: Input<typeof GrantBody>;
134
+ output: {
135
+ grant: GrantView;
136
+ created: boolean;
137
+ };
138
+ };
139
+ /** `revoked` is false when it was already gone. */
140
+ revokeGrant: {
141
+ input: Id;
142
+ output: {
143
+ revoked: boolean;
144
+ };
145
+ };
146
+ /** The caller's tokens, or every one with `manage` in every channel. 501 `no_tokens` where the board issues none. */
147
+ tokens: {
148
+ input: Nothing;
149
+ output: {
150
+ tokens: TokenView[];
151
+ };
152
+ };
153
+ /** A token acting as the caller, narrowed: the only time its secret shows. */
154
+ createToken: {
155
+ input: Input<typeof TokenBody>;
156
+ output: {
157
+ token: TokenView;
158
+ secret: string;
159
+ };
160
+ };
161
+ /** It and every token minted with it stop. `revoked` is false when it was already gone. */
162
+ revokeToken: {
163
+ input: Id;
164
+ output: {
165
+ revoked: boolean;
166
+ };
167
+ };
122
168
  upload: {
123
169
  input: {
124
170
  data: Uint8Array;
@@ -240,12 +286,6 @@ type Protocol = {
240
286
  input: Input<typeof SyncCopiesQuery>;
241
287
  output: SyncCopiesView;
242
288
  };
243
- syncRoots: {
244
- input: Input<typeof SyncRootsQuery>;
245
- output: {
246
- roots: MessageView[];
247
- };
248
- };
249
289
  /** Platform events into the caller's inbox, deduplicated by their platform IDs. */
250
290
  inboxPush: {
251
291
  input: Key & Input<typeof InboxPushBody>;
@@ -273,25 +313,6 @@ type Protocol = {
273
313
  events: InboxEntry[];
274
314
  };
275
315
  };
276
- /** A code for linking the caller's account, and the sign-in that carries it when the entry signs people in. */
277
- syncCode: {
278
- input: Input<typeof SyncCodeBody>;
279
- output: SyncCodeView;
280
- };
281
- /** 404 `bad_code` for a code that isn't live. */
282
- syncAccount: {
283
- input: Input<typeof SyncAccountBody>;
284
- output: {
285
- person: string;
286
- link: LinkedAccount;
287
- };
288
- };
289
- syncUnlink: {
290
- input: Input<typeof SyncUnlinkBody>;
291
- output: {
292
- ok: true;
293
- };
294
- };
295
316
  };
296
317
  type Method = keyof Protocol;
297
318
  //#endregion
@@ -575,11 +596,10 @@ type Subscription =
575
596
  {
576
597
  kind: 'sync';
577
598
  };
578
- /** One of board()'s sync entries: its key, its platform, and whether it can sign people in. */
599
+ /** One of board()'s sync entries: its key and its platform. */
579
600
  type SyncEntry = {
580
601
  key: string;
581
602
  platform: string;
582
- signIn: boolean;
583
603
  };
584
604
  /** A platform event sync pushed to an inbox, as the backend delivers it back. */
585
605
  type InboxDelivery = {
@@ -647,6 +667,8 @@ type BoardBinding = {
647
667
  id: string | null;
648
668
  teamId: string | null;
649
669
  }>;
670
+ /** The OIDC issuers board()'s rules name, which no grant's issuer may share principals with. */
671
+ issuers(): readonly string[];
650
672
  /**
651
673
  * A handle through board(), as `caller`: what a backend writes for someone outside a request
652
674
  * goes through it, so board()'s middleware sees it, a schedule's send as its creator, say. A
@@ -656,6 +678,27 @@ type BoardBinding = {
656
678
  /** The board's own URL, where a board elsewhere can wake it, once it's known. */
657
679
  url(): string | null;
658
680
  };
681
+ /** A token a backend issued, as board() checks a request that carries it. */
682
+ type StoredToken = {
683
+ id: string;
684
+ name: string;
685
+ /** Who minted it, as a schedule keeps its creator: what it may do is worked out again on every request, narrowed by its scope. */
686
+ minter: Known;
687
+ pinned: Pinned;
688
+ expiresAt: Date;
689
+ };
690
+ /**
691
+ * What a backend that keeps the board keeps of who it trusts: its grants, and the tokens it
692
+ * issued. board() reads them on the requests they decide, as they are then, so a revoke takes
693
+ * effect on the next request.
694
+ */
695
+ type Trust = {
696
+ grants(): Promise<readonly StoredGrant[]>;
697
+ /** The token whose secret hashes to `hash`, while it and every token it was minted with are live: null otherwise. */
698
+ token(hash: string): Promise<StoredToken | null>;
699
+ /** Whether token `id` and every token it was minted with are live: a schedule made with one stops when it doesn't. */
700
+ live(id: string): Promise<boolean>;
701
+ };
659
702
  /**
660
703
  * Where a board lives. board() serves the protocol over HTTP: it routes, validates, verifies each
661
704
  * caller, applies `access`, runs the middleware, watchers, and sync, and calls these with the caller
@@ -677,6 +720,8 @@ interface Backend extends ProtocolMethods {
677
720
  * board() screens a schedule's message once, when it's made. True when left out.
678
721
  */
679
722
  readonly sendsSchedulesHere?: boolean;
723
+ /** Its grants and tokens, for a backend that keeps them (`a2()`): board() checks requests against them. */
724
+ readonly trust?: Trust;
680
725
  /**
681
726
  * board() calls it once, when it's created, before any request: what background work needs
682
727
  * from the board (its subscriptions, its sync, its policy, a handle through it). It does no I/O.
@@ -684,4 +729,4 @@ interface Backend extends ProtocolMethods {
684
729
  bind(board: BoardBinding): void;
685
730
  }
686
731
  //#endregion
687
- export { Protocol as S, MiddlewareContext as _, Identity as a, WriteSource as b, SelfCaller as c, Board as d, BoardOptions as f, Middleware as g, DroppedError as h, Caller as i, Subscription as l, WatcherContext as m, BackendRoute as n, InboxDelivery as o, Watcher as p, BoardBinding as r, ProtocolMethods as s, Backend as t, VerifiedCaller as u, Write as v, Method as x, WritePatch as y };
732
+ export { Protocol as C, Method as S, Middleware as _, Identity as a, WritePatch as b, SelfCaller as c, VerifiedCaller as d, Board as f, DroppedError as g, WatcherContext as h, Caller as i, StoredToken as l, Watcher as m, BackendRoute as n, InboxDelivery as o, BoardOptions as p, BoardBinding as r, ProtocolMethods as s, Backend as t, Subscription as u, MiddlewareContext as v, WriteSource as x, Write as y };
@@ -1,4 +1,4 @@
1
- import { t as BlobStore } from "../blob-CqILvdmr.mjs";
1
+ import { t as BlobStore } from "../blob-Bx8QPWEd.mjs";
2
2
  //#region src/blob/vercel.d.ts
3
3
  export declare function vercelBlob(options?: {
4
4
  token?: string;