@vercel/board 0.28.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 (139) hide show
  1. package/dist/{a2-B0_VlbtC.mjs → a2-D_28Pdip.mjs} +197 -259
  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-BLXVp4oZ.mjs → actors-xPiamLWk.mjs} +3 -3
  6. package/dist/{auth-lTkOgvcE.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 +3 -3
  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 +20 -7
  27. package/dist/backend-a2.mjs +2 -1
  28. package/dist/{backend-http-BvNFrvcY.mjs → backend-http-16SbvnAK.mjs} +2 -2
  29. package/dist/{backend-http-E7YU0qhn.d.mts → backend-http-3PoBYQoo.d.mts} +15 -3
  30. package/dist/{backend-http-CfLVYqmj.mjs → backend-http-BPaNYLWj.mjs} +4 -5
  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-ZKYTe3xa.d.mts → backend-n3hajfrD.d.mts} +12 -6
  36. package/dist/{backend-vercel-D87pIp2k.mjs → backend-vercel-C4Kee2-K.mjs} +260 -30
  37. package/dist/{backend-vercel-DTrDiSk5.mjs → backend-vercel-Kd8nvW5A.mjs} +3 -3
  38. package/dist/{backend-vercel-CIRObA0t.d.mts → backend-vercel-qznuDxnN.d.mts} +20 -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-B-9AvYis.d.mts → blob-CqILvdmr.d.mts} +2 -2
  44. package/dist/blob-DQK5prXT.mjs +182 -0
  45. package/dist/{chat-provider-DnHibqnM.mjs → chat-provider-COtsU5jB.mjs} +2 -2
  46. package/dist/{chat-provider-DsNEBQiA.d.mts → chat-provider-D6jVT4wS.d.mts} +5 -3
  47. package/dist/cli-commands.d.mts +1 -1
  48. package/dist/cli-commands.mjs +1 -1
  49. package/dist/cli-embedded.d.mts +2 -2
  50. package/dist/cli-embedded.mjs +1 -1
  51. package/dist/cli.mjs +5 -5
  52. package/dist/{client-xLxeDz0f.d.mts → client-CjAatpV4.d.mts} +67 -10
  53. package/dist/{commands-BFookFtn.mjs → commands-B5DHjZKK.mjs} +54 -10
  54. package/dist/{commands-D6gvrOh3.d.mts → commands-BtH69yqt.d.mts} +17 -0
  55. package/dist/{conformance-DlnfuzEB.mjs → conformance-C7fwIvmN.mjs} +258 -18
  56. package/dist/conformance.d.mts +1 -1
  57. package/dist/conformance.mjs +1 -1
  58. package/dist/{engine-Bfe6pcXb.mjs → engine-DkdkXQEy.mjs} +21 -13
  59. package/dist/{errors-CB4tnZGz.d.mts → errors-87TVtXeb.d.mts} +1 -1
  60. package/dist/{client-D0Px7KzF.mjs → errors-C_5rB8Iz.mjs} +104 -4
  61. package/dist/{events-CewD4kw7.mjs → events-qf5XG5VQ.mjs} +2 -2
  62. package/dist/{exports-CCzw2Vdw.mjs → exports-CG0P8Qi4.mjs} +59 -33
  63. package/dist/exports-CrGA5_yN.d.mts +5 -0
  64. package/dist/{external-BV_cUncs.mjs → external-D3p5T57R.mjs} +4 -2
  65. package/dist/{folds-jS9wlh9_.d.mts → folds-8wRRXjAt.d.mts} +4 -0
  66. package/dist/{folds-Dyjput8M.mjs → folds-DGYkMW19.mjs} +3 -2
  67. package/dist/{index-C05it0mC.d.mts → index-BygAXP7t.d.mts} +2 -2
  68. package/dist/index.d.mts +6 -6
  69. package/dist/{index.default-DmKxH_E6.mjs → index.default-DhkgpfR2.mjs} +1 -1
  70. package/dist/index.mjs +5 -6
  71. package/dist/index.node.d.mts +6 -6
  72. package/dist/index.node.mjs +3 -4
  73. package/dist/inspect/app.js +29 -29
  74. package/dist/inspect/engine.js +5 -5
  75. package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
  76. package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
  77. package/dist/{main-eci7w-aP.mjs → main-D2s-QBOK.mjs} +76 -26
  78. package/dist/{middleware-ChKM1zuu.mjs → middleware-DlJQYiwV.mjs} +2 -2
  79. package/dist/{node-Zd_C-gxP.mjs → node-D8Rtg1su.mjs} +1 -1
  80. package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
  81. package/dist/{project-DMDST9kl.mjs → project-C_2xs57L.mjs} +1 -1
  82. package/dist/{provider-poIkXapd.mjs → provider-BaVmi4Hi.mjs} +3 -1
  83. package/dist/{provider-Cdg1qadh.d.mts → provider-Bm46LAdp.d.mts} +9 -2
  84. package/dist/{redis-tables-DN9PfIDi.mjs → redis-tables-BWJZHUGg.mjs} +41 -25
  85. package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
  86. package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
  87. package/dist/{reducers-BcmSs4qf.mjs → reducers-8jqLewgw.mjs} +5 -5
  88. package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
  89. package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
  90. package/dist/{schemas-Be08nyfW.mjs → schemas-DBmUO6pr.mjs} +2 -1
  91. package/dist/{serve-B09an9DQ.mjs → serve-lPoBDEds.mjs} +7 -6
  92. package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
  93. package/dist/{server-DTX0prGr.mjs → server-D82a4uMe.mjs} +4 -4
  94. package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
  95. package/dist/{serving-CvOWEym2.mjs → serving-DKny38LK.mjs} +1 -1
  96. package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
  97. package/dist/{sql-U9WTxrwJ.mjs → sql-Bj0fk6dn.mjs} +2 -2
  98. package/dist/{sqlite-db-Ds4majtT.mjs → sqlite-db-Dc8G78XV.mjs} +1 -1
  99. package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
  100. package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
  101. package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
  102. package/dist/store-memory-4IBl0Ztu.mjs +2 -0
  103. package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
  104. package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
  105. package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
  106. package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
  107. package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
  108. package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
  109. package/dist/sync-github.d.mts +4 -2
  110. package/dist/sync-github.mjs +4 -14
  111. package/dist/sync-kapso.d.mts +3 -2
  112. package/dist/sync-kapso.mjs +3 -3
  113. package/dist/sync-linear.d.mts +12 -2
  114. package/dist/sync-linear.mjs +191 -9
  115. package/dist/sync-slack.d.mts +41 -5
  116. package/dist/sync-slack.mjs +70 -11
  117. package/dist/sync.chat.d.mts +1 -1
  118. package/dist/sync.chat.mjs +1 -1
  119. package/dist/sync.d.mts +2 -2
  120. package/dist/sync.mjs +3 -3
  121. package/dist/tags-BpFo6q0_.mjs +14 -0
  122. package/dist/{targets-C-wdR5wP.mjs → targets-Cgjs29T0.mjs} +1 -1
  123. package/dist/{tokens-BuCU-61R.mjs → tokens-C5_yGRlL.mjs} +2 -2
  124. package/dist/{types-aCAf6lGU.d.mts → types-shZmGZ-q.d.mts} +2 -2
  125. package/dist/{watchers-hash-7DJU9b-n.mjs → watchers-hash-B_5b0-ZG.mjs} +50 -19
  126. package/docs/01-board/04-messages.md +93 -0
  127. package/docs/01-board/08-middleware.md +5 -2
  128. package/docs/02-sync/03-slack.md +6 -2
  129. package/docs/02-sync/07-linear.md +28 -6
  130. package/docs/04-reference/01-cli.md +27 -12
  131. package/docs/04-reference/02-board.md +20 -5
  132. package/docs/04-reference/03-http.md +17 -6
  133. package/docs/04-reference/05-conformance.md +14 -3
  134. package/package.json +2 -2
  135. package/dist/errors-CusN4x2l.mjs +0 -88
  136. package/dist/exports-D22M68rk.d.mts +0 -5
  137. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
  138. package/dist/store-memory-ZLiZbH3R.mjs +0 -2
  139. package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
