@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,8 +1,8 @@
1
1
  import { n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
- import { S as storable, a as AttachmentId, c as ChannelPath, d as LabelKey, f as LabelValue, g as MessageText, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, p as Labels, x as platformAvatar } from "./schemas-DBmUO6pr.mjs";
2
+ import { S as storable, a as AttachmentId, c as ChannelPath, d as LabelKey, f as LabelValue, g as MessageText, l as Emoji, m as MESSAGE_TEXT_MAX, p as Labels, x as platformAvatar } from "./schemas-DBmUO6pr.mjs";
3
3
  import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin, t as ExternalAuthor } from "./external-D3p5T57R.mjs";
4
- import { r as badRequest, t as HttpError } from "./errors-C_5rB8Iz.mjs";
5
- import { o as options } from "./commands-B5DHjZKK.mjs";
4
+ import { r as badRequest, t as HttpError } from "./errors-CJXhhzvT.mjs";
5
+ import { o as options } from "./commands-L1JSGz2Z.mjs";
6
6
  import { z } from "zod";
7
7
  //#region src/api/routes.ts
8
8
  /**
@@ -14,6 +14,13 @@ const Ref = z.string().min(1).max(128);
14
14
  /** The message's `revision` as it was read: if it's been edited or deleted since, 409 `revision_conflict`. */
15
15
  const Revision = z.number().int().positive();
16
16
  const id = Ref;
17
+ /** A boolean query parameter: `true` or `1`, `false` or `0`. */
18
+ const flag = z.enum([
19
+ "true",
20
+ "false",
21
+ "1",
22
+ "0"
23
+ ]).transform((value) => value === "true" || value === "1");
17
24
  const repeated = z.union([z.string(), z.array(z.string())]).transform((value) => Array.isArray(value) ? value : [value]);
18
25
  /**
19
26
  * `pr` → `['pr', null]` (has the label), `status=open` → `['status', ['open']]`,
@@ -53,7 +60,7 @@ const ThreadsQuery = Filters.extend({
53
60
  const EventsQuery = Filters.extend({
54
61
  after: Ref.optional(),
55
62
  wait: z.coerce.number().min(0).max(300).optional(),
56
- all: z.coerce.boolean().optional()
63
+ all: flag.optional()
57
64
  });
58
65
  const PostBody = z.object({
59
66
  channel: ChannelPath.optional(),
@@ -119,7 +126,27 @@ const SchedulesQuery = z.object({
119
126
  /** An actor ID or handle: the schedules it created. */
120
127
  by: z.string().optional(),
121
128
  /** Include done and canceled schedules. */
122
- all: z.coerce.boolean().optional()
129
+ all: flag.optional()
130
+ });
131
+ const CanGrant = z.union([z.literal("all"), z.array(z.string().min(1).max(16)).max(8)]);
132
+ /** What a grant or token lets do: `'all'`, a list for every channel, or a map from channel pattern to one. board() checks the patterns. */
133
+ const Can = z.union([CanGrant, z.record(z.string().min(1).max(300), CanGrant)]);
134
+ const ClaimValue = z.string().min(1).max(1e3);
135
+ /** Who a grant lets in: an issuer (`vercel`, `github`, or an https URL) and the claims its tokens carry, exactly. */
136
+ const GrantBody = z.object({
137
+ issuer: z.string().min(1).max(1e3),
138
+ claims: z.record(z.string().min(1).max(200), z.union([ClaimValue, z.array(ClaimValue).min(1).max(32)])),
139
+ can: Can.optional()
140
+ });
141
+ /** A token acting as the caller: what it may do, and the profile and session every request with it writes as. */
142
+ const TokenBody = z.object({
143
+ name: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/, "a token name is up to 64 letters, numbers, and ._-"),
144
+ can: Can.optional(),
145
+ profile: z.string().regex(/^[A-Za-z][A-Za-z0-9-]{0,31}$/, "a profile is up to 32 letters, numbers, and hyphens").optional(),
146
+ avatar: z.string().url().max(2048).optional(),
147
+ session: z.string().regex(/^[A-Za-z0-9_.:-]{1,128}$/, "a session is up to 128 letters, numbers, and _.:-").optional(),
148
+ /** How long it lives: `30d` by default, at most `90d`. */
149
+ expires: Duration.optional()
123
150
  });
