@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
- import { d as ExternalFile, f as ExternalLink, l as ExternalAuthor, p as Link, t as Actor, u as ExternalEvent } from "./folds-8wRRXjAt.mjs";
2
- import { H as MessageView, _ as BoardEventView, ft as SourceJoin, ht as SourceRead, t as BoardClient } from "./client-CjAatpV4.mjs";
1
+ import { d as ExternalFile, f as ExternalLink, l as ExternalAuthor, p as Link, t as Actor, u as ExternalEvent } from "./folds-DLUzfibJ.mjs";
2
+ import { U as MessageView, _ as BoardEventView, dt as SourceJoin, mt as SourceRead, t as BoardClient } from "./client-COfYrgfV.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/sync/events.d.ts
5
5
  declare const EventBase: z.ZodObject<{
@@ -401,33 +401,6 @@ declare function render<E extends ProviderEvent = ProviderEvent>(renderer: Rende
401
401
  */
402
402
  declare function channels<E extends ProviderEvent = ProviderEvent>(map: Record<string, SyncUnit<E>>): SyncFn<E>;
403
403
  //#endregion
404
- //#region src/sync/users.d.ts
405
- /**
406
- * People's own platform tokens, so a board message they wrote reaches Slack or
407
- * GitHub as them, and signing in proves an account is theirs. The board never
408
- * stores a token: it asks for one at each delivery, and a person without one
409
- * (never signed in, revoked, expired) is delivered as the bot, as before.
410
- */
411
- type UserTokens = {
412
- /**
413
- * A sign-in URL that binds whoever completes it to `principal`. Show it only to them. With
414
- * `callback`, the platform sends them there once they've approved.
415
- */
416
- authorize(principal: string, options?: {
417
- callback?: string;
418
- }): Promise<string>;
419
- /** `principal`'s token on the platform, or null when they haven't signed in. */
420
- token(principal: string): Promise<string | null>;
421
- };
422
- /**
423
- * Gives an adapter people's tokens, for your own OAuth: `slack({ adapter:
424
- * withUserTokens(createSlackAdapter(...), { authorize, token }) }, sync)`. A
425
- * Connect connector name (`slack('slack/acme', sync)`) does this for you.
426
- */
427
- declare function withUserTokens<A extends object>(adapter: A, tokens: UserTokens): A;
428
- /** Vercel Connect's user tokens for `connector`: the subject is the board's principal, so Connect binds the account to it. */
429
- declare function connectUserTokens(connector: string): UserTokens;
430
- //#endregion
431
404
  //#region src/sync/provider.d.ts
432
405
  declare const ResolvedTarget: z.ZodObject<{
433
406
  target: z.ZodString;
@@ -449,7 +422,7 @@ type RenderInput = {
449
422
  /** The message as clients see it: its IDs are refs. */
450
423
  message: Readonly<MessageView>;
451
424
  author: Readonly<Actor> | null;
452
- /** How this platform mentions an actor, through linked accounts; null when it can't. */
425
+ /** How this platform mentions an actor when it's one of its own (`<@U123>`); null for anyone else. */
453
426
  mentionOf: (actor: string) => string | null;
454
427
  label: string;
455
428
  target: string;
@@ -498,8 +471,6 @@ type ProviderOptions = {
498
471
  uploadsFiles?: boolean;
499
472
  /** Whether a new thread's root can carry its files in the same message as its text. */
500
473
  startsThreadsWithFiles?: boolean;
501
- /** People's own tokens, to post as them and to link accounts by signing in. */
502
- users?: UserTokens | null;
503
474
  };
504
475
  declare const ReceivedEvent: z.ZodObject<{
505
476
  event: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -629,28 +600,27 @@ declare abstract class SyncProvider<E extends ProviderEvent = ProviderEvent> {
629
600
  readonly uploadsFiles: boolean;
630
601
  readonly startsThreadsWithFiles: boolean;
631
602
  private readonly fn;
632
- /** People's tokens, when the adapter has them: messages they write post as them. */
633
- readonly users: UserTokens | null;
634
603
  protected readonly options: ProviderOptions;
635
604
  constructor(options: ProviderOptions, sync: SyncFn<E>);
636
605
  abstract receive(key: string, request: Request, emit: Receive): Promise<Response>;
637
606
  abstract resolveChannel(key: string): Promise<string>;
638
607
  abstract resolveTarget(target: string): Promise<ResolvedTarget>;
639
608
  abstract post(target: string, kind: ResolvedTarget['kind'], body: ProviderPost): Promise<ProviderDelivery>;
640
- /** The platform account a person's token belongs to. */
641
- whoAmI(_token: string): Promise<ExternalAuthor>;
642
- /** `post`, as the person whose token this is. */
643
- postAs(_token: string, _target: string, _kind: ResolvedTarget['kind'], _body: ProviderPost): Promise<ProviderDelivery>;
644
- editAs(_token: string, _thread: string, _message: string, _body: ProviderPost): Promise<void>;
645
- deleteAs(_token: string, _thread: string, _message: string): Promise<void>;
646
609
  findByEmail(_email: string): Promise<ExternalAuthor | null>;
647
610
  mention(_actor: Actor): string | null;
648
611
  fetchRoot(_thread: string): Promise<SyncRoot | null>;
649
612
  fetchFile(_file: ExternalFile): Promise<Uint8Array | null>;
650
613
  /** The platform's own link to an event's message, or null when it has none. */
651
614
  permalink(_event: E): Promise<string | null>;
652
- /** A linked message the platform didn't describe, read from the platform. */
653
- fetchLink(_link: ExternalLink): Promise<FetchedLink>;
615
+ /**
616
+ * A linked message the platform didn't describe, read from the platform. `linker` is the
617
+ * platform message that links it, by its own IDs: what the platform says about the link may be
618
+ * there by now.
619
+ */
620
+ fetchLink(_link: ExternalLink, _linker: {
621
+ thread: string;
622
+ message: string;
623
+ }): Promise<FetchedLink>;
654
624
  /**
655
625
  * The message a URL of this platform's names, by its own thread and message IDs, or null for a
656
626
  * URL that isn't one. `message` is null for a link to a thread itself. It asks the platform
@@ -692,4 +662,4 @@ declare abstract class SyncProvider<E extends ProviderEvent = ProviderEvent> {
692
662
  render(input: RenderInput): string;
693
663
  }
694
664
  //#endregion
695
- export { SyncOverrides as A, channels as C, EventBase as D, render as E, SyncRoot as M, envelope as N, ProviderEvent as O, eventSchema as P, SyncUnit as S, mirror as T, withUserTokens as _, ProviderOptions as a, SyncIn as b, ReceivedEvent as c, ResolvedTarget as d, SyncContext as f, connectUserTokens as g, UserTokens as h, ProviderFile as i, SyncResult as j, RootOverride as k, RenderInput as l, UnreadableError as m, FileDelivery as n, ProviderPost as o, SyncProvider as p, ProviderDelivery as r, Receive as s, FetchedLink as t, Renderer as u, Sync as v, into as w, SyncOut as x, SyncFn as y };
665
+ export { envelope as A, render as C, SyncOverrides as D, RootOverride as E, SyncResult as O, mirror as S, ProviderEvent as T, SyncIn as _, ProviderOptions as a, channels as b, ReceivedEvent as c, ResolvedTarget as d, SyncContext as f, SyncFn as g, Sync as h, ProviderFile as i, eventSchema as j, SyncRoot as k, RenderInput as l, UnreadableError as m, FileDelivery as n, ProviderPost as o, SyncProvider as p, ProviderDelivery as r, Receive as s, FetchedLink as t, Renderer as u, SyncOut as v, EventBase as w, into as x, SyncUnit as y };
@@ -5,7 +5,7 @@ import { m as nullProtoRecord, n as NOTIFY_TIMINGS, v as stateTailReaders } from
5
5
  import { i as presenceTombstones, n as isSerializedEvent, r as parseSerializedEvent, t as bufferedEvents } from "./stream-buffer-DrTyiIuo-Be10yOBR.mjs";
6
6
  import { i as streamActivities, n as defaultSleep, r as pollingStream, t as createStreamActivity } from "./store-polling-BZt2Rf3D-Div-bJ_D.mjs";
7
7
  import { t as retryableLazy } from "./retryable-lazy-DZWmHpii-DpTc5PIP.mjs";
8
- import { i as syncReads, n as project, r as projectSync, t as concernedMessageId } from "./project-C_2xs57L.mjs";
8
+ import { i as syncReads, n as project, r as projectSync, t as concernedMessageId } from "./project-CQ4VUibA.mjs";
9
9
  import { i as idempotentReplay, n as decodeCause } from "./store-codec-pAG2oPgQ-SqDXlPBR.mjs";
10
10
  import { t as pollPresence } from "./store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs";
11
11
  //#region ../../node_modules/.pnpm/experimental-a2@0.24.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__b1e2c243a963f2620a5d202817417c26/node_modules/experimental-a2/dist/store-redis-notify-ys_-VrSP.js
@@ -3278,12 +3278,6 @@ local out = {}
3278
3278
  for i = 1, #KEYS do out[i] = redis.call('HGETALL', KEYS[i]) end
3279
3279
  return out
3280
3280
  `;
3281
- /** A person's roots posted at or after ARGV[1]. KEYS: messages, their roots by creation time. */
3282
- const RECENT = `
3283
- local ids = redis.call('ZRANGEBYSCORE', KEYS[2], ARGV[1], '+inf')
3284
- if #ids == 0 then return {} end
3285
- return redis.call('HMGET', KEYS[1], unpack(ids))
3286
- `;
3287
3281
  /**
3288
3282
  * What an event reads before it applies, in one script so it reads the primary,
3289
3283
  * where the position check runs: a replica can lag it. Nothing, when the
@@ -3412,8 +3406,6 @@ function redisTables(send, prefix, options = {}) {
3412
3406
  mention: (actor) => `${p}:i:mn:${actor}`,
3413
3407
  attachment: (id) => `${p}:i:at:${id}`,
3414
3408
  from: (from) => `${p}:i:f:${from}`,
3415
- /** A person's roots, by when they were posted: for the echo check. */
3416
- rootsBy: (principal) => `${p}:rr:${principal}`,
3417
3409
  label: (token) => `${p}:i:${token}`,
3418
3410
  copies: (id) => `${p}:cp:${id}`
3419
3411
  };
@@ -3522,7 +3514,6 @@ function redisTables(send, prefix, options = {}) {
3522
3514
  re: m.re,
3523
3515
  c: m.channel,
3524
3516
  t: m.text.toLowerCase(),
3525
- ca: Date.parse(m.createdAt),
3526
3517
  d: m.deleted
3527
3518
  };
3528
3519
  out.push([
@@ -3577,12 +3568,6 @@ function redisTables(send, prefix, options = {}) {
3577
3568
  id,
3578
3569
  id
3579
3570
  ]);
3580
- else out.push([
3581
- "ZADD",
3582
- k.rootsBy(m.by.principal),
3583
- String(meta.ca),
3584
- id
3585
- ]);
3586
3571
  }
3587
3572
  if (change.message.terms) {
3588
3573
  const old = read.message;
@@ -3903,9 +3888,6 @@ function redisTables(send, prefix, options = {}) {
3903
3888
  return message ? [message] : [];
3904
3889
  });
3905
3890
  },
3906
- async recentRoots(principal, since) {
3907
- return (await evaluate(RECENT, [k.messages, k.rootsBy(principal)], [since])).map((row) => parse(row)).sort((a, b) => a.id - b.id);
3908
- },
3909
3891
  async close() {}
3910
3892
  };
3911
3893
  }
@@ -2,7 +2,7 @@ import { S as storable, _ as RemoteWatcherId, c as ChannelPath, d as LabelKey, g
2
2
  import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin } from "./external-D3p5T57R.mjs";
3
3
  import { n as setOwn, t as own } from "./own-OwgPWdmh.mjs";
4
4
  import { a as reducer, i as makeReducerBuilder, t as assertSyncSchema } from "./reducer-Bswr11ar-P324o9vn.mjs";
5
- import { n as foldMessages, t as foldActors } from "./folds-DGYkMW19.mjs";
5
+ import { n as foldMessages, t as foldActors } from "./folds-Cu9Q9zyQ.mjs";
6
6
  import { z } from "zod";
7
7
  //#region ../../node_modules/.pnpm/experimental-a2@0.24.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__b1e2c243a963f2620a5d202817417c26/node_modules/experimental-a2/dist/contract-DqfBSIqJ.js
8
8
  function contract(options) {
@@ -80,6 +80,14 @@ const boardEvents = {
80
80
  */
81
81
  importedBy: ActorId.optional()
82
82
  }),
83
+ /**
84
+ * A channel made before anything posted to it. A post makes one too, with no event of its own:
85
+ * this is only for one that's empty. It names no message, so it carries no `id`.
86
+ */
87
+ "channel.created": z.object({
88
+ channel: ChannelPath,
89
+ by: Author
90
+ }),
83
91
  /** A message's links changed: an edit linked something new, or the platform sent what a link says. */
84
92
  "message.linked": z.object({
85
93
  id: MessageId,
@@ -138,9 +146,7 @@ const boardEvents = {
138
146
  target: ExternalTarget,
139
147
  external: z.object({
140
148
  thread: ExternalTarget,
141
- message: z.string().min(1).max(300),
142
- /** Posted with its author's own platform token, so it shows as them. Older receipts have none. */
143
- asUser: z.literal(true).optional()
149
+ message: z.string().min(1).max(300)
144
150
  }),
145
151
  /**
146
152
  * (server) The principal of the sync elsewhere that recorded it: its copy is that sync's to
@@ -179,22 +185,6 @@ const boardEvents = {
179
185
  * signs in, whatever its ID or kind, so sync never writes as it. Others have none.
180
186
  */
181
187
  host: z.literal(true).optional()
182
- }),
183
- /**
184
- * A platform actor (`slack:user:U0456`) is the same person as a principal
185
- * (`vercel:user:…`): for display, `--by`, and mentions, never for permissions.
186
- * `method` says how it was proved: a one-time code posted from that account,
187
- * or the account's own sign-in (`consent`).
188
- */
189
- "actor.linked": z.object({
190
- actor: ActorId,
191
- to: ActorId,
192
- method: z.enum(["code", "consent"]),
193
- by: Author
194
- }),
195
- "actor.unlinked": z.object({
196
- actor: ActorId,
197
- by: Author
198
188
  })
199
189
  };
200
190
  const BOARD_SESSION = "board";
@@ -328,7 +318,9 @@ const StoredPrincipal = z.object({
328
318
  /** The board's own Vercel team, as the board knew it when it verified the token. */
329
319
  team: z.string().max(200).optional()
330
320
  }).optional(),
331
- scope: z.string().max(SCOPE_MAX).optional()
321
+ scope: z.string().max(SCOPE_MAX).optional(),
322
+ /** The board token it came with: what it stores stops when the token does. */
323
+ token: z.string().min(1).max(64).optional()
332
324
  });