@@ -1,8 +1,10 @@
1
- import { _ as RemoteWatcherId } from "./schemas-Be08nyfW.mjs";
2
- import { O as RegisterBody, a as METHODS, d as VERSION, n as compileWatcher, t as canonicalWatchers } from "./watchers-hash-7DJU9b-n.mjs";
1
+ import { r as IMPORT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
+ import { _ as RemoteWatcherId } from "./schemas-DBmUO6pr.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";
3
5
  import { o as sha256, r as derivedId } from "./wait-until-Dw4XrIDQ.mjs";
4
- import { a as forbidden, o as json, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
5
- import { a as wakePath, i as registrant, n as callerProfile, t as PROJECTS_OWN } from "./backend-http-CfLVYqmj.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";
6
8
  //#region src/server/backend-vercel.ts
7
9
  const PROFILE = /^[A-Za-z][A-Za-z0-9-]{0,31}$/;
8
10
  const SESSION = /^[A-Za-z0-9_.:-]{1,128}$/;
@@ -17,14 +19,13 @@ const PAGE_MAX = 100;
17
19
  const WAIT_MAX_S = 25;
18
20
  /** Pages a filtered read of one channel scans for matches before it answers with what it found. */
19
21
  const SCAN_PAGES = 10;
20
- /** What the API keeps none of: a check that gets one of these is told it isn't kept here. */
21
- const UNKEPT = {
22
- schedule: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
23
- schedules: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
24
- cancelSchedule: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
25
- upload: ["no_files", "Vercel's boards keep no files yet"],
26
- attachment: ["no_files", "Vercel's boards keep no files yet"]
27
- };
22
+ /** A file as the board names it. */
23
+ const fileOf = ({ id, name, type, size }) => ({
24
+ id,
25
+ name,
26
+ type,
27
+ size
28
+ });
28
29
  const notSupported = (what) => new HttpError(501, "not_supported", `Vercel's boards don't support ${what} yet`);
29
30
  /** What a caller board() verified may do. Null for this deployment's own code, which may do what its credentials may. */
30
31
  const accessOf = (caller) => "identity" in caller ? caller.identity.principal.access : null;
@@ -65,6 +66,13 @@ const authorOf = (actor) => ({
65
66
  profile: actor.profile?.name ?? null,
66
67
  session: actor.session ?? null
67
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
+ };
68
76
  /** The API names nobody, so an actor's handle is its profile, or a short form of its principal. */
69
77
  function actorView(actor) {
70
78
  const { principal } = actor;
@@ -81,14 +89,15 @@ function actorView(actor) {
81
89
  }
82
90
  /**
83
91
  * Vercel's board for your team, in Vercel's API: this deployment keeps nothing and reads and writes
84
- * it as itself, its people as themselves. Messages, threads, reactions, labels, and a channel's
85
- * 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.
86
94
  */
87
95
  function createVercel(options, environment) {
88
96
  const origin = (options.api ?? "https://api.vercel.com").replace(/\/+$/, "");
89
97
  if (!/^https:\/\/[^/]+$|^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) throw new Error(`vercel: ${origin} isn't Vercel's API: pass an https origin`);
90
98
  if (!options.fetch && !environment) throw new Error("vercel: pass a fetch that sends credentials: outside Node there's no environment to find them in");
91
99
  const doFetch = options.fetch ?? environment.fetch();
100
+ const filesFetch = options.files ?? fetch;
92
101
  let binding = null;
93
102
  /** This board's own code's names, which a caller here never takes as its profile. */
94
103
  const own = /* @__PURE__ */ new Set();
@@ -318,10 +327,42 @@ function createVercel(options, environment) {
318
327
  editedAt: message.editedAt === void 0 ? null : iso(message.editedAt),
319
328
  deleted: message.deletedAt !== void 0,
320
329
  undelivered: {},
321
- attachments: [],
330
+ attachments: message.attachments ?? [],
322
331
  links: [],
323
332
  version: message.version,
324
- 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)
325
366
  };
326
367
  }
327
368
  const positions = /* @__PURE__ */ new Map();
@@ -560,7 +601,7 @@ function createVercel(options, environment) {
560
601
  by,
561
602
  origin: null,
562
603
  from: null,
563
- attachments: [],
604
+ attachments: message.attachments,
564
605
  links: []
565
606
  };
566
607
  break;
@@ -786,6 +827,22 @@ function createVercel(options, environment) {
786
827
  function checkCaller(method, caller) {
787
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`);
788
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
+ }
789
846
  const meOf = (identity) => {
790
847
  const { principal, author, actor } = identity;
791
848
  return {
@@ -965,8 +1022,6 @@ function createVercel(options, environment) {
965
1022
  });
966
1023
  const methods = {
967
1024
  ...Object.fromEntries(METHODS.map((method) => [method, async () => {
968
- const unkept = UNKEPT[method];
969
- if (unkept) throw new HttpError(501, unkept[0], unkept[1]);
970
1025
  throw notSupported(method === "register" || method === "watchers" || method === "matches" || method === "ack" ? "watchers" : method);
971
1026
  }])),
972
1027
  async discovery() {
@@ -1144,12 +1199,83 @@ function createVercel(options, environment) {
1144
1199
  })
1145
1200
  };
1146
1201
  },
1202
+ async upload(caller, { data, name, type }) {
1203
+ const who = whoOf(caller);
1204
+ await checkUploader(caller, who);
1205
+ if (data.byteLength === 0) throw new HttpError(400, "bad_request", "the file is empty");
1206
+ if (data.byteLength > 26214400) throw new HttpError(413, "too_large", `attachments are at most ${IMPORT_BYTES_MAX / 1024 / 1024} MB`);
1207
+ const bytes = new Uint8Array(data);
1208
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
1209
+ const started = await api(who, "POST", await onBoard(who, "/attachments"), { body: {
1210
+ operationId: operationId(),
1211
+ name: fileName(name),
1212
+ type: mediaType(type),
1213
+ size: data.byteLength,
1214
+ sha256: btoa(String.fromCharCode(...digest))
1215
+ } });
1216
+ const put = await filesFetch(started.upload.url, {
1217
+ method: started.upload.method,
1218
+ headers: started.upload.headers,
1219
+ body: bytes,
1220
+ redirect: "error"
1221
+ });
1222
+ if (!put.ok) throw new HttpError(502, "upload_failed", `the upload to Vercel's storage answered ${put.status}`);
1223
+ const { attachment } = await api(who, "POST", await onBoard(who, `/attachments/${encodeURIComponent(started.attachment.id)}/complete`));
1224
+ return { attachment: fileOf(attachment) };
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
+ },
1250
+ async attachment(caller, { id }, signal) {
1251
+ const who = whoOf(caller);
1252
+ const access = accessOf(caller);
1253
+ const found = await api(who, "GET", await onBoard(who, `/attachments/${encodeURIComponent(id)}`), signal ? { signal } : {});
1254
+ if (access && !found.attachment.messageId) {
1255
+ if (!await isProject(caller, who)) throw notFound(`no attachment: ${id}`, "attachment_not_found");
1256
+ } else if (access) {
1257
+ const { message } = await readableMessage(who, access, found.attachment.messageId, signal).catch((error) => {
1258
+ if (error instanceof HttpError && error.status === 404) throw notFound(`no attachment: ${id}`, "attachment_not_found");
1259
+ throw error;
1260
+ });
1261
+ if (message.deletedAt !== void 0) throw notFound(`no attachment: ${id}`, "attachment_not_found");
1262
+ }
1263
+ const response = await filesFetch(found.url, {
1264
+ redirect: "error",
1265
+ ...signal ? { signal } : {}
1266
+ });
1267
+ if (!response.ok || !response.body) throw new HttpError(502, "download_failed", `Vercel's storage answered ${response.status}`);
1268
+ return {
1269
+ attachment: fileOf(found.attachment),
1270
+ data: response.body
1271
+ };
1272
+ },
1147
1273
  async post(caller, input, signal) {
1148
1274
  checkCaller("post", caller);
1149
- if (input.attachments?.length) throw new HttpError(501, "no_files", "Vercel's boards keep no files yet");
1150
1275
  if (input.sync?.length) throw notSupported("sync");
1151
1276
  const who = whoOf(caller);
1152
1277
  const access = accessOf(caller);
1278
+ if (input.attachments?.length) await checkOwner("post files", caller, who);
1153
1279
  const labeled = Object.keys(input.labels ?? {}).length > 0;
1154
1280
  const clientId = input.id !== void 0 ? await postId(caller, input.id) : void 0;
1155
1281
  let made;
@@ -1163,7 +1289,8 @@ function createVercel(options, environment) {
1163
1289
  channelId: channel.id,
1164
1290
  text: input.text,
1165
1291
  ...input.labels && Object.keys(input.labels).length ? { labels: input.labels } : {},
1166
- ...clientId ? { clientId } : {}
1292
+ ...clientId ? { clientId } : {},
1293
+ ...input.attachments?.length ? { attachments: input.attachments } : {}
1167
1294
  },
1168
1295
  signal
1169
1296
  });
@@ -1171,14 +1298,25 @@ function createVercel(options, environment) {
1171
1298
  const { thread, message: to, channel } = await readableMessage(who, access, input.re, signal);
1172
1299
  need(access, "post", channel.name);
1173
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
+ }
1174
1307
  made = await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(thread.id)}/messages`), {
1175
1308
  body: {
1176
1309
  operationId: operationId(),
1177
1310
  text: input.text,
1178
1311
  ...to.sequence > 1 ? { replyTo: to.sequence } : {},
1179
- ...clientId ? { clientId } : {}
1312
+ ...clientId ? { clientId } : {},
1313
+ ...input.attachments?.length ? { attachments: input.attachments } : {},
1314
+ ...after ? { after: after.sequence } : {}
1180
1315
  },
1181
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;
1182
1320
  });
1183
1321
  if (input.labels && Object.keys(input.labels).length) {
1184
1322
  const labeled = await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(thread.id)}/messages/${made.message.sequence}/labels`), {
@@ -1210,27 +1348,35 @@ function createVercel(options, environment) {
1210
1348
  if (access && !access.readable(channel.name)) throw notFound(`no post of yours has id ${id}`, "post_not_found");
1211
1349
  return { message: await view(who, found.message, channel.name) };
1212
1350
  },
1213
- async edit(caller, { id, text }, signal) {
1214
- checkCaller("edit", caller);
1351
+ async edit(caller, { id, text, revision }, signal) {
1215
1352
  const who = whoOf(caller);
1353
+ await checkOwner("edit", caller, who);
1216
1354
  const at = locate(id);
1217
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);
1218
1358
  await api(who, "PATCH", await onBoard(who, `/threads/${encodeURIComponent(at.threadId)}/messages/${at.sequence}`), {
1219
1359
  body: {
1220
1360
  operationId: operationId(),
1221
- text
1361
+ text,
1362
+ ...revision !== void 0 ? { revision } : {}
1222
1363
  },
1223
1364
  signal
1224
1365
  });
1225
1366
  return { ok: true };
1226
1367
  },
1227
- async delete(caller, { id }, signal) {
1228
- checkCaller("delete", caller);
1368
+ async delete(caller, { id, revision }, signal) {
1229
1369
  const who = whoOf(caller);
1370
+ await checkOwner("delete", caller, who);
1230
1371
  const at = locate(id);
1231
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);
1232
1375
  await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(at.threadId)}/messages/${at.sequence}/delete`), {
1233
- body: { operationId: operationId() },
1376
+ body: {
1377
+ operationId: operationId(),
1378
+ ...revision !== void 0 ? { revision } : {}
1379
+ },
1234
1380
  signal
1235
1381
  });
1236
1382
  return { ok: true };
@@ -1252,7 +1398,7 @@ function createVercel(options, environment) {
1252
1398
  });
1253
1399
  return { ok: true };
1254
1400
  },
1255
- async label(caller, { id, set, unset }, signal) {
1401
+ async label(caller, { id, set, unset, if: expected }, signal) {
1256
1402
  checkCaller("label", caller);
1257
1403
  const who = whoOf(caller);
1258
1404
  const at = locate(id);
@@ -1263,12 +1409,95 @@ function createVercel(options, environment) {
1263
1409
  body: {
1264
1410
  operationId: operationId(),
1265
1411
  ...set ? { set } : {},
1266
- ...unset ? { unset } : {}
1412
+ ...unset ? { unset } : {},
1413
+ ...expected && Object.keys(expected).length ? { if: expected } : {}
1267
1414
  },
1268
1415
  signal
1269
1416
  });
1270
1417
  return { ok: true };
1271
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
+ },
1272
1501
  async events(caller, query, signal) {
1273
1502
  checkCaller("events", caller);
1274
1503
  const who = whoOf(caller);
@@ -1341,6 +1570,7 @@ function createVercel(options, environment) {
1341
1570
  }])),
1342
1571
  routes: runner.routes,
1343
1572
  keeps: false,
1573
+ sendsSchedulesHere: false,
1344
1574
  bind(next) {
1345
1575
  if (binding) throw new Error("vercel: this backend already serves a board: create one vercel() per board()");
1346
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-D87pIp2k.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-jS9wlh9_.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-xLxeDz0f.mjs";
3
- import { i as Caller, n as BackendRoute, r as BoardBinding } from "./backend-ZKYTe3xa.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`. */
@@ -14,6 +14,8 @@ type VercelOptions = {
14
14
  * a deployment's OIDC token, or your `vercel login`. Elsewhere, pass one that sends them.
15
15
  */
16
16
  fetch?: typeof fetch;
17
+ /** How files reach Vercel's storage, on URLs the API signs: `fetch` by default, never with credentials. */
18
+ files?: typeof fetch;
17
19
  };
18
20
  /** What finds the API's credentials and the team, in the Node build. A browser has none. */
19
21
  type VercelEnvironment = {
@@ -29,8 +31,8 @@ type VercelEnvironment = {
29
31
  };
30
32
  /**
31
33
  * Vercel's board for your team, in Vercel's API: this deployment keeps nothing and reads and writes
32
- * it as itself, its people as themselves. Messages, threads, reactions, labels, and a channel's
33
- * 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.
34
36
  */
35
37
  declare function createVercel(options: VercelOptions, environment: VercelEnvironment | null): {
36
38
  discovery: (caller: Caller, input: {
@@ -94,6 +96,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
94
96
  })[] | undefined;
95
97
  id?: string | undefined;
96
98
  attachments?: string[] | undefined;
99
+ after?: string | undefined;
97
100
  }, signal: AbortSignal) => Promise<{
98
101
  message: MessageView;
99
102
  unknownMentions?: string[];
@@ -107,12 +110,15 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
107
110
  id: string;
108
111
  } & {
109
112
  text: string;
113
+ revision?: number | undefined;
110
114
  }, signal: AbortSignal) => Promise<{
111
115
  ok: true;
112
116
  unknownMentions?: string[];
113
117
  }>;
114
118
  delete: (caller: Caller, input: {
115
119
  id: string;
120
+ } & {
121
+ revision?: number | undefined;
116
122
  }, signal: AbortSignal) => Promise<{
117
123
  ok: true;
118
124
  }>;
@@ -129,6 +135,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
129
135
  } & {
130
136
  set?: Record<string, string> | undefined;
131
137
  unset?: string[] | undefined;
138
+ if?: Record<string, string | null> | undefined;
132
139
  }, signal: AbortSignal) => Promise<{
133
140
  ok: true;
134
141
  }>;
