@vercel/board 0.29.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 (156) hide show
  1. package/README.md +8 -17
  2. package/dist/{a2-HUbQJ0RD.mjs → a2-DngWY2Eb.mjs} +741 -415
  3. package/dist/access-CY5xVMIK.mjs +242 -0
  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-DcJhFbwJ.mjs → actors-Bm-ojWWT.mjs} +2 -2
  8. package/dist/{auth-JO22iIwr.mjs → auth-CyO2aw5n.mjs} +126 -236
  9. package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
  10. package/dist/{backend-CPC_vyOo.d.mts → backend-Bob-5CM7.d.mts} +93 -42
  11. package/dist/backend-a2/blob-vercel.d.mts +1 -1
  12. package/dist/backend-a2/contract.d.mts +38 -98
  13. package/dist/backend-a2/contract.mjs +1 -1
  14. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  15. package/dist/backend-a2/scheduler-local.mjs +1 -1
  16. package/dist/backend-a2/scheduler-qstash.d.mts +3 -3
  17. package/dist/backend-a2/scheduler-qstash.mjs +8 -8
  18. package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
  19. package/dist/backend-a2/scheduler-vercel.mjs +6 -6
  20. package/dist/backend-a2/store-memory.d.mts +2 -2
  21. package/dist/backend-a2/store-memory.mjs +3 -3
  22. package/dist/backend-a2/store-postgres.d.mts +1 -1
  23. package/dist/backend-a2/store-postgres.mjs +34 -15
  24. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  25. package/dist/backend-a2/store-redis-http.mjs +6 -6
  26. package/dist/backend-a2/store-redis.d.mts +2 -2
  27. package/dist/backend-a2/store-redis.mjs +5 -5
  28. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  29. package/dist/backend-a2/store-sqlite.mjs +3 -3
  30. package/dist/backend-a2.d.mts +143 -242
  31. package/dist/backend-a2.mjs +2 -2
  32. package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-BRrRbjRx.d.mts} +59 -45
  33. package/dist/{backend-http-CzIJMm58.mjs → backend-http-CvUKOn-m.mjs} +11 -10
  34. package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-D8mUzuKQ.mjs} +2 -2
  35. package/dist/backend-http.d.mts +1 -1
  36. package/dist/backend-http.mjs +1 -1
  37. package/dist/backend-http.node.d.mts +1 -1
  38. package/dist/backend-http.node.mjs +1 -1
  39. package/dist/{backend-vercel-DQc_TaHp.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
  40. package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-BaSEUdwg.d.mts} +62 -47
  41. package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-CmY9yZ9m.mjs} +279 -42
  42. package/dist/backend-vercel.d.mts +1 -1
  43. package/dist/backend-vercel.mjs +1 -1
  44. package/dist/backend-vercel.node.d.mts +1 -1
  45. package/dist/backend-vercel.node.mjs +1 -1
  46. package/dist/{blob-DmiQjrYy.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
  47. package/dist/{blob-DdbpJqN1.mjs → blob-WbB2DJKn.mjs} +30 -11
  48. package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-BChYrrA7.d.mts} +32 -28
  49. package/dist/{chat-provider-COtsU5jB.mjs → chat-provider-DkFp2hrh.mjs} +105 -60
  50. package/dist/cli-commands.d.mts +1 -1
  51. package/dist/cli-commands.mjs +1 -1
  52. package/dist/cli-embedded.d.mts +2 -2
  53. package/dist/cli-embedded.mjs +1 -1
  54. package/dist/cli.mjs +4 -4
  55. package/dist/{client-BqAjk0Q4.d.mts → client-COfYrgfV.d.mts} +240 -227
  56. package/dist/{commands-D6gvrOh3.d.mts → commands-DK4W3HCJ.d.mts} +47 -10
  57. package/dist/{commands-C3R1AWgm.mjs → commands-L1JSGz2Z.mjs} +212 -54
  58. package/dist/conformance-DK43HiHf.mjs +1000 -0
  59. package/dist/conformance.d.mts +8 -2
  60. package/dist/conformance.mjs +1 -1
  61. package/dist/{engine-DQxXk7dX.mjs → engine-DurYIRt8.mjs} +29 -210
  62. package/dist/{errors-DDktVeUa.d.mts → errors-2vGs49Db.d.mts} +1 -1
  63. package/dist/{client-D0Px7KzF.mjs → errors-CJXhhzvT.mjs} +132 -27
  64. package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
  65. package/dist/{exports-BG-0Wyn-.mjs → exports-CWIdN0Ub.mjs} +195 -103
  66. package/dist/exports-FBOQWT7O.d.mts +5 -0
  67. package/dist/{folds-BDMSxBUf.mjs → folds-Cu9Q9zyQ.mjs} +3 -22
  68. package/dist/{folds-ZkHM4M-v.d.mts → folds-DLUzfibJ.d.mts} +15 -40
  69. package/dist/{index-D3VvNfh_.d.mts → index-Ho-ayyQL.d.mts} +2 -2
  70. package/dist/index.d.mts +7 -7
  71. package/dist/{index.default-rB8sJ9RX.mjs → index.default-BUcwDaAe.mjs} +1 -1
  72. package/dist/index.mjs +5 -6
  73. package/dist/index.node.d.mts +7 -7
  74. package/dist/index.node.mjs +3 -4
  75. package/dist/inspect/app.js +62 -61
  76. package/dist/inspect/engine.js +3 -3
  77. package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
  78. package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
  79. package/dist/{main-CVBqe3VO.mjs → main-Dz-8komG.mjs} +251 -127
  80. package/dist/{middleware-CXjEeef4.mjs → middleware-BavYSDUa.mjs} +1 -1
  81. package/dist/{node-Zd_C-gxP.mjs → node-BgeGcwX0.mjs} +7 -3
  82. package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
  83. package/dist/{project-CBXlLftU.mjs → project-CQ4VUibA.mjs} +1 -1
  84. package/dist/{provider-DmSTp9lI.d.mts → provider-gynDUoyW.d.mts} +13 -43
  85. package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-DHf85ixI.mjs} +40 -42
  86. package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
  87. package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
  88. package/dist/{reducers-DJzVpXwM.mjs → reducers-DcENwtwZ.mjs} +74 -77
  89. package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
  90. package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
  91. package/dist/{serve-G9RuaDSr.mjs → serve-CTmow-YH.mjs} +7 -7
  92. package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
  93. package/dist/{server-BixV_byP.mjs → server-DG8D6VV0.mjs} +4 -4
  94. package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
  95. package/dist/{serving-B75Y4BF-.mjs → serving-DBFRo6U_.mjs} +1 -1
  96. package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
  97. package/dist/{sql-D4Z_efJX.mjs → sql-CT5xVuGr.mjs} +2 -8
  98. package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
  99. package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
  100. package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
  101. package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
  102. package/dist/store-memory-4IBl0Ztu.mjs +2 -0
  103. package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
  104. package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
  105. package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
  106. package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
  107. package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
  108. package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
  109. package/dist/{sync-kapso.d.mts → sync-chat-sdk.d.mts} +33 -37
  110. package/dist/sync-chat-sdk.mjs +44 -0
  111. package/dist/sync-github.d.mts +5 -18
  112. package/dist/sync-github.mjs +4 -70
  113. package/dist/sync-linear.d.mts +3 -3
  114. package/dist/sync-linear.mjs +6 -5
  115. package/dist/sync-slack.d.mts +7 -7
  116. package/dist/sync-slack.mjs +33 -54
  117. package/dist/sync.d.mts +3 -3
  118. package/dist/sync.mjs +2 -4
  119. package/dist/{tokens-CC3izSNr.mjs → tokens-Dnyd_y1n.mjs} +15 -6
  120. package/dist/{types-No0WdhE5.d.mts → types-CmrFjpml.d.mts} +2 -4
  121. package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-BjwlnGD9.mjs} +124 -115
  122. package/docs/01-board/01-mounting.md +2 -2
  123. package/docs/01-board/03-identity.md +5 -6
  124. package/docs/01-board/04-messages.md +96 -2
  125. package/docs/01-board/05-reading.md +2 -1
  126. package/docs/01-board/06-watchers.md +8 -6
  127. package/docs/01-board/08-middleware.md +6 -3
  128. package/docs/01-board/09-access.md +145 -14
  129. package/docs/02-sync/01-overview.md +19 -26
  130. package/docs/02-sync/02-functions.md +8 -9
  131. package/docs/02-sync/03-slack.md +5 -5
  132. package/docs/02-sync/04-github.md +2 -2
  133. package/docs/02-sync/06-providers.md +91 -62
  134. package/docs/03-guides/01-coding-agents.md +1 -1
  135. package/docs/03-guides/03-ci.md +7 -4
  136. package/docs/04-reference/01-cli.md +109 -51
  137. package/docs/04-reference/02-board.md +76 -28
  138. package/docs/04-reference/03-http.md +76 -38
  139. package/docs/04-reference/04-log.md +8 -11
  140. package/docs/04-reference/05-conformance.md +47 -7
  141. package/docs/index.md +2 -2
  142. package/package.json +7 -10
  143. package/sql/postgres/board.sql +1 -1
  144. package/sql/postgres/schema.sql +1 -1
  145. package/dist/conformance-DlnfuzEB.mjs +0 -556
  146. package/dist/errors-CusN4x2l.mjs +0 -88
  147. package/dist/events-qf5XG5VQ.mjs +0 -75
  148. package/dist/exports-mhAYiU4i.d.mts +0 -5
  149. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
  150. package/dist/store-memory-ZLiZbH3R.mjs +0 -2
  151. package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
  152. package/dist/sync-kapso.mjs +0 -89
  153. package/dist/sync.chat.d.mts +0 -2
  154. package/dist/sync.chat.mjs +0 -2
  155. package/dist/users-yryHKFMo.mjs +0 -42
  156. package/docs/02-sync/05-whatsapp.md +0 -152