124
151
  const SyncBody = z.object({ target: z.string().min(3).max(300) });
125
152
  const InboxQuery = z.object({
@@ -134,12 +161,11 @@ const Registering = z.object({
134
161
  })).refine((watch) => Object.keys(watch).length <= 32, `a board registers at most 32 watchers`),
135
162
  /**
136
163
  * Its sync entries: its sync subscribes too, to every post, change, and thread sync in the channels
137
- * where it may sync, and the board lists the entries and links accounts on them.
164
+ * where it may sync, and the board lists the entries.
138
165
  */
139
166
  sync: z.array(z.object({
140
167
  key: z.string().min(1).max(64),
141
- platform: z.string().min(1).max(64),
142
- signIn: z.boolean().default(false)
168
+ platform: z.string().min(1).max(64)
143
169
  })).max(32).optional()
144
170
  });
145
171
  /**
@@ -191,7 +217,6 @@ const SourceReadQuery = z.object({
191
217
  const SourceJoinBody = z.object({ target: z.string().min(1).max(4e3) });
192
218
  /** A platform's link to a message or thread, such as a Slack permalink or a GitHub comment's URL. */
193
219
  const LinkedQuery = z.object({ url: z.string().url().max(4e3) });
194
- const IdentityUnlinkBody = z.object({ actor: z.string().min(1).max(256) });
195
220
  /** A sync entry's key: `slack`, `github`. */
196
221
  const SyncKey = z.string().min(1).max(64);
197
222
  /** A platform message an import changes, by its platform thread and ID; without a message, the root of the board thread the platform thread belongs to. */
@@ -301,8 +326,7 @@ const SyncReceiptBody = z.discriminatedUnion("kind", [
301
326
  target: ExternalTarget,
302
327
  external: z.object({
303
328
  thread: ExternalTarget,
304
- message: z.string().min(1).max(300),
305
- asUser: z.literal(true).optional()
329
+ message: z.string().min(1).max(300)
306
330
  }),
307
331
  /** Its files, when they went with it: the platform's IDs, or null when it refused them. */
308
332
  files: PlatformFiles.optional()
@@ -329,24 +353,15 @@ const SyncReceiptBody = z.discriminatedUnion("kind", [
329
353
  cause: z.enum(["config", "request"])
330
354
  })
331
355
  ]);
332
- /** What sync looks up by platform ID: a thread or message, who an account is, and a person's accounts. */
356
+ /** What sync looks up by platform ID: a thread or a message. */
333
357
  const SyncFindQuery = z.object({
334
- /** A live one-time code: who it was issued to, for a sign-in that carries it. */
335
- code: z.string().min(1).max(64).optional(),
336
358
  /** The post the caller's sync imported with this client ID. */
337
359
  post: z.string().min(1).max(200).optional(),
338
360
  thread: ExternalTarget.optional(),
339
361
  /** With `thread`: the platform message in it. */
340
- message: z.string().min(1).max(300).optional(),
341
- account: ActorId.optional(),
342
- person: ActorId.optional()
362
+ message: z.string().min(1).max(300).optional()
343
363
  });
344
364
  const SyncCopiesQuery = z.object({ message: repeated.pipe(z.array(Ref).min(1).max(100)) });
345
- /** A person's own roots since `since`, in epoch milliseconds. */
346
- const SyncRootsQuery = z.object({
347
- by: ActorId,
348
- since: z.coerce.number().int().nonnegative()
349
- });
350
365
  /** Platform events for a sync key's inbox, each with the platform's own ID for it, which dedupes it. */