333
325
  /** A watcher as a board on `http()` registers it: its tail, and when it runs. */
334
326
  const WatcherSpec = z.object({
@@ -350,11 +342,13 @@ const registryContract = contract({
350
342
  /** Where to wake it: `<url>/wake`. Without one, it pulls on its own. */
351
343
  url: z.string().url().max(500).nullable(),
352
344
  watch: z.record(RemoteWatcherId, WatcherSpec),
353
- /** Its sync entries: with any, its sync subscribes too, to every post, change, and thread sync where it may sync. Older registrations have none. */
345
+ /**
346
+ * Its sync entries: with any, its sync subscribes too, to every post, change, and thread sync
347
+ * where it may sync. Older registrations have none.
348
+ */
354
349
  sync: z.array(z.object({
355
350
  key: z.string().min(1).max(64),
356
- platform: z.string().min(1).max(64),
357
- signIn: z.boolean()
351
+ platform: z.string().min(1).max(64)
358
352
  })).max(32).optional(),
359
353
  /** `watchersHash(watch)`, which every wake-up carries so the board can tell it's stale. */
360
354
  hash: z.string().min(1).max(100)
@@ -444,31 +438,53 @@ const schedulesContract = contract({
444
438
  }
445
439
  });
446
440
  const SCHEDULES_SESSION = "schedules";
441
+ const Grant = z.union([z.literal("all"), z.array(z.string().min(1).max(16)).max(8)]);
442
+ /** A `can`, as config writes it: board() checks it before it's kept. */
443
+ const Can = z.union([Grant, z.record(z.string().min(1).max(300), Grant)]);
444
+ const ClaimValue = z.string().min(1).max(1e3);
447
445
  /**
448
- * One-time codes for linking accounts, outside the board log: a code is a secret
449
- * until it's used, and only its hash is kept. Session `identity`.
446
+ * Who the board trusts beside its code's rules, outside the board log: grants, and the tokens it
447
+ * issued, of which only a hash is kept. Session `access`. A grant's and a token's ID is the index
448
+ * of the event that made it.
450
449
  */
451
- const identityContract = contract({
452
- name: "board-identity",
450
+ const accessContract = contract({
451
+ name: "board-access",
453
452
  events: {
454
- "identity.code_issued": z.object({
455
- hash: z.string().min(1).max(128),
456
- /** Who the account it's posted from links to. */
457
- principal: ActorId,
458
- /** The sync entry it must be posted through. */
459
- sync: z.string().min(1).max(64),
460
- expiresAt: z.string().max(64)
453
+ /** A grant, or a new `can` for the grant with this issuer and claims. */
454
+ "grant.set": z.object({
455
+ issuer: z.string().min(1).max(1e3),
456
+ claims: z.record(z.string().min(1).max(200), z.union([ClaimValue, z.array(ClaimValue).min(1).max(32)])),
457
+ can: Can,
458
+ by: ActorId
459
+ }),
460
+ "grant.revoked": z.object({
461
+ id: z.number().int().positive(),
462
+ by: ActorId
463
+ }),
464
+ "token.minted": z.object({
465
+ /** The secret's SHA-256, hex. */
466
+ hash: z.string().regex(/^[0-9a-f]{64}$/),
467
+ name: z.string().min(1).max(64),
468
+ /** Who it acts as, its scope narrowed to what the token may do. */
469
+ minter: StoredPrincipal,
470
+ profile: z.string().min(1).max(32).nullable(),
471
+ avatar: z.string().max(2048).nullable(),
472
+ session: z.string().min(1).max(128).nullable(),
473
+ /** A person's handle and name when they minted it. */
474
+ handle: Handle.nullable(),
475
+ personName: z.string().min(1).max(128).nullable(),
476
+ expiresAt: Millis,
477
+ /** The token it was minted with, which it stops with. */
478
+ parent: z.number().int().positive().nullable(),
479
+ by: ActorId
461
480
  }),
462
- /** The first account to post it wins; any later post is just a message. */
463
- "identity.code_used": z.object({
464
- hash: z.string().min(1).max(128),
465
- actor: ActorId,
466
- /** The message that used it (`externalMessageKey`): only a retry of that one claims it again. */
467
- message: z.string().max(1024).optional()
481
+ "token.revoked": z.object({
482
+ id: z.number().int().positive(),
483
+ by: ActorId
468
484
  })
469
485
  }
470
486
  });
471
- const IDENTITY_SESSION = "identity";
487
+ const ACCESS_SESSION = "access";
472
488
  //#endregion
473
489
  //#region src/contract/reducers.ts
474
490
  const messagesReducer = reducer({
@@ -487,19 +503,24 @@ reducer({
487
503
  if (event.type === "message.posted") setOwn(state, event.id, event.index);
488
504
  return state;
489
505
  });
506
+ const emptyChannel = (path) => ({
507
+ path,
508
+ lastIndex: 0,
509
+ messages: 0,
510
+ threads: 0
511
+ });
490
512
  const channelsReducer = reducer({
491
513
  contract: boardContract,
492
- name: "board.channels.v1",
514
+ name: "board.channels.v2",
493
515
  initialState: {}
494
516
  }).fold((state, event) => {
517
+ if (event.type === "channel.created") {
518
+ if (!own(state, event.payload.channel)) setOwn(state, event.payload.channel, emptyChannel(event.payload.channel));
519
+ return state;
520
+ }
495
521
  if (event.type !== "message.posted") return state;
496
522
  const { channel, re } = event.payload;
497
- const entry = own(state, channel) ?? setOwn(state, channel, {
498
- path: channel,
499
- lastIndex: 0,
500
- messages: 0,
501
- threads: 0
502
- });
523
+ const entry = own(state, channel) ?? setOwn(state, channel, emptyChannel(channel));
503
524
  entry.lastIndex = event.index;
504
525
  entry.messages += 1;
505
526
  if (re === null) entry.threads += 1;
@@ -510,32 +531,8 @@ const actorsReducer = reducer({
510
531
  name: "board.actors.v2",
511
532
  initialState: {
512
533
  byId: {},
513
- byHandle: {},
514
- links: {}
534
+ byHandle: {}
515
535
  }
516
536
  }).fold(foldActors);
517
- const identityCodesReducer = reducer({
518
- contract: identityContract,
519
- name: "board.identity-codes.v1",
520
- initialState: {}
521
- }).fold((state, event) => {
522
- if (event.type === "identity.code_issued") {
523
- const { hash, principal, sync, expiresAt } = event.payload;
524
- state[hash] ??= {
525
- principal,
526
- sync,
527
- expiresAt,
528
- usedBy: null,
529
- usedIn: null
530
- };
531
- } else if (event.type === "identity.code_used") {
532
- const code = state[event.payload.hash];
533
- if (code && code.usedBy === null) {
534
- code.usedBy = event.payload.actor;
535
- code.usedIn = event.payload.message ?? null;
536
- }
537
- }
538
- return state;
539
- });
540
537
  //#endregion
541
- export { BOARD_SESSION as _, IDENTITY_SESSION as a, SCHEDULES_SESSION as c, inboxContract as d, inboxSession as f, watchSession as g, watchContract as h, messagesReducer as i, boardContract as l, schedulesContract as m, channelsReducer as n, INBOXES_SESSION as o, registryContract as p, identityCodesReducer as r, REGISTRY_SESSION as s, actorsReducer as t, identityContract as u };
538
+ export { INBOXES_SESSION as a, accessContract as c, inboxSession as d, registryContract as f, BOARD_SESSION as g, watchSession as h, ACCESS_SESSION as i, boardContract as l, watchContract as m, channelsReducer as n, REGISTRY_SESSION as o, schedulesContract as p, messagesReducer as r, SCHEDULES_SESSION as s, actorsReducer as t, inboxContract as u };
@@ -1,9 +1,9 @@
1
- import { u as BoardError } from "./errors-C_5rB8Iz.mjs";
2
- import { s as localTokenFile } from "./node-D8Rtg1su.mjs";
3
- import { t as board } from "./server-D82a4uMe.mjs";
4
- import { t as oidcTokens } from "./tokens-C5_yGRlL.mjs";
5
- import { t as a2 } from "./a2-D_28Pdip.mjs";
6
- import { a as fsBlob } from "./blob-DQK5prXT.mjs";
1
+ import { u as BoardError } from "./errors-CJXhhzvT.mjs";
2
+ import { s as localTokenFile } from "./node-BgeGcwX0.mjs";
3
+ import { t as board } from "./server-DG8D6VV0.mjs";
4
+ import { t as oidcTokens } from "./tokens-Dnyd_y1n.mjs";
5
+ import { t as a2 } from "./a2-DngWY2Eb.mjs";
6
+ import { a as fsBlob } from "./blob-WbB2DJKn.mjs";
7
7
  import { t as localScheduler } from "./local-C76Fy5ad.mjs";
8
8
  import { mkdir, rm, writeFile } from "node:fs/promises";
9
9
  import { userInfo } from "node:os";
@@ -1,5 +1,5 @@
1
- import { t as createBoard } from "./exports-CG0P8Qi4.mjs";
2
- import { t as http } from "./backend-http-16SbvnAK.mjs";
1
+ import { t as createBoard } from "./exports-CWIdN0Ub.mjs";
2
+ import { t as http } from "./backend-http-D8mUzuKQ.mjs";
3
3
  //#region src/server/index.ts
4
4
  /**
5
5
  * The handle on a board. With a `backend`, the board lives there: `a2()` keeps it in this
@@ -9,8 +9,8 @@ import { t as http } from "./backend-http-16SbvnAK.mjs";
9
9
  function board(options = {}) {
10
10
  return createBoard(options, {
11
11
  http: () => http(),
12
- sync: () => import("./engine-DkdkXQEy.mjs"),
13
- serving: () => import("./serving-DKny38LK.mjs")
12
+ sync: () => import("./engine-DurYIRt8.mjs"),
13
+ serving: () => import("./serving-DBFRo6U_.mjs")
14
14
  });
15
15
  }
16
16
  //#endregion
@@ -1,2 +1,2 @@
1
- import { i as ownTeamId, n as discoveryIdentity, r as ownProjectId, t as oidcTokens } from "./tokens-C5_yGRlL.mjs";
1
+ import { i as ownTeamId, n as discoveryIdentity, r as ownProjectId, t as oidcTokens } from "./tokens-Dnyd_y1n.mjs";
2
2
  export { discoveryIdentity, oidcTokens, ownProjectId, ownTeamId };
@@ -1,6 +1,6 @@
1
1
  import { c as externalMessageKey } from "./external-D3p5T57R.mjs";
2
2
  import { n as setOwn } from "./own-OwgPWdmh.mjs";
3
- import { i as syncReads, n as project, r as projectSync, t as concernedMessageId } from "./project-C_2xs57L.mjs";
3
+ import { i as syncReads, n as project, r as projectSync, t as concernedMessageId } from "./project-CQ4VUibA.mjs";
4
4
  //#region src/store/sql.ts
5
5
  /** The projection's name in `board_positions`, and the A2 consumer that runs it. */
6
6
  const TABLES_CONSUMER = "board-tables";
@@ -61,7 +61,7 @@ function ddl(dialect) {
61
61
  key text primary key,
62
62
  message_id ${big} not null
63
63
  )`,
64
- "create index if not exists board_messages_recent on board_messages (by_principal, created_at) where re is null",
64
+ "drop index if exists board_messages_recent",
65
65
  ...dialect === "postgres" ? ["drop function if exists board_apply(text, bigint, boolean, jsonb, jsonb, jsonb)", APPLY_FUNCTION] : []
66
66
  ];
67
67
  }
@@ -558,12 +558,6 @@ order by m.id`,
558
558
  values: [clean(attachment)]
559
559
  })).map((found) => parse(found));
560
560
  },
561
- async recentRoots(principal, since) {
562
- return (await db.all({
563
- text: "select data from board_messages where by_principal = $1 and re is null and created_at >= $2 order by id",
564
- values: [principal, since]
565
- })).map((found) => parse(found));
566
- },
567
561
  close: () => db.close()
568
562
  };
569
563
  }
@@ -1,4 +1,4 @@
1
- import { n as ddl } from "./sql-Bj0fk6dn.mjs";
1
+ import { n as ddl } from "./sql-CT5xVuGr.mjs";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  //#region src/store/sqlite-db.ts
@@ -1,9 +1,9 @@
1
- import { S as SyncUnit, f as SyncContext, y as SyncFn } from "./provider-Bm46LAdp.mjs";
2
- import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-D6jVT4wS.mjs";
1
+ import { b as channels, g as SyncFn, p as SyncProvider } from "./provider-gynDUoyW.mjs";
2
+ import { r as ChatTransport, t as ChatInput } from "./chat-provider-BChYrrA7.mjs";
3
3
  import { z } from "zod";
4
- import { Adapter } from "chat";
5
- //#region src/sync/kapso.d.ts
6
- export declare const WhatsAppEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
4
+ import { ReactionEvent } from "chat";
5
+ //#region src/sync/chat-sdk.d.ts
6
+ declare const schemaOf: <P extends string>(platform: P) => z.ZodDiscriminatedUnion<[z.ZodObject<{
7
7
  channel: z.ZodString;
8
8
  thread: z.ZodString;
9
9
  subthread: z.ZodNullable<z.ZodString>;
@@ -49,7 +49,7 @@ export declare const WhatsAppEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
49
49
  id: z.ZodString;
50
50
  key: z.ZodString;
51
51
  raw: z.ZodUnknown;
52
- platform: z.ZodLiteral<"whatsapp">;
52
+ platform: z.ZodLiteral<P>;
53
53
  type: z.ZodLiteral<"message">;
54
54
  }, z.core.$strip>, z.ZodObject<{
55
55
  channel: z.ZodString;
@@ -83,7 +83,7 @@ export declare const WhatsAppEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
83
83
  id: z.ZodString;
84
84
  key: z.ZodString;
85
85
  raw: z.ZodUnknown;
86
- platform: z.ZodLiteral<"whatsapp">;
86
+ platform: z.ZodLiteral<P>;
87
87
  type: z.ZodLiteral<"message.edited">;
88
88
  }, z.core.$strip>, z.ZodObject<{
89
89
  channel: z.ZodString;
@@ -94,7 +94,7 @@ export declare const WhatsAppEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
94
94
  id: z.ZodString;
95
95
  key: z.ZodString;
96
96
  raw: z.ZodUnknown;
97
- platform: z.ZodLiteral<"whatsapp">;
97
+ platform: z.ZodLiteral<P>;
98
98
  type: z.ZodLiteral<"message.deleted">;
99
99
  }, z.core.$strip>, z.ZodObject<{
100
100
  channel: z.ZodString;
@@ -114,7 +114,7 @@ export declare const WhatsAppEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
114
114
  id: z.ZodString;
115
115
  key: z.ZodString;
116
116
  raw: z.ZodUnknown;
117
- platform: z.ZodLiteral<"whatsapp">;
117
+ platform: z.ZodLiteral<P>;
118
118
  type: z.ZodEnum<{
119
119
  "reaction.added": "reaction.added";
120
120
  "reaction.removed": "reaction.removed";
@@ -144,35 +144,31 @@ export declare const WhatsAppEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
144
144
  id: z.ZodString;
145
145
  key: z.ZodString;
146
146
  raw: z.ZodUnknown;
147
- platform: z.ZodLiteral<"whatsapp">;
147
+ platform: z.ZodLiteral<P>;
148
148
  }, z.core.$strip>], "kind">;
149
- export type WhatsAppEvent = z.infer<typeof WhatsAppEvent>;
150
- type MarkRead = (input: {
151
- phoneNumberId: string;
152
- messageId: string;
153
- typingIndicator: {
154
- type: 'text';
155
- };
156
- }) => Promise<unknown>;
157
- export type KapsoOptions = {
158
- adapter: Adapter;
159
- markRead: MarkRead;
149
+ /** What a `chatSdk()` entry's sync function reads as `sync.event`. `P` is its platform's name. */
150
+ export type ChatSdkEvent<P extends string = string> = z.infer<ReturnType<typeof schemaOf<P>>>;
151
+ /** The adapter, and hooks for what board can't read from it. Every hook is optional. */
152
+ export type ChatSdkOptions<P extends string = string> = Omit<ChatTransport, 'platform'> & {
153
+ /** The platform's name, in author IDs, author labels, and `vb sources`. The adapter's `name` unless given. */
154
+ platform?: P;
155
+ /**
156
+ * Whether a new board root opens a thread of its own in a mirrored channel, as on Discord or
157
+ * Teams. Off unless given: on most platforms a chat is one conversation, so the root stays on
158
+ * the board, and threads that came from the platform sync both ways.
159
+ */
160
+ canStartThreads?: boolean;
161
+ /** The platform's ID for one reaction occurrence, the same on every redelivery. Without it, a reaction is known by the webhook that brought it. */
162
+ reactionId?(reaction: ReactionEvent): string;
160
163
  };
161
164
  /**
162
- * Syncs WhatsApp numbers on Kapso with the board: `sync` is what each event
163
- * does, both ways, like `numbers({ '16315558151': 'support' })`. Each Kapso
164
- * conversation is a thread. WhatsApp has no threads in a number, so a board
165
- * thread never starts one: a board root in a mirrored channel stays on the board.
165
+ * Syncs any Chat SDK adapter with the board: `sync` is what each event does, both ways, like
166
+ * `channels({ '-1001234567890': 'support' })`. Channels are the adapter's own IDs, without its
167
+ * prefix. Slack, GitHub, and Linear have entries of their own, with more than this reads.
166
168
  */
167
- export declare function kapso(options: KapsoOptions, sync: SyncFn<WhatsAppEvent>): KapsoProvider;
168
- /**
169
- * Routes each event by its WhatsApp number. Keys are phone number IDs (`'16315558151'`), not
170
- * the numbers; values are a board channel to `mirror`, or a sync function.
171
- */
172
- export declare const numbers: (map: Record<string, SyncUnit<WhatsAppEvent>>) => SyncFn<WhatsAppEvent>;
173
- export declare class KapsoProvider extends ChatSyncProvider<WhatsAppEvent> {
174
- constructor(options: KapsoOptions, sync: SyncFn<WhatsAppEvent>);
175
- eventId(key: string, input: ChatInput): string;
176
- event(context: SyncContext): WhatsAppEvent;
177
- }
178
- //#endregion
169
+ export declare function chatSdk<P extends string>(options: ChatSdkOptions<P> & {
170
+ platform: P;
171
+ }, sync: SyncFn<ChatSdkEvent<P>>): SyncProvider<ChatSdkEvent<P>>;
172
+ export declare function chatSdk(options: ChatSdkOptions, sync: SyncFn<ChatSdkEvent>): SyncProvider<ChatSdkEvent>;
173
+ //#endregion
174
+ export { type ChatInput, channels };
@@ -0,0 +1,44 @@
1
+ import { f as channels, i as eventSchema, r as envelope } from "./events-lujPA3gQ.mjs";
2
+ import { t as ChatSyncProvider } from "./chat-provider-DkFp2hrh.mjs";
3
+ import { createHash } from "node:crypto";
4
+ //#region src/sync/chat-sdk.ts
5
+ const schemaOf = (platform) => eventSchema(platform, "message");
6
+ function chatSdk(options, sync) {
7
+ if (!options?.adapter?.name) throw new Error("chatSdk needs a Chat SDK adapter: chatSdk({ adapter }, channels({ … }))");
8
+ return new ChatSdkProvider(options, sync);
9
+ }
10
+ var ChatSdkProvider = class extends ChatSyncProvider {
11
+ schema;
12
+ reactionId;
13
+ constructor({ platform, reactionId, ...transport }, sync) {
14
+ const name = platform ?? transport.adapter.name;
15
+ super({
16
+ ...transport,
17
+ platform: name
18
+ }, sync);
19
+ this.schema = schemaOf(name);
20
+ this.reactionId = reactionId;
21
+ }
22
+ eventId(key, input) {
23
+ if (input.kind !== "reaction") return super.eventId(key, input);
24
+ const reaction = input.reaction;
25
+ const occurrence = this.reactionId?.(reaction) ?? this.occurrence();
26
+ return `reaction:${key}:${this.toNative(reaction.threadId)}|${reaction.messageId}|${reaction.rawEmoji}|${reaction.user.userId}|${reaction.added}|${occurrence}`;
27
+ }
28
+ /**
29
+ * A reaction's occurrence, when the platform gives it no ID: the webhook that brought it. A
30
+ * platform redelivers the same body, and sends a new one for each occurrence, even where the
31
+ * payload has nothing that tells two apart (Discord's forwarded reactions carry a timestamp
32
+ * beside it).
33
+ */
34
+ occurrence() {
35
+ const delivery = this.delivery();
36
+ if (delivery === null) throw new Error(`${this.platform}: a reaction outside a webhook has no identity; pass reactionId`);
37
+ return createHash("sha256").update(delivery).digest("base64url");
38
+ }
39
+ event(context) {
40
+ return this.schema.parse(envelope(this.platform, context.key, context.event, context.raw, context.id));
41
+ }
42
+ };
43
+ //#endregion
44
+ export { channels, chatSdk };