@@ -1,4 +1,5 @@
1
- import { n as ExternalEvent } from "./external-D3p5T57R.mjs";
1
+ import { d as LabelKey, p as Labels } from "./schemas-DBmUO6pr.mjs";
2
+ import { n as ExternalEvent, r as ExternalLocation } from "./external-D3p5T57R.mjs";
2
3
  import { z } from "zod";
3
4
  //#region src/sync/fn.ts
4
5
  const ROUTES = Symbol.for("board.syncRoutes");
@@ -122,8 +123,6 @@ var SyncProvider = class {
122
123
  uploadsFiles;
123
124
  startsThreadsWithFiles;
124
125
  fn;
125
- /** People's tokens, when the adapter has them: messages they write post as them. */
126
- users;
127
126
  options;
128
127
  constructor(options, sync) {
129
128
  if (typeof sync !== "function") throw new Error(`${options.platform} needs a sync function: what each event does, like channels({ '#dev': 'dev' })`);
@@ -131,23 +130,12 @@ var SyncProvider = class {
131
130
  const routes = routesOf(sync);
132
131
  if (routes) routed.set(this, routes);
133
132
  this.options = options;
134
- this.users = options.users ?? null;
135
133
  this.platform = options.platform;
136
134
  this.canStartThreads = options.canStartThreads ?? false;
137
135
  this.singleReaction = options.singleReaction ?? false;
138
136
  this.uploadsFiles = options.uploadsFiles ?? false;
139
137
  this.startsThreadsWithFiles = this.uploadsFiles && (options.startsThreadsWithFiles ?? false);
140
138
  }
141
- /** The platform account a person's token belongs to. */
142
- async whoAmI(_token) {
143
- throw new Error(`${this.platform} can't post as a person`);
144
- }
145
- /** `post`, as the person whose token this is. */
146
- async postAs(_token, _target, _kind, _body) {
147
- throw new Error(`${this.platform} can't post as a person`);
148
- }
149
- async editAs(_token, _thread, _message, _body) {}
150
- async deleteAs(_token, _thread, _message) {}
151
139
  async findByEmail(_email) {
152
140
  return null;
153
141
  }
@@ -164,8 +152,12 @@ var SyncProvider = class {
164
152
  async permalink(_event) {
165
153
  return null;
166
154
  }
167
- /** A linked message the platform didn't describe, read from the platform. */
168
- async fetchLink(_link) {
155
+ /**
156
+ * A linked message the platform didn't describe, read from the platform. `linker` is the
157
+ * platform message that links it, by its own IDs: what the platform says about the link may be
158
+ * there by now.
159
+ */
160
+ async fetchLink(_link, _linker) {
169
161
  return {
170
162
  snapshot: null,
171
163
  unavailable: `${this.platform} can't read it`
@@ -223,4 +215,75 @@ var SyncProvider = class {
223
215
  }
224
216
  };
225
217
  //#endregion
226
- export { channelMissing as a, into as c, withRoutes as d, UnreadableError as i, mirror as l, ResolvedTarget as n, routesFor as o, SyncProvider as r, channels as s, ReceivedEvent as t, render as u };
218
+ //#region src/sync/events.ts
219
+ const EventBase = z.object({
220
+ id: z.string().min(1),
221
+ platform: z.string().min(1),
222
+ key: z.string().min(1),
223
+ ...ExternalLocation.shape,
224
+ raw: z.unknown()
225
+ });
226
+ const eventTypes = {
227
+ message: "message",
228
+ edited: "message.edited",
229
+ deleted: "message.deleted",
230
+ reaction: "reaction.added",
231
+ lifecycle: "lifecycle"
232
+ };
233
+ function envelope(platform, key, event, raw, id) {
234
+ return {
235
+ ...event,
236
+ ...EventBase.parse({
237
+ id,
238
+ platform,
239
+ key,
240
+ channel: event.channel,
241
+ thread: event.thread,
242
+ subthread: "subthread" in event ? event.subthread : null,
243
+ raw
244
+ }),
245
+ type: event.kind === "lifecycle" ? event.type : event.kind === "reaction" && !event.added ? "reaction.removed" : eventTypes[event.kind]
246
+ };
247
+ }
248
+ function eventSchema(platform, messageType) {
249
+ const [message, edited, deleted, reaction, lifecycle] = ExternalEvent.options;
250
+ const common = {
251
+ ...EventBase.shape,
252
+ platform: z.literal(platform)
253
+ };
254
+ return z.discriminatedUnion("kind", [
255
+ message.extend({
256
+ ...common,
257
+ type: z.literal(messageType)
258
+ }),
259
+ edited.extend({
260
+ ...common,
261
+ type: z.literal("message.edited")
262
+ }),
263
+ deleted.extend({
264
+ ...common,
265
+ type: z.literal("message.deleted")
266
+ }),
267
+ reaction.extend({
268
+ ...common,
269
+ type: z.enum(["reaction.added", "reaction.removed"])
270
+ }),
271
+ lifecycle.extend(common)
272
+ ]);
273
+ }
274
+ const SyncOverrides = z.object({
275
+ root: z.custom((value) => typeof value === "function").optional(),
276
+ text: z.string().optional(),
277
+ labels: Labels.optional(),
278
+ unset: z.array(LabelKey).optional(),
279
+ noSync: z.boolean().optional()
280
+ }).strict();
281
+ function immutable(value) {
282
+ if (value && typeof value === "object") {
283
+ for (const item of Object.values(value)) immutable(item);
284
+ Object.freeze(value);
285
+ }
286
+ return value;
287
+ }
288
+ //#endregion
289
+ export { immutable as a, SyncProvider as c, routesFor as d, channels as f, withRoutes as g, render as h, eventSchema as i, UnreadableError as l, mirror as m, SyncOverrides as n, ReceivedEvent as o, into as p, envelope as r, ResolvedTarget as s, EventBase as t, channelMissing as u };
@@ -1,12 +1,12 @@
1
1
  import { t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
2
  import "./schemas-DBmUO6pr.mjs";
3
- import { A as SchedulesQuery, B as SyncReceiptBody, C as MatchesQuery, D as Ref, E as ReactBody, F as SyncBody, G as UploadStartBody, H as SyncUnlinkBody, I as SyncCodeBody, L as SyncCopiesQuery, M as SourceJoinBody, N as SourceReadQuery, O as RegisterBody, P as SyncAccountBody, R as SyncFindQuery, S as LinkedQuery, T as PostBody, U as ThreadsQuery, V as SyncRootsQuery, W as TypingBody, _ as InboxAckBody, a as METHODS, b as InboxQuery, c as queryObject, d as VERSION, f as SYNC, g as IdentityUnlinkBody, h as EventsQuery, j as SignInQuery, k as ScheduleBody, l as readCapped, m as EditBody, o as createRouter, p as AckBody, s as openapi, u as readJson, v as InboxNextQuery, w as MessagesQuery, x as LabelBody, y as InboxPushBody, z as SyncImportBody } from "./watchers-hash-BDhLVkV5.mjs";
4
- import { n as createBoardClient, t as BoardError } from "./client-D0Px7KzF.mjs";
3
+ import { A as RegisterBody, B as ThreadsQuery, C as LabelBody, D as PostBody, E as MessagesQuery, F as SyncBody, H as TypingBody, I as SyncCopiesQuery, L as SyncFindQuery, M as SchedulesQuery, N as SourceJoinBody, O as ReactBody, P as SourceReadQuery, R as SyncImportBody, S as InboxQuery, T as MatchesQuery, U as UploadStartBody, V as TokenBody, _ as EventsQuery, a as METHODS, b as InboxNextQuery, c as queryObject, d as VERSION, f as SYNC, g as EditBody, h as DeleteBody, j as ScheduleBody, k as Ref, l as readCapped, m as CreateChannelBody, o as createRouter, p as AckBody, s as openapi, u as readJson, v as GrantBody, w as LinkedQuery, x as InboxPushBody, y as InboxAckBody, z as SyncReceiptBody } from "./watchers-hash-BjwlnGD9.mjs";
4
+ import { a as forbidden, c as toErrorResponse, d as createBoardClient, l as unauthorized, n as RejectedError, o as json, r as badRequest, s as notFound, t as HttpError, u as BoardError } from "./errors-CJXhhzvT.mjs";
5
5
  import { r as derivedId } from "./wait-until-Dw4XrIDQ.mjs";
6
- import { a as forbidden, c as toErrorResponse, l as unauthorized, n as RejectedError, o as json, r as badRequest, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
7
- import { d as issuersOf, g as verifiesTokens, l as hostPrincipal, o as createAuthority, s as fromTokens } from "./auth-JO22iIwr.mjs";
8
- import { i as resolveActor, r as claimsOf, t as KNOWN_AGENTS } from "./actors-DcJhFbwJ.mjs";
9
- import { r as runMiddleware, t as DroppedError } from "./middleware-CXjEeef4.mjs";
6
+ import { a as scopeOf, i as parseScope, r as compileAccess } from "./access-CY5xVMIK.mjs";
7
+ import { i as resolveActor, r as claimsOf, t as KNOWN_AGENTS } from "./actors-Bm-ojWWT.mjs";
8
+ import { S as verifiesTokens, a as VERCEL_ISSUER, f as grantIssuers, h as issuersOf, l as createAuthority, p as hostPrincipal, u as fromTokens, y as tokenHash } from "./auth-CyO2aw5n.mjs";
9
+ import { r as runMiddleware, t as DroppedError } from "./middleware-BavYSDUa.mjs";
10
10
  import { n as syncTarget, r as withEmailMentions, t as mentionsEmails } from "./targets-Cgjs29T0.mjs";
11
11
  //#region src/server/policy.ts
12
12
  /**
@@ -14,10 +14,11 @@ import { n as syncTarget, r as withEmailMentions, t as mentionsEmails } from "./
14
14
  * principals its backend stores. `keeps`: the backend keeps the board, so the defaults let its
15
15
  * people and own deployments in when `access` is left out.
16
16
  */
17
- function createPolicy(options, keeps, serving) {
17
+ function createPolicy(options, keeps, serving, trust = null) {
18
18
  /** Whether requests are served at all: a board on `http()` serves none without an `access` of its own. */
19
19
  const open = keeps || options.access !== void 0;
20
20
  const access = options.access ?? [];
21
+ const grants = trust && typeof access !== "function" ? () => trust.grants() : null;
21
22
  /** The board's own Vercel project and team, from `project` or the environment, asked once. */
22
23
  let own = null;
23
24
  const project = () => (own ??= (async () => {
@@ -49,12 +50,39 @@ function createPolicy(options, keeps, serving) {
49
50
  const authority = createAuthority({
50
51
  access,
51
52
  project,
52
- hosts: options.authenticate !== void 0 && !verifiesTokens(options.authenticate)
53
+ hosts: options.authenticate !== void 0 && !verifiesTokens(options.authenticate),
54
+ grants
53
55
  });
56
+ const ruleIssuers = typeof access === "function" ? [] : issuersOf(access);
57
+ /** One for every request, so a verifier keeps what it built: its issuers change only when grants name new ones. */
54
58
  const context = {
55
59
  project,
56
- issuers: typeof access === "function" ? [] : issuersOf(access)
60
+ issuers: ruleIssuers
57
61
  };
62
+ /**
63
+ * Trusts the issuers grants name, before a token from one is verified: only for a token from an
64
+ * issuer neither Vercel, GitHub, nor a rule is, so most requests read no grants for it.
65
+ */
66
+ async function learnIssuers(credential) {
67
+ if (!grants) return;
68
+ const iss = issuerOf(credential);
69
+ if (iss === null || !iss.startsWith("https://") || iss === "https://token.actions.githubusercontent.com" || VERCEL_ISSUER.test(iss) || ruleIssuers.includes(iss)) return;
70
+ const named = grantIssuers(await grants());
71
+ const issuers = [...ruleIssuers, ...named.filter((issuer) => !ruleIssuers.includes(issuer))];
72
+ if (issuers.join("\n") !== context.issuers.join("\n")) context.issuers = issuers;
73
+ }
74
+ /** Who a board token is: the principal that minted it, narrowed, until it's revoked or expires. */
75
+ async function tokenPrincipal(secret) {
76
+ if (!trust) throw unauthorized("this board issues no tokens: send a board token to the board that minted it");
77
+ const found = await trust.token(await tokenHash(secret));
78
+ if (!found || found.expiresAt.getTime() <= Date.now()) throw unauthorized("invalid token: revoked, expired, or not this board's");
79
+ return {
80
+ ...found.minter,
81
+ expiresAt: found.expiresAt,
82
+ token: found.id,
83
+ pinned: found.pinned
84
+ };
85
+ }
58
86
  /** Verified principals by credential, until they expire, so a stream's polls don't verify from scratch. */
59
87
  const verified = /* @__PURE__ */ new Map();
60
88
  /** And by request, for one with no credential (a session cookie), for as long as it's served. */
@@ -80,7 +108,7 @@ function createPolicy(options, keeps, serving) {
80
108
  const principal = verify(request);
81
109
  perRequest.set(request, principal);
82
110
  if (key !== null) principal.then((found) => {
83
- if (found.facts?.issuer === "host" || found.expiresAt === null) return;
111
+ if (found.facts?.issuer === "host" || found.expiresAt === null || found.token !== void 0) return;
84
112
  if (verified.size >= 1e3) verified.delete(verified.keys().next().value);
85
113
  verified.set(key, {
86
114
  principal,
@@ -90,6 +118,8 @@ function createPolicy(options, keeps, serving) {
90
118
  return principal;
91
119
  }
92
120
  async function verify(request) {
121
+ if (request.credential?.startsWith("board_")) return tokenPrincipal(request.credential);
122
+ if (request.credential !== null) await learnIssuers(request.credential);
93
123
  const authenticate = await verifier();
94
124
  let result;
95
125
  try {
@@ -108,6 +138,10 @@ function createPolicy(options, keeps, serving) {
108
138
  return {
109
139
  open,
110
140
  project,
141
+ /** The OIDC issuers the rules name. */
142
+ issuers: ruleIssuers,
143
+ /** Whether grants decide anything here: not where `access` is one function, which decides alone. */
144
+ takesGrants: typeof access !== "function",
111
145
  discoveryIdentity,
112
146
  /**
113
147
  * Who a request is, and what it may do now: verified, under the rules, with the profile it
@@ -116,11 +150,13 @@ function createPolicy(options, keeps, serving) {
116
150
  */
117
151
  async identify(request) {
118
152
  const found = await principalOf(request);
119
- const principal = request.claims.scope === null ? found : {
153
+ const claims = found.pinned ? pinnedClaims(found.pinned, request.claims) : request.claims;
154
+ const scope = found.scope === void 0 ? claims.scope ?? void 0 : claims.scope === null ? found.scope : scopeOf(compileAccess(parseScope(found.scope)).narrow(parseScope(claims.scope)));
155
+ const principal = scope === void 0 ? found : {
120
156
  ...found,
121
- scope: request.claims.scope
157
+ scope
122
158
  };
123
- const { author, actor } = resolveActor(principal, request.claims, request.audience);
159
+ const { author, actor } = resolveActor(principal, claims, request.audience);
124
160
  return {
125
161
  principal: {
126
162
  ...principal,
@@ -130,10 +166,38 @@ function createPolicy(options, keeps, serving) {
130
166
  actor
131
167
  };
132
168
  },
133
- /** What a stored principal (a schedule's creator, a watcher's registrant) may do now: null once nothing. */
134
- accessNow: (principal) => authority.accessNow(principal)
169
+ /** What a stored principal (a schedule's creator, a watcher's registrant) may do now: null once nothing, its board token's revoking included. */
170
+ async accessNow(principal) {
171
+ if (principal.token !== void 0 && !await trust?.live(principal.token)) return null;
172
+ return authority.accessNow(principal);
173
+ }
135
174
  };
136
175
  }
176
+ /** A board token's pins, over what the request says: its profile and session win, and a person's name is the minter's. */
177
+ function pinnedClaims(pinned, claims) {
178
+ return {
179
+ ...claims,
180
+ ...pinned.profile !== null ? {
181
+ profile: pinned.profile,
182
+ avatar: pinned.avatar
183
+ } : {},
184
+ ...pinned.session !== null ? { session: pinned.session } : {},
185
+ handle: pinned.handle,
186
+ name: pinned.name
187
+ };
188
+ }
189
+ /** A JWT's `iss`, read but not verified: which issuer's keys would check it. Null for anything else. */
190
+ function issuerOf(credential) {
191
+ const payload = credential.split(".")[1];
192
+ if (payload === void 0) return null;
193
+ try {
194
+ const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payload.length / 4) * 4, "="));
195
+ const { iss } = JSON.parse(json);
196
+ return typeof iss === "string" ? iss : null;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
137
201
  //#endregion
138
202
  //#region src/contract/logos.ts
139
203
  const LOGOS = {
@@ -183,6 +247,10 @@ const CREATED = /* @__PURE__ */ new Set([
183
247
  "upload",
184
248
  "uploadFinish"
185
249
  ]);
250
+ /** An email, as `--mentions` names one: sync looks its platform user up. */
251
+ const EMAIL = /^[^\s@]+@[^\s@]+$/;
252
+ /** How long a filter's email lookup holds. */
253
+ const EMAIL_TTL_MS = 3e5;
186
254
  /** Shown inline only when a browser can't run it: everything else downloads. */
187
255
  const INLINE = /^(image\/(png|jpeg|gif|webp|avif)|video\/(mp4|webm)|text\/plain)$/;
188
256
  /**
@@ -211,13 +279,15 @@ async function inputOf(name, request, url, params) {
211
279
  case "me":
212
280
  case "channels":
213
281
  case "actors":
214
- case "identity":
215
- case "watchers": return {};
282
+ case "watchers":
283
+ case "grants":
284
+ case "tokens": return {};
216
285
  case "message":
217
286
  case "posted":
218
287
  case "synced":
219
- case "delete":
220
288
  case "cancelSchedule":
289
+ case "revokeGrant":
290
+ case "revokeToken":
221
291
  case "attachment":
222
292
  case "uploadFinish": return { id };
223
293
  case "messages": return MessagesQuery.parse(queryObject(url));
@@ -231,12 +301,19 @@ async function inputOf(name, request, url, params) {
231
301
  return query;
232
302
  }
233
303
  case "post": return PostBody.parse(await body());
304
+ case "createChannel": return CreateChannelBody.parse(await body());
234
305
  case "schedule": return ScheduleBody.parse(await body());
306
+ case "createGrant": return GrantBody.parse(await body());
307
+ case "createToken": return TokenBody.parse(await body());
235
308
  case "register": return RegisterBody.parse(await body());
236
309
  case "edit": return {
237
310
  id,
238
311
  ...EditBody.parse(await body())
239
312
  };
313
+ case "delete": return {
314
+ id,
315
+ ...DeleteBody.parse(await body())
316
+ };
240
317
  case "react": return {
241
318
  id,
242
319
  ...ReactBody.parse(await body())
@@ -269,7 +346,6 @@ async function inputOf(name, request, url, params) {
269
346
  case "syncThread": return { thread: params.thread ?? "" };
270
347
  case "syncFind": return SyncFindQuery.parse(queryObject(url));
271
348
  case "syncCopies": return SyncCopiesQuery.parse(queryObject(url));
272
- case "syncRoots": return SyncRootsQuery.parse(queryObject(url));
273
349
  case "inboxPush": return {
274
350
  key,
275
351
  ...InboxPushBody.parse(await body())
@@ -287,9 +363,6 @@ async function inputOf(name, request, url, params) {
287
363
  key,
288
364
  ...InboxQuery.parse(queryObject(url))
289
365
  };
290
- case "syncCode": return SyncCodeBody.parse(await body());
291
- case "syncAccount": return SyncAccountBody.parse(await body());
292
- case "syncUnlink": return SyncUnlinkBody.parse(await body());
293
366
  case "typing": return {
294
367
  id,
295
368
  ...TypingBody.parse(await body()) ?? {}
@@ -369,17 +442,48 @@ async function createServe(context) {
369
442
  * board learns who they are; the rest stay plain text.
370
443
  */
371
444
  async function withEmails(text) {
372
- return (await withEmailMentions(text, engine ? async (email) => {
373
- const actor = await engine.findByEmail(email);
374
- if (actor) await backend.syncImport(engine.caller, {
375
- op: "seen",
376
- author: {
377
- ...actor,
378
- isBot: false
379
- }
380
- }, new AbortController().signal);
381
- return actor;
382
- } : void 0)).text;
445
+ return (await withEmailMentions(text, engine ? learnEmail : void 0)).text;
446
+ }
447
+ /**
448
+ * The synced platform's user with this email, whom the board learns: a post can mention only
449
+ * an actor it knows, and a platform mention of them resolves only once it does.
450
+ */
451
+ async function learnEmail(email) {
452
+ const actor = await engine.findByEmail(email);
453
+ if (actor) await backend.syncImport(engine.caller, {
454
+ op: "seen",
455
+ author: {
456
+ ...actor,
457
+ isBot: false
458
+ }
459
+ }, new AbortController().signal);
460
+ return actor;
461
+ }
462
+ /** Emails filters named, as sync found them, for a few minutes: `tail` and `wait` ask at every page. */
463
+ const mentioned = /* @__PURE__ */ new Map();
464
+ /**
465
+ * `--mentions ada@vercel.com`: the synced platform's user with that email, for a caller that may
466
+ * read somewhere. Anything else, and an email sync doesn't know, goes on as it came.
467
+ */
468
+ async function withMentioned(caller, query) {
469
+ const email = query.mentions?.toLowerCase();
470
+ if (!engine || email === void 0 || !EMAIL.test(email)) return query;
471
+ const access = accessOf(caller);
472
+ if (access !== null && !access.anywhere("read")) return query;
473
+ let found = mentioned.get(email);
474
+ if (!found || Date.now() - found.at > EMAIL_TTL_MS) {
475
+ if (mentioned.size >= 1e3) mentioned.clear();
476
+ found = {
477
+ at: Date.now(),
478
+ actor: learnEmail(email).catch(() => null)
479
+ };
480
+ mentioned.set(email, found);
481
+ }
482
+ const actor = await found.actor;
483
+ return actor ? {
484
+ ...query,
485
+ mentions: actor.id
486
+ } : query;
383
487
  }
384
488
  /**
385
489
  * A sync target as the board that syncs it resolves it. One for an entry this board doesn't
@@ -513,6 +617,25 @@ async function createServe(context) {
513
617
  }));
514
618
  }
515
619
  /**
620
+ * A schedule, on a backend whose sends post elsewhere: its message through the middleware once,
621
+ * as the caller's post of it, since its text and labels never change and no send comes back here.
622
+ */
623
+ function screenSchedule(caller, input, signal) {
624
+ const { channel, re, text, labels, noSync, attachments } = input;
625
+ return screenPost(caller, {
626
+ ...channel !== void 0 ? { channel } : {},
627
+ ...re !== void 0 ? { re } : {},
628
+ text,
629
+ ...labels ? { labels } : {},
630
+ ...noSync !== void 0 ? { noSync } : {},
631
+ ...attachments ? { attachments } : {}
632
+ }, signal, (screened) => backend.schedule(caller, {
633
+ ...input,
634
+ text: screened.text,
635
+ ...screened.labels ? { labels: screened.labels } : {}
636
+ }, signal));
637
+ }
638
+ /**
516
639
  * The caller's own message, still there, where it may post: what an edit of it changes. One it
517
640
  * can't read may still be its own, as board()'s own code finds it, only if it was written there
518
641
  * without reading too: one its principal wrote where it reads (a repo's workflows share one) is
@@ -709,22 +832,27 @@ async function createServe(context) {
709
832
  switch (name) {
710
833
  case "post": return importPost(caller, input, signal);
711
834
  case "edit": {
712
- const { id, text } = input;
835
+ const { id, text, revision } = input;
713
836
  return change({
714
837
  op: "edit",
715
838
  author,
716
839
  from: null,
717
840
  message: id,
718
841
  text: await withEmails(text),
719
- source
842
+ source,
843
+ ...revision !== void 0 ? { revision } : {}
844
+ });
845
+ }
846
+ case "delete": {
847
+ const { id, revision } = input;
848
+ return change({
849
+ op: "delete",
850
+ author,
851
+ from: null,
852
+ message: id,
853
+ ...revision !== void 0 ? { revision } : {}
720
854
  });
721
855
  }
722
- case "delete": return change({
723
- op: "delete",
724
- author,
725
- from: null,
726
- message: input.id
727
- });
728
856
  case "react": {
729
857
  const { id, emoji, remove } = input;
730
858
  return change({
@@ -737,14 +865,15 @@ async function createServe(context) {
737
865
  });
738
866
  }
739
867
  case "label": {
740
- const { id, set, unset } = input;
868
+ const { id, set, unset, if: expected } = input;
741
869
  return change({
742
870
  op: "labels",
743
871
  author,
744
872
  from: null,
745
873
  message: id,
746
874
  ...set ? { set } : {},
747
- ...unset ? { unset } : {}
875
+ ...unset ? { unset } : {},
876
+ ...expected ? { if: expected } : {}
748
877
  });
749
878
  }
750
879
  default: return invoke(name, me, input, signal);
@@ -780,7 +909,8 @@ async function createServe(context) {
780
909
  ...input.labels ? { labels: input.labels } : {},
781
910
  ...input.noSync ? { noSync: true } : {},
782
911
  ...input.attachments?.length ? { attachments: input.attachments } : {},
783
- ...input.id !== void 0 ? { id: input.id } : {}
912
+ ...input.id !== void 0 ? { id: input.id } : {},
913
+ ...input.after !== void 0 ? { after: input.after } : {}
784
914
  }, signal));
785
915
  for (const target of targets) await backend.syncReceipt(me, {
786
916
  kind: "thread",
@@ -791,32 +921,36 @@ async function createServe(context) {
791
921
  }, signal);
792
922
  return { message: await read(message) };
793
923
  }
794
- /** The protocol method `name`, as board() serves it: what it posts and edits with the emails they mention resolved. */
924
+ /** The protocol method `name`, as board() serves it: what it posts and edits, and the filters it reads by, with their emails resolved. */
795
925
  async function invoke(name, caller, input, signal) {
926
+ if (name === "messages" || name === "threads" || name === "events") input = await withMentioned(caller, input);
796
927
  switch (name) {
797
928
  case "post": return post(caller, input, signal);
798
929
  case "edit": {
799
- const { id, text } = input;
930
+ const edit = input;
800
931
  const send = (body) => screenEdit(caller, body, signal, (patched) => backend.edit(caller, patched, signal));
801
- if (!engine || !mentionsEmails(text) || !await mayPost(caller, { re: id }, signal)) return send({
802
- id,
803
- text
804
- });
932
+ if (!engine || !mentionsEmails(edit.text) || !await mayPost(caller, { re: edit.id }, signal)) return send(edit);
805
933
  return send({
806
- id,
807
- text: await withEmails(text)
934
+ ...edit,
935
+ text: await withEmails(edit.text)
808
936
  });
809
937
  }
810
938
  case "syncImport": return syncImport(caller, input, signal);
939
+ case "grants":
940
+ case "createGrant":
941
+ case "revokeGrant":
942
+ if (backend.keeps && !policy.takesGrants) throw new HttpError(501, "no_grants", "this board's access is one function, which decides alone: it keeps no grants");
943
+ return backend[name].call(backend, caller, input, signal);
811
944
  case "schedule": {
812
945
  const body = input;
946
+ const schedule = (made) => backend.sendsSchedulesHere === false ? screenSchedule(caller, made, signal) : backend.schedule(caller, made, signal);
813
947
  const allowed = engine && mentionsEmails(body.text) ? await mayPost(caller, body, signal) : null;
814
- if (!allowed) return backend.schedule(caller, body, signal);
948
+ if (!allowed) return schedule(body);
815
949
  const text = await withEmails(body.text);
816
- const answer = await backend.schedule(caller, {
950
+ const answer = await schedule({
817
951
  ...body,
818
952
  text
819
- }, signal);
953
+ });
820
954
  const { access } = allowed;
821
955
  return text !== body.text && access !== null && !access.readable(answer.schedule.channel) ? {
822
956
  ...answer,
@@ -844,35 +978,6 @@ async function createServe(context) {
844
978
  }
845
979
  }
846
980
  }
847
- /**
848
- * A sign-in that carries a code, where the entry's sync runs: a browser opens it with no token,
849
- * so the code is all it has. `signIn` sends the person to the platform as whoever the code was
850
- * issued to; `signedIn`, where the platform sends them back, links the account they approved.
851
- */
852
- async function signingIn(name, url, signal) {
853
- const { sync: key, code } = SignInQuery.parse(queryObject(url));
854
- if (!engine || !entries[key]) throw notFound(`no sync entry named ${key}`);
855
- const page = (status, text) => new Response(`${text}\n`, {
856
- status,
857
- headers: { "content-type": "text/plain; charset=utf-8" }
858
- });
859
- const { holder } = await backend.syncFind(engine.caller, { code }, signal);
860
- if (holder === null) return page(404, "That link has expired or been used. Run `vb identity link` again for a new one.");
861
- if (name === "signIn") {
862
- const back = ownUrl();
863
- const at = await engine.signIn(key, holder, back ? `${back}/identity/signed-in?${new URLSearchParams({
864
- sync: key,
865
- code
866
- })}` : void 0);
867
- return new Response(null, {
868
- status: 302,
869
- headers: { location: at }
870
- });
871
- }
872
- const linked = await engine.redeemSignIn(key, holder, code);
873
- if (!linked) return page(409, `${entries[key].platform} hasn't said you approved yet: finish signing in, then open this page again.`);
874
- return page(200, `Linked ${linked.handle ? `@${linked.handle}` : linked.actor}. You can close this tab.`);
875
- }
876
981
  /** board()'s own routes for a caller: sync's, which run where board() does. */
877
982
  async function serve(name, caller, request, url, params) {
878
983
  const signal = request.signal;
@@ -899,19 +1004,6 @@ async function createServe(context) {
899
1004
  attached: attached ?? true
900
1005
  });
901
1006
  }
902
- case "identityUnlink": {
903
- const body = IdentityUnlinkBody.parse(await readJson(request));
904
- const { links } = await backend.identity(caller, {}, signal);
905
- const wanted = body.actor.replace(/^@/, "").toLowerCase();
906
- const found = links.filter((link) => link.actor === body.actor || link.handle?.toLowerCase() === wanted).map((link) => link.actor);
907
- if (found.length === 0) throw notFound(`${body.actor} isn't linked to you`, "not_linked");
908
- if (found.length > 1) throw badRequest(`${body.actor} names several of your accounts: use ${found.join(" or ")}`, "ambiguous_actor");
909
- await backend.syncUnlink(caller, { account: found[0] }, signal);
910
- return json({
911
- ok: true,
912
- actor: found[0]
913
- });
914
- }
915
1007
  default: throw notFound(`no route: ${request.method} ${url.pathname}`);
916
1008
  }
917
1009
  }
@@ -1020,7 +1112,6 @@ async function createServe(context) {
1020
1112
  if (!engine || !entries[match.params.key ?? ""]) throw notFound(`no sync entry named ${match.params.key}`);
1021
1113
  return await engine.handleWebhook(match.params.key, request);
1022
1114
  }
1023
- if (target.kind === "board" && (target.name === "signIn" || target.name === "signedIn")) return await signingIn(target.name, url, request.signal);
1024
1115
  if (target.kind === "board" && target.name === "avatar") {
1025
1116
  const name = match.param ?? "";
1026
1117
  if (!KNOWN_AGENTS.includes(name)) throw notFound(`no avatar: ${name}`);
@@ -1042,10 +1133,11 @@ async function createServe(context) {
1042
1133
  const caller = own ?? await callerOf(asked);
1043
1134
  if (target.kind === "board") return await serve(target.name, caller, request, url, match.params);
1044
1135
  const input = await inputOf(target.name, request, url, match.params);
1045
- if (target.name === "events" && (request.headers.get("accept") ?? "").includes("text/event-stream")) return await sse(caller, input, request, asked ? () => callerOf(asked) : null);
1136
+ if (target.name === "events" && (request.headers.get("accept") ?? "").includes("text/event-stream")) return await sse(caller, await withMentioned(caller, input), request, asked ? () => callerOf(asked) : null);
1046
1137
  const output = injected && "platform" in injected ? await asPlatform(target.name, injected, input, request.signal) : await invoke(target.name, caller, input, request.signal);
1047
1138
  if (target.name === "attachment") return attachmentResponse(output);
1048
- return json(output, CREATED.has(target.name) ? 201 : 200);
1139
+ const made = CREATED.has(target.name) || target.name === "createChannel" && output.created;
1140
+ return json(output, made ? 201 : 200);
1049
1141
  } catch (error) {
1050
1142
  return toErrorResponse(error);
1051
1143
  }
@@ -1110,7 +1202,7 @@ const EVENTS_WAIT_S = 25;
1110
1202
  */
1111
1203
  function createBoard(options, runtime) {
1112
1204
  const backend = options.backend ?? runtime.http();
1113
- const policy = createPolicy(options, backend.keeps, runtime.serving);
1205
+ const policy = createPolicy(options, backend.keeps, runtime.serving, backend.trust ?? null);
1114
1206
  const watch = options.watch ?? {};
1115
1207
  const entries = options.sync ?? {};
1116
1208
  const state = { learned: null };
@@ -1218,8 +1310,7 @@ function createBoard(options, runtime) {
1218
1310
  /** This board's sync entries, as its sync and discovery see them. */
1219
1311
  const ownEntries = Object.entries(entries).map(([key, entry]) => ({
1220
1312
  key,
1221
- platform: entry.platform,
1222
- signIn: entry.users !== null
1313
+ platform: entry.platform
1223
1314
  }));
1224
1315
  const binding = {
1225
1316
  subscriptions,
@@ -1252,6 +1343,7 @@ function createBoard(options, runtime) {
1252
1343
  typing: async (messages, status) => (await server()).typing(messages, status),
1253
1344
  accessNow: (principal) => policy.accessNow(principal),
1254
1345
  project: () => policy.project(),
1346
+ issuers: () => policy.issuers,
1255
1347
  handle: (caller) => handleOf(caller, null),
1256
1348
  url: ownUrl
1257
1349
  };
@@ -0,0 +1,5 @@
1
+ import "./auth-DLYSt_-A.mjs";
2
+ import "./folds-DLUzfibJ.mjs";
3
+ import "./client-COfYrgfV.mjs";
4
+ import "./backend-Bob-5CM7.mjs";
5
+ import "./errors-2vGs49Db.mjs";