@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,14 +1,13 @@
1
1
  import { a as typeOf, i as fileSize, r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
2
  import { C as toStorable, c as ChannelPath, m as MESSAGE_TEXT_MAX } from "./schemas-DBmUO6pr.mjs";
3
- import { c as externalMessageKey, l as isThreadTarget, s as channelOf } from "./external-D3p5T57R.mjs";
3
+ import { l as isThreadTarget, s as channelOf } from "./external-D3p5T57R.mjs";
4
4
  import { t as canonicalEmoji } from "./emoji-CTLpvH-A.mjs";
5
- import { r as badRequest, s as notFound, t as HttpError } from "./errors-C_5rB8Iz.mjs";
5
+ import { r as badRequest, s as notFound, t as HttpError } from "./errors-CJXhhzvT.mjs";
6
6
  import { r as derivedId, t as waitUntil } from "./wait-until-Dw4XrIDQ.mjs";
7
7
  import { n as replaceMentions } from "./mentions-CB6ShY4R.mjs";
8
- import { t as DroppedError } from "./middleware-DlJQYiwV.mjs";
8
+ import { t as DroppedError } from "./middleware-BavYSDUa.mjs";
9
9
  import { t as afterRetry } from "./attempts-HXcskN1w.mjs";
10
- import { a as channelMissing, i as UnreadableError, n as ResolvedTarget, o as routesFor, t as ReceivedEvent } from "./provider-BaVmi4Hi.mjs";
11
- import { a as immutable, n as SyncOverrides } from "./events-qf5XG5VQ.mjs";
10
+ import { a as immutable, d as routesFor, l as UnreadableError, n as SyncOverrides, o as ReceivedEvent, s as ResolvedTarget, u as channelMissing } from "./events-lujPA3gQ.mjs";
12
11
  import { t as displayText } from "./bots-CGkO76qH.mjs";
13
12
  //#region src/sync/engine.ts
14
13
  /** `files`: a message with files may have no text. */
@@ -44,18 +43,6 @@ const linkKey = (link) => JSON.stringify([
44
43
  ]);
45
44
  /** Whether two link lists say the same, whatever order their keys were written in. */
46
45
  const sameLinks = (a, b) => a.length === b.length && a.every((link, i) => linkKey(link) === linkKey(b[i]));
47
- /**
48
- * A one-time code for linking an account: a platform message that is only a live
49
- * code is claimed, not imported.
50
- */
51
- const CODE = /^\s*`?(vb-[a-z0-9]{4}-[a-z0-9]{4})`?\s*$/i;
52
- /**
53
- * How long a board message written by someone who posts as themselves may still be
54
- * on its way out: their copy comes back through the webhook looking like any of
55
- * their messages, and until its receipt lands, it's held back rather than imported twice.
56
- */
57
- const ECHO_WINDOW_MS = 6e4;
58
- const ECHO_ATTEMPTS = 6;
59
46
  /** Tries per file before it's noted instead: a retry repeats the whole message, so keep it short. */
60
47
  const FILE_ATTEMPTS = 3;
61
48
  /** How long a webhook waits for the board's tables before it lets an event into the inbox anyway: platforms resend after about 3 seconds. */
@@ -153,23 +140,16 @@ function createSyncEngine(entries) {
153
140
  /** The actors sync renders with, as the board has them now. */
154
141
  async function actorsNow() {
155
142
  const { actors } = await call("actors", {});
156
- const state = {
157
- byId: {},
158
- links: {}
143
+ const state = { byId: {} };
144
+ for (const actor of actors) state.byId[actor.id] = {
145
+ id: actor.id,
146
+ kind: actor.kind,
147
+ handle: actor.handle,
148
+ name: actor.name,
149
+ avatar: actor.avatar
159
150
  };
160
- for (const { person, ...actor } of actors) {
161
- state.byId[actor.id] = {
162
- id: actor.id,
163
- kind: actor.kind,
164
- handle: actor.handle,
165
- name: actor.name,
166
- avatar: actor.avatar
167
- };
168
- if (person) state.links[actor.id] = person.id;
169
- }
170
151
  return state;
171
152
  }
172
- const linkedTo = (actors, person) => Object.entries(actors.links).filter(([, to]) => to === person).map(([actor]) => actor);
173
153
  /** How long a name that didn't resolve (say, a Slack channel that doesn't exist yet) waits before another try. */
174
154
  const RETRY_MS = 6e4;
175
155
  const resolutions = /* @__PURE__ */ new Map();
@@ -331,10 +311,11 @@ function createSyncEngine(entries) {
331
311
  * message the board has (it was synced) in that same channel is that board message; otherwise
332
312
  * it's what the platform sent about it (Slack's unfurl, which the platform checked against who
333
313
  * posted the link), what an earlier version of this message had, or what the platform says when
334
- * asked. A board copy in another channel is never one: sync reads every channel, and the link's
335
- * readers may not read that one. A link nobody can read says why.
314
+ * asked about it and `linker`, the platform message that links it. A board copy in another channel
315
+ * is never one: sync reads every channel, and the link's readers may not read that one. A link
316
+ * nobody can read says why.
336
317
  */
337
- async function resolveLinks(platform, links, channel, previous = []) {
318
+ async function resolveLinks(platform, links, linker, channel, previous = []) {
338
319
  if (links.length === 0) return [];
339
320
  const resolved = [];
340
321
  for (const link of links) {
@@ -375,7 +356,7 @@ function createSyncEngine(entries) {
375
356
  resolved.push(kept);
376
357
  continue;
377
358
  }
378
- const fetched = await platform.fetchLink(link).catch((error) => ({
359
+ const fetched = await platform.fetchLink(link, linker).catch((error) => ({
379
360
  snapshot: null,
380
361
  unavailable: describe(error)
381
362
  }));
@@ -466,9 +447,7 @@ function createSyncEngine(entries) {
466
447
  if (copied !== null) {
467
448
  const origin = (await messageOf(copied))?.origin;
468
449
  if (!origin || origin.message !== event.message || (origin.subthread ?? origin.thread) !== (event.subthread ?? event.thread)) return;
469
- } else if (platform.users && meta.attempt < ECHO_ATTEMPTS && await deliveringAsPerson(linked, event)) throw new Error(`holding ${event.message} until ${event.author.id}'s board message there has its receipt`);
470
- const code = event.author.isBot ? void 0 : CODE.exec(event.text)?.[1];
471
- if (code && await claimCode(key, event, code)) return;
450
+ }
472
451
  }
473
452
  const author = "author" in event ? event.author : {
474
453
  id: `${platform.platform}:system`,
@@ -559,86 +538,6 @@ function createSyncEngine(entries) {
559
538
  }
560
539
  };
561
540
  }
562
- /**
563
- * Whether a board message by the person behind `event`'s author (an account they
564
- * signed in with) may be on its way there as them, without a receipt yet: then
565
- * `event` could be that very copy. In a thread the board knows (`thread`), that's
566
- * any of the person's messages still going to it; anywhere else, a new thread of
567
- * theirs still going to `event`'s channel, whose platform thread the board learns
568
- * only from the receipt.
569
- */
570
- async function deliveringAsPerson(thread, event) {
571
- const person = (await find({ account: event.author.id })).person;
572
- if (!person || person.method !== "consent") return false;
573
- const now = Date.now();
574
- const pending = (message, target, copies) => message.by.principal === person.id && message.by.profile === null && message.from === null && !message.deleted && !message.noSync && now - Date.parse(message.createdAt) < ECHO_WINDOW_MS && !copies[target] && !message.undelivered[target];
575
- if (thread !== void 0) {
576
- const synced = await syncOf(thread);
577
- if (!synced) return false;
578
- const targets = synced.targets.filter((target) => synced.external[target] === event.thread);
579
- const messages = await threadOf(synced.root);
580
- const copies = await copiesOf(messages.map((message) => message.id));
581
- return messages.some((message) => targets.some((target) => pending(message, target, copies[message.id] ?? {})));
582
- }
583
- const { roots } = await call("syncRoots", {
584
- by: person.id,
585
- since: now - ECHO_WINDOW_MS
586
- });
587
- const copies = await copiesOf(roots.map((root) => root.id));
588
- for (const root of roots) {
589
- const synced = await syncOf(root.thread);
590
- if (synced?.targets.includes(event.channel) && !synced.external[event.channel] && pending(root, event.channel, copies[root.id] ?? {})) return true;
591
- }
592
- return false;
593
- }
594
- /** A live code, posted from `event`'s account through the entry it was issued for: link them. */
595
- async function claimCode(key, event, code) {
596
- try {
597
- await call("syncAccount", {
598
- sync: key,
599
- account: event.author,
600
- code,
601
- message: externalMessageKey(event.subthread ?? event.thread, event.message)
602
- });
603
- } catch (error) {
604
- if (missing(error)) return false;
605
- throw error;
606
- }
607
- await platformOf(key).react(native(event.subthread ?? event.thread), event.message, "check", true).catch((error) => console.warn(`[board] couldn't confirm a linking code on ${key}:`, describe(error)));
608
- return true;
609
- }
610
- /** Whether entry `key` signs people in: its sign-in links the account, and lets the board post as them. */
611
- const signsIn = (key) => platformOf(key).users !== null;
612
- /**
613
- * A sign-in on entry `key` for `subject`, the person a code was issued to: the platform's URL,
614
- * which sends them to `callback` once they've approved.
615
- */
616
- async function signIn(key, subject, callback) {
617
- const platform = platformOf(key);
618
- if (!platform.users) throw badRequest(`${key} can't sign people in: link it with a code`);
619
- return platform.users.authorize(subject, callback ? { callback } : {});
620
- }
621
- /** The account `subject` signed in with on entry `key`, or null while they haven't. */
622
- async function signedIn(key, subject) {
623
- const platform = platformOf(key);
624
- if (!platform.users) throw badRequest(`${key} can't sign people in: link it with a code`);
625
- const token = await platform.users.token(subject);
626
- if (!token) return null;
627
- const account = await platform.whoAmI(token);
628
- if (!account.id.startsWith(`${platform.platform}:user:`)) throw badRequest(`${key} named an account that isn't a ${platform.platform} user`);
629
- return account;
630
- }
631
- /** Links the account `subject` signed in with to them, by the code they carried through: null while they haven't. */
632
- async function redeemSignIn(key, subject, code) {
633
- const account = await signedIn(key, subject);
634
- if (!account) return null;
635
- return (await call("syncAccount", {
636
- sync: key,
637
- account,
638
- code,
639
- method: "consent"
640
- })).link;
641
- }
642
541
  async function applyIncoming(key, item, meta, channel, overrides) {
643
542
  const { event, raw } = item;
644
543
  const source = originOf(key, event);
@@ -729,7 +628,10 @@ function createSyncEngine(entries) {
729
628
  attachments: [],
730
629
  notes: []
731
630
  };
732
- const links = await resolveLinks(platform, event.links ?? [], synced?.channel ?? channel);
631
+ const links = await resolveLinks(platform, event.links ?? [], {
632
+ thread: event.subthread ?? event.thread,
633
+ message: event.message
634
+ }, synced?.channel ?? channel);
733
635
  const base = {
734
636
  ...links.length ? { links } : {},
735
637
  author: event.author,
@@ -833,7 +735,7 @@ function createSyncEngine(entries) {
833
735
  text,
834
736
  source: from
835
737
  }));
836
- const links = stored ? await resolveLinks(platform, event.links ?? [], stored.channel, stored.links) : [];
738
+ const links = stored ? await resolveLinks(platform, event.links ?? [], origin, stored.channel, stored.links) : [];
837
739
  if (stored && !sameLinks(links, stored.links)) await call("syncImport", {
838
740
  op: "links",
839
741
  author,
@@ -959,63 +861,10 @@ function createSyncEngine(entries) {
959
861
  return null;
960
862
  });
961
863
  }
962
- /**
963
- * How `platform` mentions an actor: its own account, its person's, or any account
964
- * its person linked there. An agent is just itself.
965
- */
864
+ /** How `platform` mentions an actor: natively when it's the platform's own account, or null. */
966
865
  function nativeMention(platform, actor, actors) {
967
- const native = (id) => {
968
- const account = actors.byId[id];
969
- return account ? platform.mention(account) : null;
970
- };
971
- const person = actors.links[actor] ?? actor;
972
- const direct = native(actor) ?? (person === actor ? null : native(person));
973
- if (direct) return direct;
974
- for (const id of linkedTo(actors, person)) {
975
- const linked = native(id);
976
- if (linked) return linked;
977
- }
978
- return null;
979
- }
980
- /**
981
- * A person's own token on `platform`, to post `message` as them: only for what they
982
- * wrote on the board themselves, not their agents, and null when they haven't
983
- * signed in. A token that can't be had is the bot's turn, never a failed delivery.
984
- * A new post (`linked`) goes as them only once they've linked the account they signed
985
- * in with, so its echo is recognized as theirs and `vb identity unlink` stops it. Their
986
- * copies already out there stay theirs to change.
987
- */
988
- async function personToken(platform, message, linked) {
989
- if (!platform.users || message.from !== null || message.by.profile !== null || !message.by.principal.startsWith("vercel:user:")) return null;
990
- if (linked) {
991
- const { accounts } = await find({ person: message.by.principal });
992
- if (!accounts.some((account) => account.method === "consent" && account.id.startsWith(`${platform.platform}:user:`))) return null;
993
- }
994
- try {
995
- return await platform.users.token(message.by.principal);
996
- } catch (error) {
997
- console.warn(`[board] no ${platform.platform} token for ${message.by.principal}; posting #${message.id} as the app:`, describe(error));
998
- return null;
999
- }
1000
- }
1001
- /** What a message says as its author: no name in front, since it's theirs. */
1002
- function renderAsPerson(target, message, actors) {
1003
- const platform = platformOf(keyOf(target));
1004
- const markdown = textFor(platform, message, actors, displayText(message.text, message.by.actor));
1005
- return { markdown: message.attachments.length > 0 && !platform.uploadsFiles ? `${markdown}\n${attachmentNotes(message)}` : markdown };
1006
- }
1007
- /** Posts as the message's author when they've signed in; null to post as the app. */
1008
- async function postAsPerson(target, place, kind, message, actors) {
1009
- const platform = platformOf(keyOf(target));
1010
- if (!message.text.trim()) return null;
1011
- const token = await personToken(platform, message, true);
1012
- if (!token) return null;
1013
- try {
1014
- return await platform.postAs(token, native(place), kind, renderAsPerson(target, message, actors));
1015
- } catch (error) {
1016
- console.warn(`[board] couldn't post #${message.id} to ${target} as its author; posting as the app:`, describe(error));
1017
- return null;
1018
- }
866
+ const account = actors.byId[actor];
867
+ return account ? platform.mention(account) : null;
1019
868
  }
1020
869
  /** A board message as markdown for a platform, which converts it to its own format: by `renderer`, or the provider's own. */
1021
870
  function render(key, target, message, actors, renderer) {
@@ -1087,16 +936,6 @@ function createSyncEngine(entries) {
1087
936
  files = null;
1088
937
  }
1089
938
  else {
1090
- const asPerson = await postAsPerson(target, place, "thread", message, actors);
1091
- if (asPerson) {
1092
- const external = {
1093
- thread: place,
1094
- message: asPerson.message,
1095
- asUser: true
1096
- };
1097
- await recordDelivery(message, target, external);
1098
- return external;
1099
- }
1100
939
  if (!body.markdown.trim()) throw badRequest("a renderer must return text unless the message has media");
1101
940
  sentId = (await platform.post(native(place), "thread", body)).message;
1102
941
  }
@@ -1115,8 +954,7 @@ function createSyncEngine(entries) {
1115
954
  target,
1116
955
  external: {
1117
956
  thread: copy.thread,
1118
- message: copy.message,
1119
- ...copy.asUser ? { asUser: true } : {}
957
+ message: copy.message
1120
958
  },
1121
959
  ...copy.files !== void 0 ? { files: copy.files } : {}
1122
960
  });
@@ -1281,14 +1119,8 @@ function createSyncEngine(entries) {
1281
1119
  const mediaOnly = !body.markdown.trim() && root.attachments.length > 0 && platform.uploadsFiles;
1282
1120
  if (!body.markdown.trim() && !mediaOnly) throw badRequest("a renderer must return text unless the message has media");
1283
1121
  let uploaded;
1284
- let postedAsPerson = false;
1285
1122
  const post = async () => {
1286
1123
  if (!mediaOnly) {
1287
- const asPerson = await postAsPerson(target, target, "channel", root, actors);
1288
- if (asPerson) {
1289
- postedAsPerson = true;
1290
- return asPerson;
1291
- }
1292
1124
  if (root.attachments.length === 0 || !platform.startsThreadsWithFiles) return platform.post(native(target), "channel", body);
1293
1125
  }
1294
1126
  try {
@@ -1309,8 +1141,7 @@ function createSyncEngine(entries) {
1309
1141
  const external = {
1310
1142
  thread: externalThread,
1311
1143
  message: sent.message,
1312
- ...uploaded !== void 0 ? { files: uploaded } : {},
1313
- ...postedAsPerson ? { asUser: true } : {}
1144
+ ...uploaded !== void 0 ? { files: uploaded } : {}
1314
1145
  };
1315
1146
  await recordDelivery(root, target, external);
1316
1147
  await deliverFiles(root, target, external, actors, renderer);
@@ -1381,17 +1212,9 @@ function createSyncEngine(entries) {
1381
1212
  await offer(keyOf(target), event, message, target, async (renderer) => {
1382
1213
  const platform = platformOf(keyOf(target));
1383
1214
  const thread = native(copy.thread);
1384
- const theirs = copy.asUser && (event.type === "message.edited" || event.type === "message.deleted");
1385
- const token = theirs ? await personToken(platform, message, false) : null;
1386
- if (theirs && !token) {
1387
- console.warn(`[board] skipped ${event.type} on ${target}: #${message.id} was posted as its author, and their token is gone`);
1388
- return;
1389
- }
1390
- if (event.type === "message.edited") if (token) await platform.editAs(token, thread, copy.message, renderAsPerson(target, message, actors));
1391
- else await platform.edit(thread, copy.message, renderFor(target, message, actors, copy.files === null, renderer));
1215
+ if (event.type === "message.edited") await platform.edit(thread, copy.message, renderFor(target, message, actors, copy.files === null, renderer));
1392
1216
  else if (event.type === "message.deleted") {
1393
- if (token) await platform.deleteAs(token, thread, copy.message);
1394
- else await platform.delete(thread, copy.message);
1217
+ await platform.delete(thread, copy.message);
1395
1218
  if (copy.files?.length) await platform.deleteFiles(copy.files);
1396
1219
  } else if (event.type === "reaction.added") await platform.react(thread, copy.message, canonicalEmoji(event.payload.emoji), true);
1397
1220
  else await platform.react(thread, copy.message, canonicalEmoji(event.payload.emoji), false);
@@ -1536,10 +1359,6 @@ function createSyncEngine(entries) {
1536
1359
  findByEmail,
1537
1360
  handleWebhook,
1538
1361
  inboxChannel,
1539
- signsIn,
1540
- signIn,
1541
- signedIn,
1542
- redeemSignIn,
1543
1362
  typing,
1544
1363
  readSource,
1545
1364
  joinSource
@@ -1,4 +1,4 @@
1
- import { r as ErrorOrigin } from "./client-CjAatpV4.mjs";
1
+ import { r as ErrorOrigin } from "./client-COfYrgfV.mjs";
2
2
  //#region src/server/errors.d.ts
3
3
  declare class HttpError extends Error {
4
4
  readonly status: number;
@@ -179,11 +179,12 @@ function createBoardClient(options) {
179
179
  discovery: () => call("GET", "/", { auth: false }),
180
180
  me: () => call("GET", "/me"),
181
181
  channels: async () => (await call("GET", "/channels")).channels,
182
+ /**
183
+ * Makes a channel before anything posts to it, as a first post would. Needs `read` and `post`
184
+ * there. `created` is false when it was there already; it's left as it is.
185
+ */
186
+ createChannel: (path) => call("POST", "/channels", { body: { path } }),
182
187
  actors: async () => (await call("GET", "/actors")).actors,
183
- /** Your linked platform accounts, and the sync entries you haven't linked (`unlinked`). */
184
- identity: () => call("GET", "/identity"),
185
- /** Unlinks one of your accounts, by actor ID or handle. History keeps its messages as they were. */
186
- unlinkAccount: (account) => call("POST", "/identity/unlink", { body: { actor: account } }),
187
188
  messages: (query = {}) => call("GET", `/messages${queryString(query)}`),
188
189
  thread: (id) => call("GET", `/messages/${path(id)}`),
189
190
  /** Your own post with the client ID you gave it, as posting it answered: 404 `post_not_found` when there's none. */
@@ -222,6 +223,26 @@ function createBoardClient(options) {
222
223
  /** Stops a schedule: yours, or any with manage in its channel. `canceled` is false when it was already done or canceled. */
223
224
  cancelSchedule: (id) => call("POST", `/schedules/${path(id)}/cancel`, { body: {} }),
224
225
  /**
226
+ * Lets in callers with an OIDC token from `issuer` that carries every one of `claims`, to do
227
+ * what `can` says (read, post, react, and label everywhere when left out). Setting the same
228
+ * issuer and claims again changes its `can`: `created` says which. Needs manage in every channel.
229
+ */
230
+ createGrant: (input) => call("POST", "/grants", { body: input }),
231
+ /** The board's grants. Needs manage in every channel. */
232
+ grants: async () => (await call("GET", "/grants")).grants,
233
+ /** `revoked` is false when it was already gone. Its principals' next requests are refused. */
234
+ revokeGrant: (id) => call("POST", `/grants/${path(id)}/revoke`, { body: {} }),
235
+ /**
236
+ * A token that acts as you, narrowed to `can`, writing as `profile` and `session` when they're
237
+ * given: for a widget, a sandbox, a script. `secret` shows only here. It lives 30 days, or
238
+ * `expires`, at most 90.
239
+ */
240
+ createToken: (input) => call("POST", "/tokens", { body: input }),
241
+ /** Your live tokens, or everyone's with manage in every channel. Never their secrets. */
242
+ tokens: async () => (await call("GET", "/tokens")).tokens,
243
+ /** It and every token minted with it stop. `revoked` is false when it was already revoked. */
244
+ revokeToken: (id) => call("POST", `/tokens/${path(id)}/revoke`, { body: {} }),
245
+ /**
225
246
  * Stores a file on the board, for a post or reply's `attachments`. Up to 4 MB goes in one
226
247
  * request; bigger, up to 100 MB, goes straight to the board's blob store on a URL the board
227
248
  * signs, where its store can (`no_direct_uploads` where it can't).
@@ -284,9 +305,8 @@ function createBoardClient(options) {
284
305
  */
285
306
  label: (id, change) => call("POST", `/messages/${path(id)}/labels`, { body: change }),
286
307
  /**
287
- * Shows typing in the platform threads the message's thread is synced to
288
- * (on WhatsApp, also marking the latest inbound message read). Ephemeral:
289
- * nothing goes in the log. Answers the external threads that took it.
308
+ * Shows typing in the platform threads the message's thread is synced to.
309
+ * Ephemeral: nothing goes in the log. Answers the external threads that took it.
290
310
  * `status` replaces "Typing…" on Slack ("running tests…").
291
311
  */
292
312
  typing: async (id, status) => (await call("POST", `/messages/${path(id)}/typing`, { body: status ? { status } : {} })).targets,
@@ -330,31 +350,16 @@ function createBoardClient(options) {
330
350
  syncReceipt: (body) => call("POST", "/sync/receipts", { body }),
331
351
  /** A board thread's sync, by its key. */
332
352
  syncThread: (thread) => call("GET", `/sync/threads/${path(thread)}`),
333
- /** What platform IDs are on the board: a thread, a message, who an account is, a person's accounts. */
353
+ /** What platform IDs are on the board: a thread, or a message. */
334
354
  syncFind: (query, opts = {}) => call("GET", `/sync/find${queryString(query)}`, opts.signal ? { signal: opts.signal } : {}),
335
355
  /** Messages' copies on platforms, by message and target. */
336
356
  syncCopies: async (messages) => (await call("GET", `/sync/copies?${messages.map((id) => `message=${encodeURIComponent(id)}`).join("&")}`)).copies,
337
- /** A person's roots since `since`. */
338
- syncRoots: async (by, since) => (await call("GET", `/sync/roots${queryString({
339
- by,
340
- since: since instanceof Date ? since.getTime() : since
341
- })}`)).roots,
342
357
  /** Pushes platform events into your inbox for sync key `key`; each platform ID lands once. */
343
358
  inboxPush: (key, events) => call("POST", `/inbox/${path(key)}`, { body: { events } }),
344
359
  /** Your next inbox events for `key`, leased to you until you ack them, one platform thread at a time. */
345
360
  inboxNext: async (key, opts = {}) => (await call("GET", `/inbox/${path(key)}/next${queryString(opts)}`)).items,
346
361
  /** How an inbox event went: done, or an error to try it again. */
347
362
  inboxAck: (key, id, body) => call("POST", `/inbox/${path(key)}/${path(id)}/ack`, { body }),
348
- /**
349
- * A one-time code for linking your account on sync entry `sync`, to post from that account where
350
- * the board syncs, and `signIn` when the entry signs people in: open it (only you) and approve.
351
- * Only people get one, each for themselves. `identity()` shows the link once it's made.
352
- */
353
- syncCode: (sync) => call("POST", "/sync/codes", { body: { sync } }),
354
- /** Links a platform account to whoever a code was issued to: one posted from it, or carried through its sign-in. */
355
- syncAccount: (body) => call("POST", "/sync/accounts", { body }),
356
- /** Unlinks a platform account: your own, or anyone's with `sync`. */
357
- syncUnlink: (account) => call("POST", "/sync/accounts/unlink", { body: { account } }),
358
363
  /** Live events. Reconnects from the last seen index whenever the stream closes. */
359
364
  async *events(query = {}, opts = {}) {
360
365
  let after = query.after;
@@ -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 };