@vercel/board 0.29.0 → 0.30.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 (124) hide show
  1. package/dist/{a2-HUbQJ0RD.mjs → a2-D_28Pdip.mjs} +190 -76
  2. package/dist/access-CQ39gqEV.mjs +226 -0
  3. package/dist/access.d.mts +1 -1
  4. package/dist/access.mjs +3 -3
  5. package/dist/{actors-DcJhFbwJ.mjs → actors-xPiamLWk.mjs} +2 -2
  6. package/dist/{auth-JO22iIwr.mjs → auth-BPinwfN0.mjs} +3 -226
  7. package/dist/backend-a2/blob-vercel.d.mts +1 -1
  8. package/dist/backend-a2/contract.d.mts +2 -2
  9. package/dist/backend-a2/contract.mjs +1 -1
  10. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  11. package/dist/backend-a2/scheduler-local.mjs +1 -1
  12. package/dist/backend-a2/scheduler-qstash.d.mts +3 -3
  13. package/dist/backend-a2/scheduler-qstash.mjs +8 -8
  14. package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
  15. package/dist/backend-a2/scheduler-vercel.mjs +6 -6
  16. package/dist/backend-a2/store-memory.d.mts +2 -2
  17. package/dist/backend-a2/store-memory.mjs +3 -3
  18. package/dist/backend-a2/store-postgres.d.mts +1 -1
  19. package/dist/backend-a2/store-postgres.mjs +34 -15
  20. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  21. package/dist/backend-a2/store-redis-http.mjs +6 -6
  22. package/dist/backend-a2/store-redis.d.mts +2 -2
  23. package/dist/backend-a2/store-redis.mjs +5 -5
  24. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  25. package/dist/backend-a2/store-sqlite.mjs +3 -3
  26. package/dist/backend-a2.d.mts +17 -7
  27. package/dist/backend-a2.mjs +2 -2
  28. package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-16SbvnAK.mjs} +2 -2
  29. package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-3PoBYQoo.d.mts} +13 -3
  30. package/dist/{backend-http-CzIJMm58.mjs → backend-http-BPaNYLWj.mjs} +2 -3
  31. package/dist/backend-http.d.mts +1 -1
  32. package/dist/backend-http.mjs +1 -1
  33. package/dist/backend-http.node.d.mts +1 -1
  34. package/dist/backend-http.node.mjs +1 -1
  35. package/dist/{backend-CPC_vyOo.d.mts → backend-n3hajfrD.d.mts} +11 -5
  36. package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-C4Kee2-K.mjs} +203 -31
  37. package/dist/{backend-vercel-DQc_TaHp.mjs → backend-vercel-Kd8nvW5A.mjs} +3 -3
  38. package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-qznuDxnN.d.mts} +16 -5
  39. package/dist/backend-vercel.d.mts +1 -1
  40. package/dist/backend-vercel.mjs +1 -1
  41. package/dist/backend-vercel.node.d.mts +1 -1
  42. package/dist/backend-vercel.node.mjs +1 -1
  43. package/dist/{blob-DmiQjrYy.d.mts → blob-CqILvdmr.d.mts} +2 -2
  44. package/dist/{blob-DdbpJqN1.mjs → blob-DQK5prXT.mjs} +1 -1
  45. package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-D6jVT4wS.d.mts} +3 -3
  46. package/dist/cli-commands.d.mts +1 -1
  47. package/dist/cli-commands.mjs +1 -1
  48. package/dist/cli-embedded.d.mts +2 -2
  49. package/dist/cli-embedded.mjs +1 -1
  50. package/dist/cli.mjs +4 -4
  51. package/dist/{client-BqAjk0Q4.d.mts → client-CjAatpV4.d.mts} +60 -10
  52. package/dist/{commands-C3R1AWgm.mjs → commands-B5DHjZKK.mjs} +50 -9
  53. package/dist/{commands-D6gvrOh3.d.mts → commands-BtH69yqt.d.mts} +17 -0
  54. package/dist/{conformance-DlnfuzEB.mjs → conformance-C7fwIvmN.mjs} +258 -18
  55. package/dist/conformance.d.mts +1 -1
  56. package/dist/conformance.mjs +1 -1
  57. package/dist/{engine-DQxXk7dX.mjs → engine-DkdkXQEy.mjs} +2 -2
  58. package/dist/{errors-DDktVeUa.d.mts → errors-87TVtXeb.d.mts} +1 -1
  59. package/dist/{client-D0Px7KzF.mjs → errors-C_5rB8Iz.mjs} +104 -4
  60. package/dist/{exports-BG-0Wyn-.mjs → exports-CG0P8Qi4.mjs} +54 -28
  61. package/dist/exports-CrGA5_yN.d.mts +5 -0
  62. package/dist/{folds-ZkHM4M-v.d.mts → folds-8wRRXjAt.d.mts} +2 -0
  63. package/dist/{folds-BDMSxBUf.mjs → folds-DGYkMW19.mjs} +1 -0
  64. package/dist/{index-D3VvNfh_.d.mts → index-BygAXP7t.d.mts} +2 -2
  65. package/dist/index.d.mts +6 -6
  66. package/dist/{index.default-rB8sJ9RX.mjs → index.default-DhkgpfR2.mjs} +1 -1
  67. package/dist/index.mjs +5 -6
  68. package/dist/index.node.d.mts +6 -6
  69. package/dist/index.node.mjs +3 -4
  70. package/dist/inspect/app.js +29 -29
  71. package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
  72. package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
  73. package/dist/{main-CVBqe3VO.mjs → main-D2s-QBOK.mjs} +70 -23
  74. package/dist/{middleware-CXjEeef4.mjs → middleware-DlJQYiwV.mjs} +1 -1
  75. package/dist/{node-Zd_C-gxP.mjs → node-D8Rtg1su.mjs} +1 -1
  76. package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
  77. package/dist/{project-CBXlLftU.mjs → project-C_2xs57L.mjs} +1 -1
  78. package/dist/{provider-DmSTp9lI.d.mts → provider-Bm46LAdp.d.mts} +2 -2
  79. package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-BWJZHUGg.mjs} +40 -24
  80. package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
  81. package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
  82. package/dist/{reducers-DJzVpXwM.mjs → reducers-8jqLewgw.mjs} +3 -3
  83. package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
  84. package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
  85. package/dist/{serve-G9RuaDSr.mjs → serve-lPoBDEds.mjs} +7 -7
  86. package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
  87. package/dist/{server-BixV_byP.mjs → server-D82a4uMe.mjs} +4 -4
  88. package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
  89. package/dist/{serving-B75Y4BF-.mjs → serving-DKny38LK.mjs} +1 -1
  90. package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
  91. package/dist/{sql-D4Z_efJX.mjs → sql-Bj0fk6dn.mjs} +1 -1
  92. package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-Dc8G78XV.mjs} +1 -1
  93. package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
  94. package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
  95. package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
  96. package/dist/store-memory-4IBl0Ztu.mjs +2 -0
  97. package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
  98. package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
  99. package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
  100. package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
  101. package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
  102. package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
  103. package/dist/sync-github.d.mts +2 -2
  104. package/dist/sync-kapso.d.mts +2 -2
  105. package/dist/sync-linear.d.mts +3 -3
  106. package/dist/sync-linear.mjs +4 -2
  107. package/dist/sync-slack.d.mts +3 -3
  108. package/dist/sync.chat.d.mts +1 -1
  109. package/dist/sync.d.mts +2 -2
  110. package/dist/{tokens-CC3izSNr.mjs → tokens-C5_yGRlL.mjs} +2 -2
  111. package/dist/{types-No0WdhE5.d.mts → types-shZmGZ-q.d.mts} +2 -2
  112. package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-B_5b0-ZG.mjs} +46 -17
  113. package/docs/01-board/04-messages.md +93 -0
  114. package/docs/01-board/08-middleware.md +5 -2
  115. package/docs/04-reference/01-cli.md +23 -10
  116. package/docs/04-reference/02-board.md +20 -5
  117. package/docs/04-reference/03-http.md +17 -6
  118. package/docs/04-reference/05-conformance.md +14 -3
  119. package/package.json +2 -2
  120. package/dist/errors-CusN4x2l.mjs +0 -88
  121. package/dist/exports-mhAYiU4i.d.mts +0 -5
  122. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
  123. package/dist/store-memory-ZLiZbH3R.mjs +0 -2
  124. package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
