@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,6 +1,6 @@
1
1
  import { n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
2
  import { i as Attachment } from "./schemas-DBmUO6pr.mjs";
3
- import { r as badRequest, s as notFound, t as HttpError } from "./errors-C_5rB8Iz.mjs";
3
+ import { r as badRequest, s as notFound, t as HttpError } from "./errors-CJXhhzvT.mjs";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import { dirname, join } from "node:path";
@@ -61,8 +61,19 @@ function attachmentId() {
61
61
  }
62
62
  }
63
63
  /**
64
+ * Whether an upload is the caller's own, to read and attach before a message carries it: its
65
+ * principal sent it, and the caller holds no board token, or holds the one it came with or one
66
+ * that token was minted with. A token acts as its minter, so the principal alone would hand it
67
+ * every file its minter, or another of its minter's tokens, uploaded.
68
+ */
69
+ function ownUpload(upload, caller) {
70
+ if (upload.uploadedBy !== caller.id) return false;
71
+ return caller.token === void 0 || (upload.uploadedWith?.includes(caller.token) ?? false);
72
+ }
73
+ /**
64
74
  * Stores an upload under a fresh ID, with its metadata beside it. `uploadedBy`
65
- * is the principal that sent it, who can read it before any message carries it.
75
+ * is the principal that sent it, who can read it before any message carries it,
76
+ * and `uploadedWith` the board tokens it came with (`ownUpload`).
66
77
  * `max` is the most it can be: `ATTACHMENT_BYTES_MAX` unless the board's own sync imports it.
67
78
  */