@@ -180,6 +187,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
180
187
  name?: string | null | undefined;
181
188
  type?: string | null | undefined;
182
189
  size: number;
190
+ sha256?: string | undefined;
183
191
  }, signal: AbortSignal) => Promise<UploadTicket>;
184
192
  uploadFinish: (caller: Caller, input: {
185
193
  id: string;
@@ -257,6 +265,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
257
265
  } & {
258
266
  target: string;
259
267
  limit?: number | undefined;
268
+ after?: string | undefined;
260
269
  }, signal: AbortSignal) => Promise<SourceRead>;
261
270
  joinSource: (caller: Caller, input: {
262
271
  key: string;
@@ -312,6 +321,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
312
321
  message: string | null;
313
322
  }[] | undefined;
314
323
  id?: string | undefined;
324
+ after?: string | undefined;
315
325
  } | {
316
326
  author: {
317
327
  id: string;
@@ -333,6 +343,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
333
343
  channel: string;
334
344
  thread: string;
335
345
  } | undefined;
346
+ revision?: number | undefined;
336
347
  } | {
337
348
  author: {
338
349
  id: string;
@@ -348,6 +359,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
348
359
  } | undefined;
349
360
  message?: string | undefined;
350
361
  op: "delete";
362
+ revision?: number | undefined;
351
363
  } | {
352
364
  author: {
353
365
  id: string;
@@ -382,6 +394,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
382
394
  op: "labels";
383
395
  set?: Record<string, string> | undefined;
384
396
  unset?: string[] | undefined;
397
+ if?: Record<string, string | null> | undefined;
385
398
  } | {
386
399
  author: {
387
400
  id: string;
@@ -574,6 +587,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
574
587
  type: string;
575
588
  title: string;
576
589
  url: string | null;
590
+ body?: string | null | undefined;
577
591
  labels: Record<string, string>;
578
592
  unset: string[];
579
593
  owns?: string[] | undefined;
@@ -644,6 +658,7 @@ declare function createVercel(options: VercelOptions, environment: VercelEnviron
644
658
  }>;
645
659
  routes: BackendRoute[];
646
660
  keeps: boolean;
661
+ sendsSchedulesHere: boolean;
647
662
  bind(next: BoardBinding): void;
648
663
  /** Registers this board's watchers with Vercel's API now, as `POST /watchers` does. */
649
664
  registerWatchers: () => Promise<RegistrationView>;
@@ -1,4 +1,4 @@
1
- import { n as VercelOptions, t as VercelBackend } from "./backend-vercel-CIRObA0t.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-D87pIp2k.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-CIRObA0t.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-DTrDiSk5.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-jS9wlh9_.mjs";
2
- import "./errors-CB4tnZGz.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