@@ -1,10 +1,10 @@
1
1
  import { r as IMPORT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
2
  import { _ as RemoteWatcherId } from "./schemas-DBmUO6pr.mjs";
3
- import { O as RegisterBody, a as METHODS, d as VERSION, n as compileWatcher, t as canonicalWatchers } from "./watchers-hash-BDhLVkV5.mjs";
3
+ import { a as METHODS, d as VERSION, k as RegisterBody, n as compileWatcher, t as canonicalWatchers } from "./watchers-hash-B_5b0-ZG.mjs";
4
+ import { a as forbidden, f as duration, o as json, r as badRequest, s as notFound, t as HttpError } from "./errors-C_5rB8Iz.mjs";
4
5
  import { o as sha256, r as derivedId } from "./wait-until-Dw4XrIDQ.mjs";
5
- import { a as forbidden, o as json, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
6
- import { a as wakePath, i as registrant, n as callerProfile, t as PROJECTS_OWN } from "./backend-http-CzIJMm58.mjs";
7
- import { o as mediaType, r as fileName } from "./blob-DdbpJqN1.mjs";
6
+ import { a as wakePath, i as registrant, n as callerProfile, t as PROJECTS_OWN } from "./backend-http-BPaNYLWj.mjs";
7
+ import { o as mediaType, r as fileName } from "./blob-DQK5prXT.mjs";
8
8
  //#region src/server/backend-vercel.ts
9
9
  const PROFILE = /^[A-Za-z][A-Za-z0-9-]{0,31}$/;
10
10
  const SESSION = /^[A-Za-z0-9_.:-]{1,128}$/;
@@ -19,14 +19,6 @@ const PAGE_MAX = 100;
19
19
  const WAIT_MAX_S = 25;
20
20
  /** Pages a filtered read of one channel scans for matches before it answers with what it found. */
21
21
  const SCAN_PAGES = 10;
22
- /** What the API keeps none of: a check that gets one of these is told it isn't kept here. */
23
- const UNKEPT = {
24
- schedule: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
25
- schedules: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
26
- cancelSchedule: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
27
- uploadStart: ["no_direct_uploads", "Vercel's boards take files up to 4 MB in one upload for now"],
28
- uploadFinish: ["no_direct_uploads", "Vercel's boards take files up to 4 MB in one upload for now"]
29
- };
30
22
  /** A file as the board names it. */
31
23
  const fileOf = ({ id, name, type, size }) => ({
32
24
  id,
@@ -74,6 +66,13 @@ const authorOf = (actor) => ({
74
66
  profile: actor.profile?.name ?? null,
75
67
  session: actor.session ?? null
76
68
  });
69
+ /** Who cancels a schedule when no one did: the API, once its creator can no longer post it, as a2()'s scheduler. */
70
+ const SCHEDULER = {
71
+ actor: "board:schedule",
72
+ principal: "board:schedule",
73
+ profile: null,
74
+ session: null
75
+ };
77
76
  /** The API names nobody, so an actor's handle is its profile, or a short form of its principal. */
78
77
  function actorView(actor) {
79
78
  const { principal } = actor;
@@ -90,8 +89,8 @@ function actorView(actor) {
90
89
  }
91
90
  /**
92
91
  * Vercel's board for your team, in Vercel's API: this deployment keeps nothing and reads and writes
93
- * it as itself, its people as themselves. Messages, threads, reactions, labels, and a channel's
94
- * changes work; what the API keeps none of yet (schedules, files, watchers, sync) answers 501.
92
+ * it as itself, its people as themselves. Messages, threads, reactions, labels, a channel's
93
+ * changes, schedules, files, and watchers work; what the API keeps none of yet (sync) answers 501.
95
94
  */
96
95
  function createVercel(options, environment) {
97
96
  const origin = (options.api ?? "https://api.vercel.com").replace(/\/+$/, "");
@@ -331,7 +330,39 @@ function createVercel(options, environment) {
331
330
  attachments: message.attachments ?? [],
332
331
  links: [],
333
332
  version: message.version,
334
- author: await nameOf(who, message.createdBy)
333
+ revision: message.revision,
334
+ author: await nameOf(who, message.createdBy),
335
+ ...message.schedule ? { schedule: {
336
+ id: message.schedule.id,
337
+ n: message.schedule.n
338
+ } } : {}
339
+ };
340
+ }
341
+ async function scheduleView(who, schedule) {
342
+ const { canceled } = schedule;
343
+ return {
344
+ id: schedule.id,
345
+ channel: (await channelById(who, schedule.channelId)).name,
346
+ re: schedule.threadId === void 0 ? null : idAt(schedule.threadId, 1),
347
+ text: schedule.text,
348
+ labels: { ...schedule.labels },
349
+ noSync: false,
350
+ attachments: [],
351
+ first: iso(schedule.firstAt),
352
+ every: schedule.every ?? null,
353
+ until: schedule.until === void 0 ? null : iso(schedule.until),
354
+ times: schedule.times ?? null,
355
+ sent: schedule.sent,
356
+ next: schedule.nextAt === null ? null : iso(schedule.nextAt),
357
+ last: schedule.lastMessageId ?? null,
358
+ state: schedule.state,
359
+ canceled: canceled ? {
360
+ by: canceled.by ? authorOf(canceled.by) : SCHEDULER,
361
+ reason: canceled.reason === "disabled" ? "revoked" : canceled.reason
362
+ } : null,
363
+ by: authorOf(schedule.createdBy),
364
+ author: await nameOf(who, schedule.createdBy),
365
+ createdAt: iso(schedule.createdAt)
335
366
  };
336
367
  }
337
368
  const positions = /* @__PURE__ */ new Map();
@@ -796,6 +827,22 @@ function createVercel(options, environment) {
796
827
  function checkCaller(method, caller) {
797
828
  if ("identity" in caller && PROJECTS_OWN.has(method)) throw forbidden(`this board's callers act as its project on Vercel's board, which can't tell them apart: ${method} is the project's own to do there`);
798
829
  }
830
+ /**
831
+ * The API owns messages and schedules by principal, and sees every caller here as this project:
832
+ * a caller that is the project (its own deployment's token) changes what it wrote, as on any
833
+ * board, and anyone else would be changing what the project wrote for others.
834
+ */
835
+ /** Whether a caller is this project: the deployment's own code, or a caller verified as its principal. */
836
+ const isProject = async (caller, who) => !("identity" in caller) || caller.identity.principal.id === principalId((await self(who)).actor.principal);
837
+ async function checkOwner(what, caller, who) {
838
+ if (!await isProject(caller, who)) throw forbidden(`this board's callers act as its project on Vercel's board, which owns what any of them wrote: only the project itself may ${what} there`);
839
+ }
840
+ /** A file is no use until a post or an import carries it, so uploading takes `post` or `sync` somewhere, as on a2(). */
841
+ async function checkUploader(caller, who) {
842
+ await checkOwner("upload", caller, who);
843
+ const access = accessOf(caller);
844
+ if (access && !access.anywhere("post") && !access.anywhere("sync")) throw forbidden("this token can't post anywhere, so it can't upload");
845
+ }
799
846
  const meOf = (identity) => {
800
847
  const { principal, author, actor } = identity;
801
848
  return {
@@ -975,8 +1022,6 @@ function createVercel(options, environment) {
975
1022
  });
976
1023
  const methods = {
977
1024
  ...Object.fromEntries(METHODS.map((method) => [method, async () => {
978
- const unkept = UNKEPT[method];
979
- if (unkept) throw new HttpError(501, unkept[0], unkept[1]);
980
1025
  throw notSupported(method === "register" || method === "watchers" || method === "matches" || method === "ack" ? "watchers" : method);
981
1026
  }])),
982
1027
  async discovery() {
@@ -1155,10 +1200,10 @@ function createVercel(options, environment) {
1155
1200
  };
1156
1201
  },
1157
1202
  async upload(caller, { data, name, type }) {
1158
- checkCaller("upload", caller);
1203
+ const who = whoOf(caller);
1204
+ await checkUploader(caller, who);
1159
1205
  if (data.byteLength === 0) throw new HttpError(400, "bad_request", "the file is empty");
1160
1206
  if (data.byteLength > 26214400) throw new HttpError(413, "too_large", `attachments are at most ${IMPORT_BYTES_MAX / 1024 / 1024} MB`);
1161
- const who = whoOf(caller);
1162
1207
  const bytes = new Uint8Array(data);
1163
1208
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
1164
1209
  const started = await api(who, "POST", await onBoard(who, "/attachments"), { body: {
@@ -1178,12 +1223,37 @@ function createVercel(options, environment) {
1178
1223
  const { attachment } = await api(who, "POST", await onBoard(who, `/attachments/${encodeURIComponent(started.attachment.id)}/complete`));
1179
1224
  return { attachment: fileOf(attachment) };
1180
1225
  },
1226
+ async uploadStart(caller, { name, type, size, sha256 }) {
1227
+ const who = whoOf(caller);
1228
+ await checkUploader(caller, who);
1229
+ if (sha256 === void 0) throw new HttpError(400, "invalid_request", "Vercel's API signs a direct upload for its bytes' SHA-256: send `sha256`, as board's client does");
1230
+ const started = await api(who, "POST", await onBoard(who, "/attachments"), { body: {
1231
+ operationId: operationId(),
1232
+ name: fileName(name ?? null),
1233
+ type: mediaType(type ?? null),
1234
+ size,
1235
+ sha256
1236
+ } });
1237
+ return {
1238
+ id: started.attachment.id,
1239
+ url: started.upload.url,
1240
+ headers: started.upload.headers,
1241
+ expiresAt: iso(started.upload.expiresAt)
1242
+ };
1243
+ },
1244
+ async uploadFinish(caller, { id }) {
1245
+ const who = whoOf(caller);
1246
+ await checkUploader(caller, who);
1247
+ const { attachment } = await api(who, "POST", await onBoard(who, `/attachments/${encodeURIComponent(id)}/complete`));
1248
+ return { attachment: fileOf(attachment) };
1249
+ },
1181
1250
  async attachment(caller, { id }, signal) {
1182
1251
  const who = whoOf(caller);
1183
1252
  const access = accessOf(caller);
1184
1253
  const found = await api(who, "GET", await onBoard(who, `/attachments/${encodeURIComponent(id)}`), signal ? { signal } : {});
1185
- if (access) {
1186
- if (!found.attachment.messageId) throw notFound(`no attachment: ${id}`, "attachment_not_found");
1254
+ if (access && !found.attachment.messageId) {
1255
+ if (!await isProject(caller, who)) throw notFound(`no attachment: ${id}`, "attachment_not_found");
1256
+ } else if (access) {
1187
1257
  const { message } = await readableMessage(who, access, found.attachment.messageId, signal).catch((error) => {
1188
1258
  if (error instanceof HttpError && error.status === 404) throw notFound(`no attachment: ${id}`, "attachment_not_found");
1189
1259
  throw error;
@@ -1205,7 +1275,7 @@ function createVercel(options, environment) {
1205
1275
  if (input.sync?.length) throw notSupported("sync");
1206
1276
  const who = whoOf(caller);
1207
1277
  const access = accessOf(caller);
1208
- if (access && input.attachments?.length) throw forbidden("this board's callers act as its project on Vercel's board, which can't tell them apart: only the project's own code posts files there");
1278
+ if (input.attachments?.length) await checkOwner("post files", caller, who);
1209
1279
  const labeled = Object.keys(input.labels ?? {}).length > 0;
1210
1280
  const clientId = input.id !== void 0 ? await postId(caller, input.id) : void 0;
1211
1281
  let made;
@@ -1228,15 +1298,25 @@ function createVercel(options, environment) {
1228
1298
  const { thread, message: to, channel } = await readableMessage(who, access, input.re, signal);
1229
1299
  need(access, "post", channel.name);
1230
1300
  if (labeled) need(access, "label", channel.name);
1301
+ const after = input.after === void 0 ? void 0 : locate(input.after);
1302
+ if (after !== void 0 && after?.threadId !== thread.id) {
1303
+ const landed = clientId ? await api(who, "GET", await onBoard(who, `/posts/${encodeURIComponent(clientId)}`), { signal }).catch(() => null) : null;
1304
+ if (landed?.thread.id === thread.id) return { message: await view(who, landed.message, channel.name) };
1305
+ throw badRequest(`after: ${input.after} isn't in ${input.re}'s thread`, "invalid_after");
1306
+ }
1231
1307
  made = await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(thread.id)}/messages`), {
1232
1308
  body: {
1233
1309
  operationId: operationId(),
1234
1310
  text: input.text,
1235
1311
  ...to.sequence > 1 ? { replyTo: to.sequence } : {},
1236
1312
  ...clientId ? { clientId } : {},
1237
- ...input.attachments?.length ? { attachments: input.attachments } : {}
1313
+ ...input.attachments?.length ? { attachments: input.attachments } : {},
1314
+ ...after ? { after: after.sequence } : {}
1238
1315
  },
1239
1316
  signal
1317
+ }).catch((error) => {
1318
+ if (error instanceof HttpError && error.code === "newer_message") throw new HttpError(409, "newer_message", `a message came after ${input.after}`);
1319
+ throw error;
1240
1320
  });
1241
1321
  if (input.labels && Object.keys(input.labels).length) {
1242
1322
  const labeled = await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(thread.id)}/messages/${made.message.sequence}/labels`), {
@@ -1268,27 +1348,35 @@ function createVercel(options, environment) {
1268
1348
  if (access && !access.readable(channel.name)) throw notFound(`no post of yours has id ${id}`, "post_not_found");
1269
1349
  return { message: await view(who, found.message, channel.name) };
1270
1350
  },
1271
- async edit(caller, { id, text }, signal) {
1272
- checkCaller("edit", caller);
1351
+ async edit(caller, { id, text, revision }, signal) {
1273
1352
  const who = whoOf(caller);
1353
+ await checkOwner("edit", caller, who);
1274
1354
  const at = locate(id);
1275
1355
  if (!at) throw notFound(`message not found: ${id}`, "message_not_found");
1356
+ const access = accessOf(caller);
1357
+ if (access) need(access, "post", (await readableMessage(who, access, id, signal)).channel.name);
1276
1358
  await api(who, "PATCH", await onBoard(who, `/threads/${encodeURIComponent(at.threadId)}/messages/${at.sequence}`), {
1277
1359
  body: {
1278
1360
  operationId: operationId(),
1279
- text
1361
+ text,
1362
+ ...revision !== void 0 ? { revision } : {}
1280
1363
  },
1281
1364
  signal
1282
1365
  });
1283
1366
  return { ok: true };
1284
1367
  },
1285
- async delete(caller, { id }, signal) {
1286
- checkCaller("delete", caller);
1368
+ async delete(caller, { id, revision }, signal) {
1287
1369
  const who = whoOf(caller);
1370
+ await checkOwner("delete", caller, who);
1288
1371
  const at = locate(id);
1289
1372
  if (!at) throw notFound(`message not found: ${id}`, "message_not_found");
1373
+ const access = accessOf(caller);
1374
+ if (access) need(access, "post", (await readableMessage(who, access, id, signal)).channel.name);
1290
1375
  await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(at.threadId)}/messages/${at.sequence}/delete`), {
1291
- body: { operationId: operationId() },
1376
+ body: {
1377
+ operationId: operationId(),
1378
+ ...revision !== void 0 ? { revision } : {}
1379
+ },
1292
1380
  signal
1293
1381
  });
1294
1382
  return { ok: true };
@@ -1310,7 +1398,7 @@ function createVercel(options, environment) {
1310
1398
  });
1311
1399
  return { ok: true };
1312
1400
  },
1313
- async label(caller, { id, set, unset }, signal) {
1401
+ async label(caller, { id, set, unset, if: expected }, signal) {
1314
1402
  checkCaller("label", caller);
1315
1403
  const who = whoOf(caller);
1316
1404
  const at = locate(id);
@@ -1321,12 +1409,95 @@ function createVercel(options, environment) {
1321
1409
  body: {
1322
1410
  operationId: operationId(),
1323
1411
  ...set ? { set } : {},
1324
- ...unset ? { unset } : {}
1412
+ ...unset ? { unset } : {},
1413
+ ...expected && Object.keys(expected).length ? { if: expected } : {}
1325
1414
  },
1326
1415
  signal
1327
1416
  });
1328
1417
  return { ok: true };
1329
1418
  },
1419
+ async schedule(caller, input, signal) {
1420
+ if (input.attachments?.length) throw notSupported("files on scheduled messages");
1421
+ const who = whoOf(caller);
1422
+ await checkOwner("schedule", caller, who);
1423
+ const access = accessOf(caller);
1424
+ const labels = input.labels && Object.keys(input.labels).length ? input.labels : void 0;
1425
+ let target;
1426
+ if (input.channel !== void 0) {
1427
+ need(access, "post", input.channel);
1428
+ if (labels) need(access, "label", input.channel);
1429
+ target = { channelId: (await channelToPost(who, input.channel)).id };
1430
+ } else {
1431
+ if (labels) throw notSupported("labels on a scheduled reply");
1432
+ const { thread, channel } = await readableMessage(who, access, input.re, signal);
1433
+ need(access, "post", channel.name);
1434
+ target = { threadId: thread.id };
1435
+ }
1436
+ const ms = (value) => Math.round(duration(value));
1437
+ const firstAt = input.at !== void 0 ? Date.parse(input.at) : Date.now() + ms(input.in ?? input.every);
1438
+ const until = input.until !== void 0 ? Date.parse(input.until) : input.for !== void 0 ? firstAt + ms(input.for) : void 0;
1439
+ const { schedule } = await api(who, "POST", await onBoard(who, "/schedules"), {
1440
+ body: {
1441
+ operationId: operationId(),
1442
+ ...target,
1443
+ text: input.text,
1444
+ ...labels ? { labels } : {},
1445
+ firstAt,
1446
+ ...input.every !== void 0 ? { every: ms(input.every) } : {},
1447
+ ...until !== void 0 ? { until } : {},
1448
+ ...input.times !== void 0 ? { times: input.times } : {}
1449
+ },
1450
+ signal
1451
+ });
1452
+ return { schedule: await scheduleView(who, schedule) };
1453
+ },
1454
+ async schedules(caller, query, signal) {
1455
+ const who = whoOf(caller);
1456
+ const access = accessOf(caller);
1457
+ const filter = { state: query.all ? void 0 : "pending" };
1458
+ if (query.re !== void 0) {
1459
+ const found = await readableMessage(who, access, query.re, signal).catch((error) => {
1460
+ if (error instanceof HttpError && error.status === 404) return null;
1461
+ throw error;
1462
+ });
1463
+ if (!found) return { schedules: [] };
1464
+ filter.threadId = found.thread.id;
1465
+ }
1466
+ if (query.channel !== void 0) {
1467
+ const channel = await channelNamed(who, query.channel);
1468
+ if (!channel) return { schedules: [] };
1469
+ filter.channelId = channel.id;
1470
+ }
1471
+ const me = query.by === "me" ? await actorOf(who) : void 0;
1472
+ const by = me ?? query.by;
1473
+ if (by?.startsWith("vercel:")) filter.createdBy = by;
1474
+ const created = (actor) => by === void 0 || (me ? actor.id === me : [
1475
+ actor.id,
1476
+ principalId(actor.principal),
1477
+ actor.profile?.name
1478
+ ].includes(by));
1479
+ const listed = await all(who, await onBoard(who, "/schedules"), "schedules", filter, signal);
1480
+ return { schedules: (await Promise.all(listed.filter((schedule) => created(schedule.createdBy)).map((schedule) => scheduleView(who, schedule)))).filter((schedule) => access === null || access.readable(schedule.channel)) };
1481
+ },
1482
+ async cancelSchedule(caller, { id }, signal) {
1483
+ const who = whoOf(caller);
1484
+ await checkOwner("cancelSchedule", caller, who);
1485
+ const access = accessOf(caller);
1486
+ if (access) {
1487
+ const found = (await all(who, await onBoard(who, "/schedules"), "schedules", {}, signal)).find((schedule) => schedule.id === id);
1488
+ const channel = found && await channelById(who, found.channelId);
1489
+ if (!channel || !access.readable(channel.name)) throw notFound(`no schedule: ${id}`, "schedule_not_found");
1490
+ need(access, "post", channel.name);
1491
+ }
1492
+ const { schedule, canceled } = await api(who, "POST", await onBoard(who, `/schedules/${encodeURIComponent(id)}/cancel`), {
1493
+ body: { operationId: operationId() },
1494
+ signal
1495
+ });
1496
+ return {
1497
+ schedule: await scheduleView(who, schedule),
1498
+ canceled
1499
+ };
1500
+ },
1330
1501
  async events(caller, query, signal) {
1331
1502
  checkCaller("events", caller);
1332
1503
  const who = whoOf(caller);
@@ -1399,6 +1570,7 @@ function createVercel(options, environment) {
1399
1570
  }])),
1400
1571
  routes: runner.routes,
1401
1572
  keeps: false,
1573
+ sendsSchedulesHere: false,
1402
1574
  bind(next) {
1403
1575
  if (binding) throw new Error("vercel: this backend already serves a board: create one vercel() per board()");
1404
1576
  if (Object.values(next.subscriptions).map((subscription) => subscription.kind).includes("sync")) throw new Error("vercel: Vercel's boards don't run sync yet: leave `sync` out");
@@ -1,6 +1,6 @@
1
- import { t as HttpError } from "./errors-CusN4x2l.mjs";
2
- import { c as loginHanded, d as vercelProjectFile, i as environmentProfile, t as asYou, u as vercelLogin } from "./node-Zd_C-gxP.mjs";
3
- import { t as createVercel } from "./backend-vercel-BNauAs7w.mjs";
1
+ import { t as HttpError } from "./errors-C_5rB8Iz.mjs";
2
+ import { c as loginHanded, d as vercelProjectFile, i as environmentProfile, t as asYou, u as vercelLogin } from "./node-D8Rtg1su.mjs";
3
+ import { t as createVercel } from "./backend-vercel-C4Kee2-K.mjs";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { decodeJwt } from "jose";
6
6
  import { getVercelOidcToken } from "@vercel/oidc";
@@ -1,6 +1,6 @@
1
- import { t as Actor, x as Attachment } from "./folds-ZkHM4M-v.mjs";
2
- import { A as InboxItem, B as Me, D as IdentityView, Ft as ThreadView, H as MessagesPage, Ht as UploadTicket, I as LinkedAccount, It as ThreadsPage, K as PersonView, Nt as SyncedView, R as MatchView, S as EventsPage, St as SyncCopiesView, Tt as SyncFindView, V as MessageView, b as Discovery, bt as SyncCodeView, dt as SourceJoin, et as RegistrationView, jt as SyncThreadView, k as InboxEntry, mt as SourceRead, o as ProfileOption, ot as ScheduleView, v as ChannelView } from "./client-BqAjk0Q4.mjs";
3
- import { i as Caller, n as BackendRoute, r as BoardBinding } from "./backend-CPC_vyOo.mjs";
1
+ import { t as Actor, x as Attachment } from "./folds-8wRRXjAt.mjs";
2
+ import { A as InboxItem, Ct as SyncCopiesView, D as IdentityView, Et as SyncFindView, H as MessageView, It as ThreadView, L as LinkedAccount, Lt as ThreadsPage, Mt as SyncThreadView, Pt as SyncedView, S as EventsPage, U as MessagesPage, Ut as UploadTicket, V as Me, b as Discovery, ft as SourceJoin, ht as SourceRead, k as InboxEntry, o as ProfileOption, q as PersonView, st as ScheduleView, tt as RegistrationView, v as ChannelView, xt as SyncCodeView, z as MatchView } from "./client-CjAatpV4.mjs";
3
+ import { i as Caller, n as BackendRoute, r as BoardBinding } from "./backend-n3hajfrD.mjs";
4
4
  //#region src/server/backend-vercel.d.ts
5
5
  type VercelOptions = {
6
6
  /** The team whose board it is: its ID (`team_…`) or its slug. In Node, by default your deployment's, or `VERCEL_ORG_ID`. */
@@ -31,8 +31,8 @@ type VercelEnvironment = {
31
31
  };
32
32
  /**
33
33
  * Vercel's board for your team, in Vercel's API: this deployment keeps nothing and reads and writes
34
- * it as itself, its people as themselves. Messages, threads, reactions, labels, and a channel's
35
- * changes work; what the API keeps none of yet (schedules, files, watchers, sync) answers 501.
34
+ * it as itself, its people as themselves. Messages, threads, reactions, labels, a channel's
35
+ * changes, schedules, files, and watchers work; what the API keeps none of yet (sync) answers 501.
36
36
  */
37
37
  declare function createVercel(options: VercelOptions, environment: VercelEnvironment | null): {
38
38
  discovery: (caller: Caller, input: {
@@ -96,6 +96,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
96
96
  })[] | undefined;
97
97
  id?: string | undefined;
98
98
  attachments?: string[] | undefined;
99
+ after?: string | undefined;
99
100
  }, signal: AbortSignal) => Promise<{
100
101
  message: MessageView;
101
102
  unknownMentions?: string[];
@@ -109,12 +110,15 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
109
110
  id: string;
110
111
  } & {
111
112
  text: string;
113
+ revision?: number | undefined;
112
114
  }, signal: AbortSignal) => Promise<{
113
115
  ok: true;
114
116
  unknownMentions?: string[];
115
117
  }>;
116
118
  delete: (caller: Caller, input: {
117
119
  id: string;
120
+ } & {
121
+ revision?: number | undefined;
118
122
  }, signal: AbortSignal) => Promise<{
119
123
  ok: true;
120
124
  }>;
@@ -131,6 +135,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
131
135
  } & {
132
136
  set?: Record<string, string> | undefined;
133
137
  unset?: string[] | undefined;
138
+ if?: Record<string, string | null> | undefined;
134
139
  }, signal: AbortSignal) => Promise<{
135
140
  ok: true;
136
141
  }>;
@@ -182,6 +187,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
182
187
  name?: string | null | undefined;
183
188
  type?: string | null | undefined;
184
189
  size: number;
190
+ sha256?: string | undefined;
185
191
  }, signal: AbortSignal) => Promise<UploadTicket>;
186
192
  uploadFinish: (caller: Caller, input: {
187
193
  id: string;
@@ -315,6 +321,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
315
321
  message: string | null;
316
322
  }[] | undefined;
317
323
  id?: string | undefined;
324
+ after?: string | undefined;
318
325
  } | {
319
326
  author: {
320
327
  id: string;
@@ -336,6 +343,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
336
343
  channel: string;
337
344
  thread: string;
338
345
  } | undefined;
346
+ revision?: number | undefined;
339
347
  } | {
340
348
  author: {
341
349
  id: string;
@@ -351,6 +359,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
351
359
  } | undefined;
352
360
  message?: string | undefined;
353
361
  op: "delete";
362
+ revision?: number | undefined;
354
363
  } | {
355
364
  author: {
356
365
  id: string;
@@ -385,6 +394,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
385
394
  op: "labels";
386
395
  set?: Record<string, string> | undefined;
387
396
  unset?: string[] | undefined;
397
+ if?: Record<string, string | null> | undefined;
388
398
  } | {
389
399
  author: {
390
400
  id: string;
@@ -648,6 +658,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
648
658
  }>;
649
659
  routes: BackendRoute[];
650
660
  keeps: boolean;
661
+ sendsSchedulesHere: boolean;
651
662
  bind(next: BoardBinding): void;
652
663
  /** Registers this board's watchers with Vercel's API now, as `POST /watchers` does. */
653
664
  registerWatchers: () => Promise<RegistrationView>;
@@ -1,4 +1,4 @@
1
- import { n as VercelOptions, t as VercelBackend } from "./backend-vercel-4tvcPKqX.mjs";
1
+ import { n as VercelOptions, t as VercelBackend } from "./backend-vercel-qznuDxnN.mjs";
2
2
  //#region src/backend-vercel/index.default.d.ts
3
3
  /**
4
4
  * Your team's board, in Vercel's API. Outside Node there's no environment to find credentials or
@@ -1,4 +1,4 @@
1
- import { t as createVercel } from "./backend-vercel-BNauAs7w.mjs";
1
+ import { t as createVercel } from "./backend-vercel-C4Kee2-K.mjs";
2
2
  //#region src/backend-vercel/index.default.ts
3
3
  /**
4
4
  * Your team's board, in Vercel's API. Outside Node there's no environment to find credentials or
@@ -1,4 +1,4 @@
1
- import { n as VercelOptions, t as VercelBackend } from "./backend-vercel-4tvcPKqX.mjs";
1
+ import { n as VercelOptions, t as VercelBackend } from "./backend-vercel-qznuDxnN.mjs";
2
2
  //#region src/backend-vercel/index.d.ts
3
3
  /** Where `token` comes from, in words: what `vb whoami` says. */
4
4
  export declare function credentialSource(): string;
@@ -1,2 +1,2 @@
1
- import { n as handTeam, r as vercel, t as credentialSource } from "./backend-vercel-DQc_TaHp.mjs";
1
+ import { n as handTeam, r as vercel, t as credentialSource } from "./backend-vercel-Kd8nvW5A.mjs";
2
2
  export { credentialSource, handTeam, vercel };
@@ -1,5 +1,5 @@
1
- import "./folds-ZkHM4M-v.mjs";
2
- import "./errors-DDktVeUa.mjs";
1
+ import "./folds-8wRRXjAt.mjs";
2
+ import "./errors-87TVtXeb.mjs";
3
3
  //#region src/server/blob.d.ts
4
4
  /**
5
5
  * Where attachments' bytes live. The board writes each file once, under a key
@@ -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-CusN4x2l.mjs";
3
+ import { r as badRequest, s as notFound, t as HttpError } from "./errors-C_5rB8Iz.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";
@@ -1,6 +1,6 @@
1
- import { d as ExternalFile, f as ExternalLink, l as ExternalAuthor, t as Actor } from "./folds-ZkHM4M-v.mjs";
2
- import { dt as SourceJoin, mt as SourceRead } from "./client-BqAjk0Q4.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-DmSTp9lI.mjs";
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
4
  import { Adapter, Author, FileUpload, Message, ReactionEvent } from "chat";
5
5
  //#region src/sync/types.d.ts
6
6
  declare function externalAuthor(platform: string, author: Author): ExternalAuthor;
@@ -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-D6gvrOh3.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-BtH69yqt.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-C3R1AWgm.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-B5DHjZKK.mjs";
2
2
  export { commands, filters, globalOptions, groups, option, options, usage };
@@ -1,5 +1,5 @@
1
- import { d as Board } from "./backend-CPC_vyOo.mjs";
2
- import "./index-D3VvNfh_.mjs";
1
+ import { d as Board } from "./backend-n3hajfrD.mjs";
2
+ import "./index-BygAXP7t.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-CVBqe3VO.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-D2s-QBOK.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-C3R1AWgm.mjs";
5
- import { l as projectDir } from "./node-Zd_C-gxP.mjs";
6
- import { r as main, u as who } from "./main-CVBqe3VO.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";
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-G9RuaDSr.mjs");
664
+ const { localHandle, serveLocal } = await import("./serve-lPoBDEds.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"),