68
79
  async function writeUpload(blob, input, max = ATTACHMENT_BYTES_MAX) {
@@ -78,7 +89,8 @@ async function writeUpload(blob, input, max = ATTACHMENT_BYTES_MAX) {
78
89
  await blob.put(dataKey(attachment.id), input.data, attachment.type);
79
90
  const meta = input.uploadedBy === void 0 ? attachment : {
80
91
  ...attachment,
81
- uploadedBy: input.uploadedBy
92
+ uploadedBy: input.uploadedBy,
93
+ ...input.uploadedWith ? { uploadedWith: input.uploadedWith } : {}
82
94
  };
83
95
  await blob.put(metaKey(attachment.id), new TextEncoder().encode(JSON.stringify(meta)), "application/json");
84
96
  return attachment;
@@ -97,9 +109,11 @@ async function readUpload(blob, id) {
97
109
  const found = Attachment.shape.id.safeParse(id).success ? await blob.get(metaKey(id)) : null;
98
110
  if (!found) throw attachmentNotFound(id);
99
111
  const meta = JSON.parse(new TextDecoder().decode(await bytes(found.data)));
112
+ const tokens = Array.isArray(meta.uploadedWith) && meta.uploadedWith.every((token) => typeof token === "string") ? meta.uploadedWith : null;
100
113
  return {
101
114
  attachment: Attachment.parse(meta),
102
- uploadedBy: typeof meta.uploadedBy === "string" ? meta.uploadedBy : null
115
+ uploadedBy: typeof meta.uploadedBy === "string" ? meta.uploadedBy : null,
116
+ uploadedWith: tokens
103
117
  };
104
118
  }
105
119
  /** An attachment's bytes as the store keeps them, a stream or the bytes, for board() to serve. */
@@ -127,6 +141,7 @@ async function startUpload(blob, input) {
127
141
  type: mediaType(input.type),
128
142
  size: input.size,
129
143
  uploadedBy: input.uploadedBy,
144
+ ...input.uploadedWith ? { uploadedWith: input.uploadedWith } : {},
130
145
  expiresAt: expires.toISOString()
131
146
  };
132
147
  await blob.put(ticketKey(ticket.id), new TextEncoder().encode(JSON.stringify(ticket)), "application/json");
@@ -147,15 +162,15 @@ async function startUpload(blob, input) {
147
162
  }
148
163
  /**
149
164
  * Finishes a direct upload: once the store has exactly the bytes its start said, the board records
150
- * the attachment, as `writeUpload` does. Only its uploader may finish it; a second finish answers
151
- * the same attachment. It never reads the record before writing it: a store with a cache (Vercel
152
- * Blob's CDN) would keep that miss, and the post that names the file would find nothing.
165
+ * the attachment, as `writeUpload` does. Only its uploader may finish it (`ownUpload`); a second
166
+ * finish answers the same attachment. It never reads the record before writing it: a store with a
167
+ * cache (Vercel Blob's CDN) would keep that miss, and the post that names the file would find nothing.
153
168
  */
154
- async function finishUpload(blob, id, principal) {
169
+ async function finishUpload(blob, id, caller) {
155
170
  if (!blob) throw noBlob();
156
171
  const found = Attachment.shape.id.safeParse(id).success ? await blob.get(ticketKey(id)) : null;
157
172
  const ticket = found ? JSON.parse(new TextDecoder().decode(await bytes(found.data))) : null;
158
- if (!ticket || ticket.uploadedBy !== principal) throw notFound(`no upload: ${id}`, "upload_not_found");
173
+ if (!ticket || !ownUpload(ticket, caller)) throw notFound(`no upload: ${id}`, "upload_not_found");
159
174
  if (!blob.head) throw noDirectUploads();
160
175
  const stored = await blob.head(dataKey(id));
161
176
  if (!stored) throw new HttpError(409, "not_uploaded", `nothing's been uploaded for ${id} yet: PUT the file to its URL first`);
@@ -167,9 +182,13 @@ async function finishUpload(blob, id, principal) {
167
182
  size: stored.size
168
183
  };
169
184
  try {
185
+ const uploader = {
186
+ uploadedBy: ticket.uploadedBy,
187
+ ...ticket.uploadedWith ? { uploadedWith: ticket.uploadedWith } : {}
188
+ };
170
189
  await blob.put(metaKey(id), new TextEncoder().encode(JSON.stringify({
171
190
  ...attachment,
172
- uploadedBy: principal
191
+ ...uploader
173
192
  })), "application/json");
174
193
  } catch (error) {
175
194
  const done = await blob.get(metaKey(id)).catch(() => null);
@@ -179,4 +198,4 @@ async function finishUpload(blob, id, principal) {
179
198
  return attachment;
180
199
  }
181
200
  //#endregion
182
- export { fsBlob as a, readUpload as c, finishUpload as i, startUpload as l, attachmentNotFound as n, mediaType as o, fileName as r, memoryBlob as s, attachmentBody as t, writeUpload as u };
201
+ export { fsBlob as a, ownUpload as c, writeUpload as d, finishUpload as i, readUpload as l, attachmentNotFound as n, mediaType as o, fileName as r, memoryBlob as s, attachmentBody as t, startUpload as u };
@@ -1,10 +1,7 @@
1
- import { d as ExternalFile, f as ExternalLink, l as ExternalAuthor, t as Actor } from "./folds-8wRRXjAt.mjs";
2
- import { ft as SourceJoin, ht as SourceRead } from "./client-CjAatpV4.mjs";
3
- import { M as SyncRoot, O as ProviderEvent, a as ProviderOptions, c as ReceivedEvent, d as ResolvedTarget, i as ProviderFile, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, t as FetchedLink, y as SyncFn } from "./provider-Bm46LAdp.mjs";
4
- import { Adapter, Author, FileUpload, Message, ReactionEvent } from "chat";
5
- //#region src/sync/types.d.ts
6
- declare function externalAuthor(platform: string, author: Author): ExternalAuthor;
7
- //#endregion
1
+ import { d as ExternalFile, f as ExternalLink, l as ExternalAuthor, t as Actor } from "./folds-DLUzfibJ.mjs";
2
+ import { dt as SourceJoin, mt as SourceRead } from "./client-COfYrgfV.mjs";
3
+ import { T as ProviderEvent, a as ProviderOptions, c as ReceivedEvent, d as ResolvedTarget, g as SyncFn, i as ProviderFile, k as SyncRoot, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, t as FetchedLink } from "./provider-gynDUoyW.mjs";
4
+ import { Adapter, Author, FileUpload, Message, ReactionEvent, StateAdapter } from "chat";
8
5
  //#region src/sync/chat-provider.d.ts
9
6
  type ChatInput = {
10
7
  kind: 'message' | 'edited';
@@ -20,11 +17,21 @@ type ChatInput = {
20
17
  };
21
18
  type ChatTransport = Omit<ProviderOptions, 'uploadsFiles'> & {
22
19
  adapter: Adapter;
20
+ /**
21
+ * Where Chat SDK and the adapter keep their state, like `createRedisState()`. In memory unless
22
+ * given, which is enough when the board's log is all an adapter needs to last.
23
+ */
24
+ state?: StateAdapter;
23
25
  findByEmail?(email: string): Promise<ExternalAuthor | null>;
24
26
  mention?(actor: Actor): string | null;
25
27
  author?(author: Author, raw: unknown): ExternalAuthor;
26
- resolveChannel(key: string): Promise<string>;
27
- resolveTarget(target: string): Promise<ResolvedTarget>;
28
+ /** A channel's name as its native ID. Without it, a name is the ID. */
29
+ resolveChannel?(key: string): Promise<string>;
30
+ /**
31
+ * A target's channel and kind, called on the provider. Without it, the adapter reads the target
32
+ * as a thread: one it reads as its own channel, or can't read, is a channel.
33
+ */
34
+ resolveTarget?(this: SyncProvider, target: string): Promise<ResolvedTarget>;
28
35
  groupThread?(thread: string): string;
29
36
  channelOf?(thread: string): string;
30
37
  isRoot?(thread: string, message: string): boolean;
@@ -34,7 +41,10 @@ type ChatTransport = Omit<ProviderOptions, 'uploadsFiles'> & {
34
41
  markdown?(message: Message): string;
35
42
  /** Platform messages a message links to, located under sync key `key`. */
36
43
  links?(key: string, message: Message): ExternalLink[];
37
- fetchLink?(link: ExternalLink): Promise<FetchedLink>;
44
+ fetchLink?(link: ExternalLink, linker: {
45
+ thread: string;
46
+ message: string;
47
+ }): Promise<FetchedLink>;
38
48
  /** The message a URL of the platform's names, by its own IDs: `SyncProvider.locate`. */
39
49
  locate?(url: string): {
40
50
  thread: string;
@@ -65,14 +75,11 @@ type ChatTransport = Omit<ProviderOptions, 'uploadsFiles'> & {
65
75
  /** What the adapter needs to fetch a file again, if not everything the event carried. */
66
76
  fileToFetch?(file: ExternalFile): ExternalFile;
67
77
  startTyping?(thread: string, latest: string | null, status?: string): Promise<void>;
68
- /** Runs adapter calls with a person's token instead of the bot's: how a message posts as them. */
69
- runAs?<T>(token: string, fn: () => Promise<T>): Promise<T>;
70
- /** The platform account a person's token belongs to. */
71
- whoAmI?(token: string): Promise<ExternalAuthor>;
72
78
  };
73
79
  declare abstract class ChatSyncProvider<E extends ProviderEvent = ProviderEvent> extends SyncProvider<E> {
74
80
  protected readonly transport: ChatTransport;
75
81
  private readonly chat;
82
+ private readonly state;
76
83
  private readonly receiving;
77
84
  private initialized;
78
85
  constructor(transport: ChatTransport, sync: SyncFn<E>);
@@ -101,7 +108,11 @@ declare abstract class ChatSyncProvider<E extends ProviderEvent = ProviderEvent>
101
108
  }>;
102
109
  protected normalize(key: string, input: ChatInput): Promise<ReceivedEvent>;
103
110
  private emit;
111
+ /** The body of the webhook being handled, as the platform sent it: the same on a redelivery. */
112
+ protected delivery(): string | null;
104
113
  receive(key: string, request: Request, emit: Receive): Promise<Response>;
114
+ /** `state`, noting each first claim (`setIfNotExists`) made while a webhook is handled. */
115
+ private claiming;
105
116
  private receiveRequest;
106
117
  protected ready(): Promise<Adapter<unknown, unknown>>;
107
118
  resolveChannel(key: string): Promise<string>;
@@ -110,6 +121,8 @@ declare abstract class ChatSyncProvider<E extends ProviderEvent = ProviderEvent>
110
121
  channel: string;
111
122
  kind: "channel" | "thread";
112
123
  }>;
124
+ /** A target as the adapter reads it: a thread in its channel, or a channel. */
125
+ private targetOf;
113
126
  findByEmail(email: string): Promise<{
114
127
  id: string;
115
128
  handle: string;
@@ -119,7 +132,10 @@ declare abstract class ChatSyncProvider<E extends ProviderEvent = ProviderEvent>
119
132
  } | null>;
120
133
  mention(actor: Actor): string | null;
121
134
  fetchRoot(thread: string): Promise<SyncRoot | null>;
122
- fetchLink(link: ExternalLink): Promise<FetchedLink>;
135
+ fetchLink(link: ExternalLink, linker: {
136
+ thread: string;
137
+ message: string;
138
+ }): Promise<FetchedLink>;
123
139
  locate(url: string): {
124
140
  thread: string;
125
141
  message: string | null;
@@ -132,18 +148,6 @@ declare abstract class ChatSyncProvider<E extends ProviderEvent = ProviderEvent>
132
148
  join(target: string): Promise<SourceJoin>;
133
149
  fetchFile(file: ExternalFile): Promise<Uint8Array | null>;
134
150
  post(target: string, kind: ResolvedTarget['kind'], { as: _sender, files, ...body }: ProviderPost): Promise<ProviderDelivery>;
135
- whoAmI(token: string): Promise<{
136
- id: string;
137
- handle: string;
138
- name: string;
139
- avatar: string | null;
140
- isBot: boolean;
141
- }>;
142
- /** As the person: ready first, so the adapter learns its own identity with the bot's token, never theirs. */
143
- private as;
144
- postAs(token: string, target: string, kind: ResolvedTarget['kind'], body: ProviderPost): Promise<ProviderDelivery>;
145
- editAs(token: string, thread: string, message: string, body: ProviderPost): Promise<void>;
146
- deleteAs(token: string, thread: string, message: string): Promise<void>;
147
151
  edit(thread: string, message: string, { as: _sender, files, ...body }: ProviderPost): Promise<void>;
148
152
  delete(thread: string, message: string): Promise<void>;
149
153
  react(thread: string, message: string, emoji: string, added: boolean): Promise<void>;
@@ -155,4 +159,4 @@ declare abstract class ChatSyncProvider<E extends ProviderEvent = ProviderEvent>
155
159
  deleteFiles(ids: string[]): Promise<void>;
156
160
  }
157
161
  //#endregion
158
- export { externalAuthor as i, ChatSyncProvider as n, ChatTransport as r, ChatInput as t };
162
+ export { ChatSyncProvider as n, ChatTransport as r, ChatInput as t };
@@ -1,8 +1,28 @@
1
1
  import "./external-D3p5T57R.mjs";
2
- import { n as ResolvedTarget, r as SyncProvider } from "./provider-BaVmi4Hi.mjs";
3
- import { n as userTokensOf } from "./users-yryHKFMo.mjs";
2
+ import { c as SyncProvider, s as ResolvedTarget } from "./events-lujPA3gQ.mjs";
4
3
  import { AsyncLocalStorage } from "node:async_hooks";
5
4
  import { Chat, stringifyMarkdown } from "chat";
5
+ //#region src/sync/chat.ts
6
+ /**
7
+ * One Chat SDK instance per sync entry. Platform modules call this, so
8
+ * the board (`@vercel/board`) never loads Chat SDK unless sync is configured.
9
+ */
10
+ const createChat = (adapter, state) => new Chat({
11
+ userName: "board",
12
+ adapters: { [adapter.name]: adapter },
13
+ state,
14
+ concurrency: "concurrent",
15
+ logger: "warn"
16
+ });
17
+ /** A message as markdown, the board's text format: Chat SDK parses every platform's own. */
18
+ function markdownOf(message) {
19
+ try {
20
+ return stringifyMarkdown(message.formatted).trim() || message.text;
21
+ } catch {
22
+ return message.text;
23
+ }
24
+ }
25
+ //#endregion
6
26
  //#region src/sync/state.ts
7
27
  /**
8
28
  * Chat SDK state for sync. Correctness comes from the board log (deterministic
@@ -81,27 +101,6 @@ function ephemeralState() {
81
101
  };
82
102
  }
83
103
  //#endregion
84
- //#region src/sync/chat.ts
85
- /**
86
- * One Chat SDK instance per sync entry. Platform modules call this, so
87
- * the board (`@vercel/board`) never loads Chat SDK unless sync is configured.
88
- */
89
- const createChat = (adapter) => new Chat({
90
- userName: "board",
91
- adapters: { [adapter.name]: adapter },
92
- state: ephemeralState(),
93
- concurrency: "concurrent",
94
- logger: "warn"
95
- });
96
- /** A message as markdown, the board's text format: Chat SDK parses every platform's own. */
97
- function markdownOf(message) {
98
- try {
99
- return stringifyMarkdown(message.formatted).trim() || message.text;
100
- } catch {
101
- return message.text;
102
- }
103
- }
104
- //#endregion
105
104
  //#region src/sync/types.ts
106
105
  function externalAuthor(platform, author) {
107
106
  const handle = author.userName || author.userId;
@@ -126,16 +125,32 @@ const externalFile = (file) => ({
126
125
  var ChatSyncProvider = class extends SyncProvider {
127
126
  transport;
128
127
  chat;
128
+ state;
129
129
  receiving = new AsyncLocalStorage();
130
130
  initialized;
131
131
  constructor(transport, sync) {
132
132
  super({
133
133
  ...transport,
134
- users: transport.users ?? userTokensOf(transport.adapter),
135
134
  uploadsFiles: !!transport.uploadFiles
136
135
  }, sync);
137
136
  this.transport = transport;
138
- this.chat = createChat(transport.adapter);
137
+ this.state = transport.state ?? ephemeralState();
138
+ this.chat = createChat(transport.adapter, this.claiming(this.state));
139
+ const chat = this.chat;
140
+ for (const name of [
141
+ "processMessage",
142
+ "processMessageUpdated",
143
+ "processMessageDeleted",
144
+ "processReaction"
145
+ ]) {
146
+ const original = chat[name];
147
+ if (typeof original !== "function") throw new Error(`Chat SDK has no ${name}: board can't wait for what it processes`);
148
+ chat[name] = (...args) => {
149
+ const task = original.apply(this.chat, args);
150
+ if (task instanceof Promise) this.receiving.getStore()?.tasks.push(task.catch(() => {}));
151
+ return task;
152
+ };
153
+ }
139
154
  const onMessage = async (_thread, message) => this.emit({
140
155
  kind: "message",
141
156
  message
@@ -251,23 +266,55 @@ var ChatSyncProvider = class extends SyncProvider {
251
266
  async emit(input) {
252
267
  const context = this.receiving.getStore();
253
268
  if (!context) throw new Error("Chat SDK emitted an event outside a verified request");
254
- await context.emit(await this.normalize(context.key, input));
269
+ try {
270
+ await context.emit(await this.normalize(context.key, input));
271
+ } catch (error) {
272
+ context.failures.push(error);
273
+ throw error;
274
+ }
255
275
  }
256
- receive(key, request, emit) {
257
- return this.receiving.run({
258
- key,
259
- emit
260
- }, () => this.receiveRequest(key, request, emit));
276
+ /** The body of the webhook being handled, as the platform sent it: the same on a redelivery. */
277
+ delivery() {
278
+ return this.receiving.getStore()?.body ?? null;
279
+ }
280
+ async receive(key, request, emit) {
281
+ const claims = [];
282
+ const body = await request.clone().text();
283
+ try {
284
+ return await this.receiving.run({
285
+ key,
286
+ emit,
287
+ body,
288
+ claims,
289
+ failures: [],
290
+ tasks: []
291
+ }, () => this.receiveRequest(key, request, emit));
292
+ } catch (error) {
293
+ await Promise.allSettled(claims.map((claim) => this.state.delete(claim)));
294
+ throw error;
295
+ }
296
+ }
297
+ /** `state`, noting each first claim (`setIfNotExists`) made while a webhook is handled. */
298
+ claiming(state) {
299
+ return new Proxy(state, { get: (target, property) => {
300
+ if (property === "setIfNotExists") return async (key, value, ttlMs) => {
301
+ const claimed = await target.setIfNotExists(key, value, ttlMs);
302
+ if (claimed) this.receiving.getStore()?.claims.push(key);
303
+ return claimed;
304
+ };
305
+ const value = Reflect.get(target, property, target);
306
+ return typeof value === "function" ? value.bind(target) : value;
307
+ } });
261
308
  }
262
309
  async receiveRequest(key, request, emit) {
263
- const body = this.transport.intercept || this.transport.missed ? await request.clone().text() : null;
264
- const tasks = [];
310
+ const { body, tasks, failures } = this.receiving.getStore();
265
311
  const response = await this.chat.webhooks[this.transport.adapter.name](request, { waitUntil: (task) => void tasks.push(task) });
266
312
  const results = [];
267
313
  while (tasks.length) results.push(...await Promise.allSettled(tasks.splice(0)));
314
+ if (failures.length) throw failures[0];
268
315
  const failed = results.find((result) => result.status === "rejected");
269
316
  if (failed) throw failed.reason;
270
- if (response.ok && body !== null) {
317
+ if (response.ok && (this.transport.intercept || this.transport.missed)) {
271
318
  for (const input of this.transport.missed?.(request.headers, body) ?? []) await emit(await this.normalize(key, input));
272
319
  for (const received of this.transport.intercept?.(key, request.headers, body) ?? []) await emit(received);
273
320
  }
@@ -281,11 +328,29 @@ var ChatSyncProvider = class extends SyncProvider {
281
328
  await this.initialized;
282
329
  return this.transport.adapter;
283
330
  }
284
- resolveChannel(key) {
285
- return this.transport.resolveChannel(key);
331
+ async resolveChannel(key) {
332
+ return this.transport.resolveChannel ? this.transport.resolveChannel(key) : key;
286
333
  }
287
334
  async resolveTarget(target) {
288
- return ResolvedTarget.parse(await this.transport.resolveTarget.call(this, target));
335
+ return ResolvedTarget.parse(this.transport.resolveTarget ? await this.transport.resolveTarget.call(this, target) : this.targetOf(target));
336
+ }
337
+ /** A target as the adapter reads it: a thread in its channel, or a channel. */
338
+ targetOf(target) {
339
+ let channel;
340
+ try {
341
+ channel = this.transport.channelOf?.(target) ?? this.toNative(this.transport.adapter.channelIdFromThreadId(this.toChat(target)));
342
+ } catch {
343
+ return {
344
+ target,
345
+ channel: target,
346
+ kind: "channel"
347
+ };
348
+ }
349
+ return {
350
+ target,
351
+ channel,
352
+ kind: channel === target ? "channel" : "thread"
353
+ };
289
354
  }
290
355
  async findByEmail(email) {
291
356
  return this.transport.findByEmail?.(email) ?? null;
@@ -300,8 +365,8 @@ var ChatSyncProvider = class extends SyncProvider {
300
365
  raw: root.raw ?? null
301
366
  } : null;
302
367
  }
303
- async fetchLink(link) {
304
- return this.transport.fetchLink ? this.transport.fetchLink(link) : super.fetchLink(link);
368
+ async fetchLink(link, linker) {
369
+ return this.transport.fetchLink ? this.transport.fetchLink(link, linker) : super.fetchLink(link, linker);
305
370
  }
306
371
  locate(url) {
307
372
  return this.transport.locate ? this.transport.locate(url) : super.locate(url);
@@ -334,26 +399,6 @@ var ChatSyncProvider = class extends SyncProvider {
334
399
  ...files?.length ? { files: this.transport.fileIds?.(sent.raw) ?? [] } : {}
335
400
  };
336
401
  }
337
- async whoAmI(token) {
338
- if (!this.transport.whoAmI) return super.whoAmI(token);
339
- await this.ready();
340
- return this.transport.whoAmI(token);
341
- }
342
- /** As the person: ready first, so the adapter learns its own identity with the bot's token, never theirs. */
343
- async as(token, fn) {
344
- if (!this.transport.runAs) throw new Error(`${this.platform} can't post as a person`);
345
- await this.ready();
346
- return this.transport.runAs(token, fn);
347
- }
348
- async postAs(token, target, kind, body) {
349
- return this.as(token, () => this.post(target, kind, body));
350
- }
351
- async editAs(token, thread, message, body) {
352
- await this.as(token, () => this.edit(thread, message, body));
353
- }
354
- async deleteAs(token, thread, message) {
355
- await this.as(token, () => this.delete(thread, message));
356
- }
357
402
  async edit(thread, message, { as: _sender, files, ...body }) {
358
403
  await (await this.ready()).editMessage(this.toChat(thread), message, files ? {
359
404
  ...body,
@@ -395,4 +440,4 @@ var ChatSyncProvider = class extends SyncProvider {
395
440
  }
396
441
  };
397
442
  //#endregion
398
- export { externalAuthor as n, markdownOf as r, ChatSyncProvider as t };
443
+ export { markdownOf as n, ChatSyncProvider as t };
@@ -1,2 +1,2 @@
1
- import { a as OptionName, c as globalOptions, d as options, f as usage, i as CliOption, l as groups, n as CliCommand, o as commands, r as CliExample, s as filters, t as CliArgument, u as option } from "./commands-BtH69yqt.mjs";
1
+ import { a as OptionName, c as globalOptions, d as options, f as usage, i as CliOption, l as groups, n as CliCommand, o as commands, r as CliExample, s as filters, t as CliArgument, u as option } from "./commands-DK4W3HCJ.mjs";
2
2
  export { CliArgument, CliCommand, CliExample, CliOption, OptionName, commands, filters, globalOptions, groups, option, options, usage };
@@ -1,2 +1,2 @@
1
- import { a as option, i as groups, n as filters, o as options, r as globalOptions, s as usage, t as commands } from "./commands-B5DHjZKK.mjs";
1
+ import { a as option, i as groups, n as filters, o as options, r as globalOptions, s as usage, t as commands } from "./commands-L1JSGz2Z.mjs";
2
2
  export { commands, filters, globalOptions, groups, option, options, usage };
@@ -1,5 +1,5 @@
1
- import { d as Board } from "./backend-n3hajfrD.mjs";
2
- import "./index-BygAXP7t.mjs";
1
+ import { f as Board } from "./backend-Bob-5CM7.mjs";
2
+ import "./index-Ho-ayyQL.mjs";
3
3
  //#region src/cli/failure.d.ts
4
4
  /**
5
5
  * What kind of failure it is, which fixes the exit code: 2 for `usage` and `missing_input`, 130
@@ -1,4 +1,4 @@
1
- import { a as commandLine, c as failureLines, i as mainWith, l as formatFailure, n as commandWord, o as exitCode, r as main, s as failureDocument, t as Failed } from "./main-D2s-QBOK.mjs";
1
+ import { a as commandLine, c as failureLines, i as mainWith, l as formatFailure, n as commandWord, o as exitCode, r as main, s as failureDocument, t as Failed } from "./main-Dz-8komG.mjs";
2
2
  //#region src/cli/embedded.ts
3
3
  /**
4
4
  * Board's CLI as `appName` names it in help, usage, and errors (`vercel board`). With no board
package/dist/cli.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
3
3
  import "./schemas-DBmUO6pr.mjs";
4
- import { a as option } from "./commands-B5DHjZKK.mjs";
5
- import { l as projectDir } from "./node-D8Rtg1su.mjs";
6
- import { r as main, u as who } from "./main-D2s-QBOK.mjs";
4
+ import { a as option } from "./commands-L1JSGz2Z.mjs";
5
+ import { l as projectDir } from "./node-BgeGcwX0.mjs";
6
+ import { r as main, u as who } from "./main-Dz-8komG.mjs";
7
7
  import { randomBytes, timingSafeEqual } from "node:crypto";
8
8
  import { appendFile, readFile } from "node:fs/promises";
9
9
  import { join } from "node:path";
@@ -661,7 +661,7 @@ const local = {
661
661
  if (args.length > 0) throw usage("serve takes no arguments: pass its port with --port, and its directory with --dir.");
662
662
  const port = portFlag();
663
663
  const root = projectDir();
664
- const { localHandle, serveLocal } = await import("./serve-lPoBDEds.mjs");
664
+ const { localHandle, serveLocal } = await import("./serve-CTmow-YH.mjs");
665
665
  const handle = flags.handle ?? process.env.BOARD_HANDLE;
666
666
  const local = await serveLocal({
667
667
  dir: flags.dir ?? join(root, ".board"),