351
366
  const InboxPushBody = z.object({ events: z.array(z.object({
352
367
  id: z.string().min(1).max(300),
@@ -358,26 +373,6 @@ const InboxAckBody = z.object({
358
373
  lease: z.string().min(1).max(64),
359
374
  error: z.string().min(1).max(500).optional()
360
375
  });
361
- /** A one-time code for linking an account on sync entry `sync`: a person's own, for themselves. */
362
- const SyncCodeBody = z.object({ sync: SyncKey });
363
- /**
364
- * A platform account, linked to whoever a code was issued to: one posted from the account, or
365
- * carried through its sign-in. `message` is the platform message that posted it.
366
- */
367
- const SyncAccountBody = z.object({
368
- sync: SyncKey,
369
- account: PlatformUser,
370
- code: z.string().min(1).max(64),
371
- message: z.string().max(1024).optional(),
372
- /** `consent` when the code came through the account's own sign-in, so the board posts as them. */
373
- method: z.enum(["code", "consent"]).optional()
374
- });
375
- /** A sign-in that carries a code, on the board whose sync runs the entry. */
376
- const SignInQuery = z.object({
377
- sync: SyncKey,
378
- code: z.string().min(1).max(64)
379
- });
380
- const SyncUnlinkBody = z.object({ account: ActorId });
381
376
  /**
382
377
  * A direct upload, before its bytes: what the file will be called, its type, its exact size, and
383
378
  * the SHA-256 of its bytes, in base64, for a store that signs its URL for them (Vercel's).
@@ -393,12 +388,14 @@ const EditBody = z.object({
393
388
  revision: Revision.optional()
394
389
  });
395
390
  const DeleteBody = z.object({ revision: Revision.optional() }).optional();
396
- /** `status`: what Slack shows in place of "Typing…" (WhatsApp and GitHub ignore it). */
391
+ /** `status`: what Slack shows in place of "Typing…" (other platforms ignore it). */
397
392
  const TypingBody = z.object({ status: z.string().trim().min(1).max(100).optional() }).optional();
398
393
  const ReactBody = z.object({
399
394
  emoji: Emoji,
400
395
  remove: z.boolean().optional()
401
396
  });
397
+ /** A channel to make before anything posts to it. */
398
+ const CreateChannelBody = z.object({ path: ChannelPath });
402
399
  const LabelBody = z.object({
403
400
  set: Labels.optional(),
404
401
  unset: z.array(LabelKey).optional(),
@@ -410,7 +407,7 @@ const LabelBody = z.object({
410
407
  }).refine((body) => body.set !== void 0 || body.unset !== void 0, { message: "nothing to change: pass `set` or `unset`" });
411
408
  /** The route table: method, path, and the schemas that validate input. */
412
409
  const routes = {
413
- /** Where the board is, its project, and the sync entries it links accounts on. No token. */
410
+ /** Where the board is, its project, and its sync entries. No token. */
414
411
  discovery: {
415
412
  method: "GET",
416
413
  path: "/"
@@ -423,6 +420,12 @@ const routes = {
423
420
  method: "GET",
424
421
  path: "/channels"
425
422
  },
423
+ /** Makes a channel before anything posts to it: needs `read` and `post` there. One that's there already is left as it is. */
424
+ createChannel: {
425
+ method: "POST",
426
+ path: "/channels",
427
+ body: CreateChannelBody
428
+ },
426
429
  actors: {
427
430
  method: "GET",
428
431
  path: "/actors"
@@ -506,6 +509,34 @@ const routes = {
506
509
  method: "POST",
507
510
  path: "/schedules/:id/cancel"
508
511
  },
512
+ /** Grants need manage in every channel. Setting one with the same issuer and claims changes its `can`. */
513
+ grants: {
514
+ method: "GET",
515
+ path: "/grants"
516
+ },
517
+ createGrant: {
518
+ method: "POST",
519
+ path: "/grants",
520
+ body: GrantBody
521
+ },
522
+ revokeGrant: {
523
+ method: "POST",
524
+ path: "/grants/:id/revoke"
525
+ },
526
+ /** Your tokens, or every one with manage in every channel. A token acts as whoever minted it. */
527
+ tokens: {
528
+ method: "GET",
529
+ path: "/tokens"
530
+ },
531
+ createToken: {
532
+ method: "POST",
533
+ path: "/tokens",
534
+ body: TokenBody
535
+ },
536
+ revokeToken: {
537
+ method: "POST",
538
+ path: "/tokens/:id/revoke"
539
+ },
509
540
  /** The caller's own post with the client ID it gave: what `POST /messages` answered, for telling a retry before posting. */
510
541
  posted: {
511
542
  method: "GET",
@@ -564,30 +595,6 @@ const routes = {
564
595
  path: "/sync/copies",
565
596
  query: SyncCopiesQuery
566
597
  },
567
- /** A person's recent roots, for telling their own platform copies from new messages. */
568
- syncRoots: {
569
- method: "GET",
570
- path: "/sync/roots",
571
- query: SyncRootsQuery
572
- },
573
- /** A one-time code for linking a platform account. */
574
- syncCode: {
575
- method: "POST",
576
- path: "/sync/codes",
577
- body: SyncCodeBody
578
- },
579
- /** Links a platform account, by a code or for a person. */
580
- syncAccount: {
581
- method: "POST",
582
- path: "/sync/accounts",
583
- body: SyncAccountBody
584
- },
585
- /** Unlinks a platform account. */
586
- syncUnlink: {
587
- method: "POST",
588
- path: "/sync/accounts/unlink",
589
- body: SyncUnlinkBody
590
- },
591
598
  /** Reads a platform channel or thread on demand, where that sync runs, without syncing or storing it. Full access only. */
592
599
  readSource: {
593
600
  method: "GET",
@@ -637,28 +644,6 @@ const routes = {
637
644
  method: "GET",
638
645
  path: "/openapi.json"
639
646
  },
640
- /** Your linked platform accounts. */
641
- identity: {
642
- method: "GET",
643
- path: "/identity"
644
- },
645
- /** Starts a sign-in that carries a code (no token), on the board that runs the entry's sync. */
646
- signIn: {
647
- method: "GET",
648
- path: "/identity/sign-in",
649
- query: SignInQuery
650
- },
651
- /** Where a sign-in comes back (no token): links the account it authorized to the code's person. */
652
- signedIn: {
653
- method: "GET",
654
- path: "/identity/signed-in",
655
- query: SignInQuery
656
- },
657
- identityUnlink: {
658
- method: "POST",
659
- path: "/identity/unlink",
660
- body: IdentityUnlinkBody
661
- },
662
647
  /** Watchers registered by boards on `http()`: every one to `manage` everywhere, the caller's own to anyone else. */
663
648
  watchers: {
664
649
  method: "GET",
@@ -693,7 +678,7 @@ const routes = {
693
678
  const SYNC = "@sync";
694
679
  //#endregion
695
680
  //#region src/version.ts
696
- const VERSION = "0.30.0";
681
+ const VERSION = "0.31.0";
697
682
  //#endregion
698
683
  //#region src/server/routing.ts
699
684
  /** board()'s own routes: the files it serves, and sync's, which run where board() does. */
@@ -701,9 +686,6 @@ const BOARD_ROUTES = [
701
686
  "avatar",
702
687
  "openapi",
703
688
  "webhook",
704
- "signIn",
705
- "signedIn",
706
- "identityUnlink",
707
689
  "sync"
708
690
  ];
709
691
  /** Every protocol method, by the route it's served at. */
@@ -711,8 +693,8 @@ const METHODS = [
711
693
  "discovery",
712
694
  "me",
713
695
  "channels",
696
+ "createChannel",
714
697
  "actors",
715
- "identity",
716
698
  "messages",
717
699
  "threads",
718
700
  "message",
@@ -725,6 +707,12 @@ const METHODS = [
725
707
  "schedule",
726
708
  "schedules",
727
709
  "cancelSchedule",
710
+ "grants",
711
+ "createGrant",
712
+ "revokeGrant",
713
+ "tokens",
714
+ "createToken",
715
+ "revokeToken",
728
716
  "upload",
729
717
  "attachment",
730
718
  "uploadStart",
@@ -739,14 +727,10 @@ const METHODS = [
739
727
  "syncThread",
740
728
  "syncFind",
741
729
  "syncCopies",
742
- "syncRoots",
743
730
  "inboxPush",
744
731
  "inboxNext",
745
732
  "inboxAck",
746
733
  "inbox",
747
- "syncCode",
748
- "syncAccount",
749
- "syncUnlink",
750
734
  "typing",
751
735
  "readSource",
752
736
  "joinSource",
@@ -1041,12 +1025,8 @@ function canonicalWatchers(watch, sync = []) {
1041
1025
  watch[id].tail,
1042
1026
  watch[id].when
1043
1027
  ]);
1044
- if (sync.length > 0) canonical.push(["sync", [...sync].sort((a, b) => byCodeUnit(a.key, b.key)).map((entry) => [
1045
- entry.key,
1046
- entry.platform,
1047
- entry.signIn
1048
- ])]);
1028
+ if (sync.length > 0) canonical.push(["sync", [...sync].sort((a, b) => byCodeUnit(a.key, b.key)).map((entry) => [entry.key, entry.platform])]);
1049
1029
  return JSON.stringify(canonical);
1050
1030
  }
1051
1031
  //#endregion
1052
- export { ScheduleBody as A, SyncImportBody as B, LinkedQuery as C, ReactBody as D, PostBody as E, SyncAccountBody as F, TypingBody as G, SyncRootsQuery as H, SyncBody as I, UploadStartBody as K, SyncCodeBody as L, SignInQuery as M, SourceJoinBody as N, Ref as O, SourceReadQuery as P, SyncCopiesQuery as R, LabelBody as S, MessagesQuery as T, SyncUnlinkBody as U, SyncReceiptBody as V, ThreadsQuery as W, IdentityUnlinkBody as _, METHODS as a, InboxPushBody as b, queryObject as c, VERSION as d, SYNC as f, EventsQuery as g, EditBody as h, withMentions as i, SchedulesQuery as j, RegisterBody as k, readCapped as l, DeleteBody as m, compileWatcher as n, createRouter as o, AckBody as p, routes as q, hasCriteria as r, openapi as s, canonicalWatchers as t, readJson as u, InboxAckBody as v, MatchesQuery as w, InboxQuery as x, InboxNextQuery as y, SyncFindQuery as z };
1032
+ export { RegisterBody as A, ThreadsQuery as B, LabelBody as C, PostBody as D, MessagesQuery as E, SyncBody as F, TypingBody as H, SyncCopiesQuery as I, SyncFindQuery as L, SchedulesQuery as M, SourceJoinBody as N, ReactBody as O, SourceReadQuery as P, SyncImportBody as R, InboxQuery as S, MatchesQuery as T, UploadStartBody as U, TokenBody as V, routes as W, EventsQuery as _, METHODS as a, InboxNextQuery as b, queryObject as c, VERSION as d, SYNC as f, EditBody as g, DeleteBody as h, withMentions as i, ScheduleBody as j, Ref as k, readCapped as l, CreateChannelBody as m, compileWatcher as n, createRouter as o, AckBody as p, hasCriteria as r, openapi as s, canonicalWatchers as t, readJson as u, GrantBody as v, LinkedQuery as w, InboxPushBody as x, InboxAckBody as y, SyncReceiptBody as z };
@@ -60,8 +60,8 @@ export const GET = team.fetch, POST = team.fetch
60
60
 
61
61
  A board on `http()` keeps nothing:
62
62
 
63
- - **It answers only wake-ups and webhooks,** and the sign-in routes of the
64
- sync it runs. Anything else, discovery included, answers 404 `<its URL>
63
+ - **It answers only wake-ups and webhooks.** Anything else, discovery
64
+ included, answers 404 `<its URL>
65
65
  answers only wake-ups and webhooks`, and verifies nothing. People and the
66
66
  CLI use the other board, by its URL. With an
67
67
  `access` of its own, it serves its callers too, as a gateway: see
@@ -13,8 +13,7 @@ a host can recognize its own credentials too: see
13
13
  [A host's own verifier](./09-access.md#a-hosts-own-verifier).
14
14
 
15
15
  Only a few routes take no token: discovery, the built-in avatars, the OpenAPI
16
- document, the sync sign-in pages (`/identity/sign-in` and `/identity/signed-in`),
17
- and `/webhooks/:key`, which the platform's adapter authenticates instead. A
16
+ document, and `/webhooks/:key`, which the platform's adapter authenticates instead. A
18
17
  board on `http()` also answers `POST /wake` without one: a wake-up only says
19
18
  to pull.
20
19
 
@@ -24,7 +23,7 @@ The CLI and `board()` in Node find a token themselves. The first match wins:
24
23
 
25
24
  | Where | How | Principal |
26
25
  | --- | --- | --- |
27
- | Explicit | `BOARD_TOKEN`: an OIDC JWT for the board's audience, from Vercel, GitHub Actions, or an issuer `access` names | depends on the issuer |
26
+ | Explicit | `BOARD_TOKEN`: an OIDC JWT for the board's audience, from Vercel, GitHub Actions, or an issuer `access` or a grant names; or a [board token](./09-access.md#tokens) | the issuer's, or a board token's minter |
28
27
  | A board from `vb serve` | the token it writes to a file only you can read, for its `127.0.0.1` URL | `local:user:<your OS user>` |
29
28
  | GitHub Actions | the job's OIDC token, for the board's audience; needs `permissions: id-token: write` | `github:repo:<repository_id>` |
30
29
  | A Vercel deployment | `getVercelOidcToken({ audience })` from `@vercel/oidc`, when `VERCEL=1` | `vercel:project:<project_id>:<env>` |
@@ -268,6 +267,6 @@ exist ([Access](./09-access.md#what-a-caller-sees)). Mentions are stored as acto
268
267
  IDs, so a later handle change never reroutes an old one, and edits resolve them
269
268
  again. Platform text never fails a write: a mention there that doesn't resolve
270
269
  stays plain text. On their own platform, mentions render natively: `<@U0456>` in
271
- Slack, `@login` on GitHub. A person who linked accounts (`vb identity link`)
272
- renders natively wherever they linked one: `@julian` or their GitHub account
273
- reads as `<@U0456>` in Slack.
270
+ Slack, `@login` on GitHub. Anyone else, a board person included, reads as their
271
+ name there: mention a Slack user in Slack by their actor
272
+ (`@[slack:user:U0456]`) or their email (`@[ada@vercel.com]`).
@@ -11,8 +11,9 @@ vb post www/production "rollout paused"
11
11
  ```
12
12
 
13
13
  A channel is a path: one or more segments of lowercase letters, numbers, `_`,
14
- and `-`, joined by `/`, up to 80 characters. The first post creates it, and its
15
- path never changes. Prefixes are display groups: `vb channels` lists
14
+ and `-`, joined by `/`, up to 80 characters. The first post creates it, or
15
+ `vb channels create` does before that, and its path never changes. Prefixes
16
+ are display groups: `vb channels` lists
16
17
  `www/production` and `www/preview` under `www/`, and filters take `www/` for the
17
18
  whole subtree.
18
19
 
@@ -74,7 +74,7 @@ read includes it.
74
74
  | `<channel>` | that channel; `www/` with a trailing slash for the subtree |
75
75
  | `--re <id>` | the thread rooted at `<id>`: the root and its replies |
76
76
  | `--by <handle>` | messages by that actor: a handle without the `@`, or an exact actor ID |
77
- | `--mentions` | messages that mention you; `--mentions @ada` for someone else |
77
+ | `--mentions` | messages that mention you; `--mentions @ada` for someone else, or `--mentions ada@vercel.com` for whoever has that email on Slack |
78
78
  | `--source <source>` | where it was written: a sync key (`slack`, `github`), or `board` |
79
79
  | `--label <key>` | messages with that label; `key=value`, or `key=a\|b` for either |
80
80
  | `--text <words>` | text containing the words, case-insensitive (a substring, not search) |
@@ -84,6 +84,7 @@ read includes it.
84
84
  vb read prs --label state=open # what's pending
85
85
  vb read issues --label tag.bug --source github
86
86
  vb read --mentions # everywhere I'm mentioned
87
+ vb tail --mentions julian@vercel.com # Slack pings of me, as they land
87
88
  vb read errors-www --by ada --text ENOTFOUND
88
89
  ```
89
90
 
@@ -49,7 +49,9 @@ A channel (or `prefix/` for a subtree) and the shared
49
49
  - **Positions aren't filters.** A watcher starts at the head when it's deployed
50
50
  and sees every match after it, so `--after` fails the boot.
51
51
  - **A watcher has no "me".** `--mentions` alone fails; name a handle:
52
- `--mentions @ada`, or `--mentions @julian/claude` for an agent.
52
+ `--mentions @ada`, or `--mentions @julian/claude` for an agent. A watcher
53
+ doesn't look emails up: name a Slack user by actor
54
+ (`--mentions slack:user:U0456`).
53
55
  - **A bad id or expression fails the boot** with the CLI's error:
54
56
  `watch triage: --after isn't a filter`.
55
57
  - **Ids** are lowercase letters, numbers, `-`, and `_`.
@@ -251,8 +253,8 @@ export const GET = team.fetch, POST = team.fetch
251
253
  for the next match to wake this board. A board that pulls on its own
252
254
  doesn't wait.
253
255
  - **Its [sync](../02-sync/01-overview.md#on-a-board-elsewhere) subscribes the
254
- same way,** with its entries in its registration (`sync: [{ key, platform,
255
- signIn }]`): it gets every post, change, and thread sync in the channels its
256
- deployment may `sync`, as matches of watcher `@sync`, and runs them here. The
257
- other board lists those entries in its discovery and links accounts on them,
258
- and refuses a key its own sync has (409 `sync_key_taken`).
256
+ same way,** with its entries in its registration (`sync: [{ key, platform }]`):
257
+ it gets every post, change, and thread sync in the channels its deployment
258
+ may `sync`, as matches of watcher `@sync`, and runs them here. The other
259
+ board lists those entries in its discovery, and refuses a key its own sync
260
+ has (409 `sync_key_taken`).
@@ -34,7 +34,7 @@ export const { scheduler } = backend
34
34
  ```
35
35
 
36
36
  Every post and edit goes through `middleware`, in order, before it lands: the
37
- API (the CLI, scripts, CI), messages synced in from Slack, GitHub, and WhatsApp
37
+ API (the CLI, scripts, CI), messages synced in from Slack, GitHub, and Linear
38
38
  (sync callbacks' writes included), watchers, the board's
39
39
  [handle](./01-mounting.md#the-handle), and scheduled sends. Reactions, labels,
40
40
  and deletes don't. It's `board()`'s, so it runs before the write reaches the