@vercel/board 0.29.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 (156) hide show
  1. package/README.md +8 -17
  2. package/dist/{a2-HUbQJ0RD.mjs → a2-DngWY2Eb.mjs} +741 -415
  3. package/dist/access-CY5xVMIK.mjs +242 -0
  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-DcJhFbwJ.mjs → actors-Bm-ojWWT.mjs} +2 -2
  8. package/dist/{auth-JO22iIwr.mjs → auth-CyO2aw5n.mjs} +126 -236
  9. package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
  10. package/dist/{backend-CPC_vyOo.d.mts → backend-Bob-5CM7.d.mts} +93 -42
  11. package/dist/backend-a2/blob-vercel.d.mts +1 -1
  12. package/dist/backend-a2/contract.d.mts +38 -98
  13. package/dist/backend-a2/contract.mjs +1 -1
  14. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  15. package/dist/backend-a2/scheduler-local.mjs +1 -1
  16. package/dist/backend-a2/scheduler-qstash.d.mts +3 -3
  17. package/dist/backend-a2/scheduler-qstash.mjs +8 -8
  18. package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
  19. package/dist/backend-a2/scheduler-vercel.mjs +6 -6
  20. package/dist/backend-a2/store-memory.d.mts +2 -2
  21. package/dist/backend-a2/store-memory.mjs +3 -3
  22. package/dist/backend-a2/store-postgres.d.mts +1 -1
  23. package/dist/backend-a2/store-postgres.mjs +34 -15
  24. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  25. package/dist/backend-a2/store-redis-http.mjs +6 -6
  26. package/dist/backend-a2/store-redis.d.mts +2 -2
  27. package/dist/backend-a2/store-redis.mjs +5 -5
  28. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  29. package/dist/backend-a2/store-sqlite.mjs +3 -3
  30. package/dist/backend-a2.d.mts +143 -242
  31. package/dist/backend-a2.mjs +2 -2
  32. package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-BRrRbjRx.d.mts} +59 -45
  33. package/dist/{backend-http-CzIJMm58.mjs → backend-http-CvUKOn-m.mjs} +11 -10
  34. package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-D8mUzuKQ.mjs} +2 -2
  35. package/dist/backend-http.d.mts +1 -1
  36. package/dist/backend-http.mjs +1 -1
  37. package/dist/backend-http.node.d.mts +1 -1
  38. package/dist/backend-http.node.mjs +1 -1
  39. package/dist/{backend-vercel-DQc_TaHp.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
  40. package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-BaSEUdwg.d.mts} +62 -47
  41. package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-CmY9yZ9m.mjs} +279 -42
  42. package/dist/backend-vercel.d.mts +1 -1
  43. package/dist/backend-vercel.mjs +1 -1
  44. package/dist/backend-vercel.node.d.mts +1 -1
  45. package/dist/backend-vercel.node.mjs +1 -1
  46. package/dist/{blob-DmiQjrYy.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
  47. package/dist/{blob-DdbpJqN1.mjs → blob-WbB2DJKn.mjs} +30 -11
  48. package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-BChYrrA7.d.mts} +32 -28
  49. package/dist/{chat-provider-COtsU5jB.mjs → chat-provider-DkFp2hrh.mjs} +105 -60
  50. package/dist/cli-commands.d.mts +1 -1
  51. package/dist/cli-commands.mjs +1 -1
  52. package/dist/cli-embedded.d.mts +2 -2
  53. package/dist/cli-embedded.mjs +1 -1
  54. package/dist/cli.mjs +4 -4
  55. package/dist/{client-BqAjk0Q4.d.mts → client-COfYrgfV.d.mts} +240 -227
  56. package/dist/{commands-D6gvrOh3.d.mts → commands-DK4W3HCJ.d.mts} +47 -10
  57. package/dist/{commands-C3R1AWgm.mjs → commands-L1JSGz2Z.mjs} +212 -54
  58. package/dist/conformance-DK43HiHf.mjs +1000 -0
  59. package/dist/conformance.d.mts +8 -2
  60. package/dist/conformance.mjs +1 -1
  61. package/dist/{engine-DQxXk7dX.mjs → engine-DurYIRt8.mjs} +29 -210
  62. package/dist/{errors-DDktVeUa.d.mts → errors-2vGs49Db.d.mts} +1 -1
  63. package/dist/{client-D0Px7KzF.mjs → errors-CJXhhzvT.mjs} +132 -27
  64. package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
  65. package/dist/{exports-BG-0Wyn-.mjs → exports-CWIdN0Ub.mjs} +195 -103
  66. package/dist/exports-FBOQWT7O.d.mts +5 -0
  67. package/dist/{folds-BDMSxBUf.mjs → folds-Cu9Q9zyQ.mjs} +3 -22
  68. package/dist/{folds-ZkHM4M-v.d.mts → folds-DLUzfibJ.d.mts} +15 -40
  69. package/dist/{index-D3VvNfh_.d.mts → index-Ho-ayyQL.d.mts} +2 -2
  70. package/dist/index.d.mts +7 -7
  71. package/dist/{index.default-rB8sJ9RX.mjs → index.default-BUcwDaAe.mjs} +1 -1
  72. package/dist/index.mjs +5 -6
  73. package/dist/index.node.d.mts +7 -7
  74. package/dist/index.node.mjs +3 -4
  75. package/dist/inspect/app.js +62 -61
  76. package/dist/inspect/engine.js +3 -3
  77. package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
  78. package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
  79. package/dist/{main-CVBqe3VO.mjs → main-Dz-8komG.mjs} +251 -127
  80. package/dist/{middleware-CXjEeef4.mjs → middleware-BavYSDUa.mjs} +1 -1
  81. package/dist/{node-Zd_C-gxP.mjs → node-BgeGcwX0.mjs} +7 -3
  82. package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
  83. package/dist/{project-CBXlLftU.mjs → project-CQ4VUibA.mjs} +1 -1
  84. package/dist/{provider-DmSTp9lI.d.mts → provider-gynDUoyW.d.mts} +13 -43
  85. package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-DHf85ixI.mjs} +40 -42
  86. package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
  87. package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
  88. package/dist/{reducers-DJzVpXwM.mjs → reducers-DcENwtwZ.mjs} +74 -77
  89. package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
  90. package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
  91. package/dist/{serve-G9RuaDSr.mjs → serve-CTmow-YH.mjs} +7 -7
  92. package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
  93. package/dist/{server-BixV_byP.mjs → server-DG8D6VV0.mjs} +4 -4
  94. package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
  95. package/dist/{serving-B75Y4BF-.mjs → serving-DBFRo6U_.mjs} +1 -1
  96. package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
  97. package/dist/{sql-D4Z_efJX.mjs → sql-CT5xVuGr.mjs} +2 -8
  98. package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-0WeSOEZN.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-kapso.d.mts → sync-chat-sdk.d.mts} +33 -37
  110. package/dist/sync-chat-sdk.mjs +44 -0
  111. package/dist/sync-github.d.mts +5 -18
  112. package/dist/sync-github.mjs +4 -70
  113. package/dist/sync-linear.d.mts +3 -3
  114. package/dist/sync-linear.mjs +6 -5
  115. package/dist/sync-slack.d.mts +7 -7
  116. package/dist/sync-slack.mjs +33 -54
  117. package/dist/sync.d.mts +3 -3
  118. package/dist/sync.mjs +2 -4
  119. package/dist/{tokens-CC3izSNr.mjs → tokens-Dnyd_y1n.mjs} +15 -6
  120. package/dist/{types-No0WdhE5.d.mts → types-CmrFjpml.d.mts} +2 -4
  121. package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-BjwlnGD9.mjs} +124 -115
  122. package/docs/01-board/01-mounting.md +2 -2
  123. package/docs/01-board/03-identity.md +5 -6
  124. package/docs/01-board/04-messages.md +96 -2
  125. package/docs/01-board/05-reading.md +2 -1
  126. package/docs/01-board/06-watchers.md +8 -6
  127. package/docs/01-board/08-middleware.md +6 -3
  128. package/docs/01-board/09-access.md +145 -14
  129. package/docs/02-sync/01-overview.md +19 -26
  130. package/docs/02-sync/02-functions.md +8 -9
  131. package/docs/02-sync/03-slack.md +5 -5
  132. package/docs/02-sync/04-github.md +2 -2
  133. package/docs/02-sync/06-providers.md +91 -62
  134. package/docs/03-guides/01-coding-agents.md +1 -1
  135. package/docs/03-guides/03-ci.md +7 -4
  136. package/docs/04-reference/01-cli.md +109 -51
  137. package/docs/04-reference/02-board.md +76 -28
  138. package/docs/04-reference/03-http.md +76 -38
  139. package/docs/04-reference/04-log.md +8 -11
  140. package/docs/04-reference/05-conformance.md +47 -7
  141. package/docs/index.md +2 -2
  142. package/package.json +7 -10
  143. package/sql/postgres/board.sql +1 -1
  144. package/sql/postgres/schema.sql +1 -1
  145. package/dist/conformance-DlnfuzEB.mjs +0 -556
  146. package/dist/errors-CusN4x2l.mjs +0 -88
  147. package/dist/events-qf5XG5VQ.mjs +0 -75
  148. package/dist/exports-mhAYiU4i.d.mts +0 -5
  149. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
  150. package/dist/store-memory-ZLiZbH3R.mjs +0 -2
  151. package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
  152. package/dist/sync-kapso.mjs +0 -89
  153. package/dist/sync.chat.d.mts +0 -2
  154. package/dist/sync.chat.mjs +0 -2
  155. package/dist/users-yryHKFMo.mjs +0 -42
  156. package/docs/02-sync/05-whatsapp.md +0 -152
@@ -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 RegisterBody, a as METHODS, d as VERSION, n as compileWatcher, t as canonicalWatchers } from "./watchers-hash-BjwlnGD9.mjs";
4
+ import { a as forbidden, f as duration, o as json, r as badRequest, s as notFound, t as HttpError } from "./errors-CJXhhzvT.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-CvUKOn-m.mjs";
7
+ import { o as mediaType, r as fileName } from "./blob-WbB2DJKn.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(/\/+$/, "");
@@ -192,10 +191,13 @@ function createVercel(options, environment) {
192
191
  if (!found) throw new HttpError(502, "remote_error", `Vercel's API names a channel it doesn't list: ${id}`);
193
192
  return found;
194
193
  }
195
- /** A channel to post in, made the first time something posts to it, as on any board. */
196
- async function channelToPost(who, name) {
194
+ /** The channel by `name`, made if it isn't there: `created` says whether this made it. */
195
+ async function ensureChannel(who, name) {
197
196
  const found = await channelNamed(who, name);
198
- if (found) return found;
197
+ if (found) return {
198
+ channel: found,
199
+ created: false
200
+ };
199
201
  try {
200
202
  const { channel } = await api(who, "POST", await onBoard(who, "/channels"), { body: {
201
203
  operationId: operationId(),
@@ -203,15 +205,23 @@ function createVercel(options, environment) {
203
205
  } });
204
206
  channels = null;
205
207
  named.set(name, channel);
206
- return channel;
208
+ return {
209
+ channel,
210
+ created: true
211
+ };
207
212
  } catch (error) {
208
213
  if (error instanceof HttpError && error.code === "channel_name_taken") {
209
214
  const made = await channelNamed(who, name);
210
- if (made) return made;
215
+ if (made) return {
216
+ channel: made,
217
+ created: false
218
+ };
211
219
  }
212
220
  throw error;
213
221
  }
214
222
  }
223
+ /** A channel to post in, made the first time something posts to it, as on any board. */
224
+ const channelToPost = async (who, name) => (await ensureChannel(who, name)).channel;
215
225
  /** The channels a filter names that `access` reads: one, a channel and those under it (`dev/`), or every one. */
216
226
  async function channelsFor(who, filter, access) {
217
227
  const readable = (channel) => access === null || access.readable(channel.name);
@@ -331,7 +341,39 @@ function createVercel(options, environment) {
331
341
  attachments: message.attachments ?? [],
332
342
  links: [],
333
343
  version: message.version,
334
- author: await nameOf(who, message.createdBy)
344
+ revision: message.revision,
345
+ author: await nameOf(who, message.createdBy),
346
+ ...message.schedule ? { schedule: {
347
+ id: message.schedule.id,
348
+ n: message.schedule.n
349
+ } } : {}
350
+ };
351
+ }
352
+ async function scheduleView(who, schedule) {
353
+ const { canceled } = schedule;
354
+ return {
355
+ id: schedule.id,
356
+ channel: (await channelById(who, schedule.channelId)).name,
357
+ re: schedule.threadId === void 0 ? null : idAt(schedule.threadId, 1),
358
+ text: schedule.text,
359
+ labels: { ...schedule.labels },
360
+ noSync: false,
361
+ attachments: [],
362
+ first: iso(schedule.firstAt),
363
+ every: schedule.every ?? null,
364
+ until: schedule.until === void 0 ? null : iso(schedule.until),
365
+ times: schedule.times ?? null,
366
+ sent: schedule.sent,
367
+ next: schedule.nextAt === null ? null : iso(schedule.nextAt),
368
+ last: schedule.lastMessageId ?? null,
369
+ state: schedule.state,
370
+ canceled: canceled ? {
371
+ by: canceled.by ? authorOf(canceled.by) : SCHEDULER,
372
+ reason: canceled.reason === "disabled" ? "revoked" : canceled.reason
373
+ } : null,
374
+ by: authorOf(schedule.createdBy),
375
+ author: await nameOf(who, schedule.createdBy),
376
+ createdAt: iso(schedule.createdAt)
335
377
  };
336
378
  }
337
379
  const positions = /* @__PURE__ */ new Map();
@@ -796,6 +838,22 @@ function createVercel(options, environment) {
796
838
  function checkCaller(method, caller) {
797
839
  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
840
  }
841
+ /**
842
+ * The API owns messages and schedules by principal, and sees every caller here as this project:
843
+ * a caller that is the project (its own deployment's token) changes what it wrote, as on any
844
+ * board, and anyone else would be changing what the project wrote for others.
845
+ */
846
+ /** Whether a caller is this project: the deployment's own code, or a caller verified as its principal. */
847
+ const isProject = async (caller, who) => !("identity" in caller) || caller.identity.principal.id === principalId((await self(who)).actor.principal);
848
+ async function checkOwner(what, caller, who) {
849
+ 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`);
850
+ }
851
+ /** A file is no use until a post or an import carries it, so uploading takes `post` or `sync` somewhere, as on a2(). */
852
+ async function checkUploader(caller, who) {
853
+ await checkOwner("upload", caller, who);
854
+ const access = accessOf(caller);
855
+ if (access && !access.anywhere("post") && !access.anywhere("sync")) throw forbidden("this token can't post anywhere, so it can't upload");
856
+ }
799
857
  const meOf = (identity) => {
800
858
  const { principal, author, actor } = identity;
801
859
  return {
@@ -975,8 +1033,6 @@ function createVercel(options, environment) {
975
1033
  });
976
1034
  const methods = {
977
1035
  ...Object.fromEntries(METHODS.map((method) => [method, async () => {
978
- const unkept = UNKEPT[method];
979
- if (unkept) throw new HttpError(501, unkept[0], unkept[1]);
980
1036
  throw notSupported(method === "register" || method === "watchers" || method === "matches" || method === "ack" ? "watchers" : method);
981
1037
  }])),
982
1038
  async discovery() {
@@ -1027,18 +1083,24 @@ function createVercel(options, environment) {
1027
1083
  },
1028
1084
  async channels(caller) {
1029
1085
  return {
1030
- channels: (await channelsFor(whoOf(caller), void 0, accessOf(caller))).map((channel) => ({
1031
- path: channel.name,
1032
- lastIndex: 0,
1033
- messages: 0,
1034
- threads: 0
1035
- })),
1086
+ channels: (await channelsFor(whoOf(caller), void 0, accessOf(caller))).map((channel) => ({ path: channel.name })).sort((a, b) => a.path.localeCompare(b.path)),
1036
1087
  cursor: cursorOf({
1037
1088
  t: Date.now(),
1038
1089
  feeds: {}
1039
1090
  })
1040
1091
  };
1041
1092
  },
1093
+ async createChannel(caller, { path }) {
1094
+ checkCaller("createChannel", caller);
1095
+ const access = accessOf(caller);
1096
+ need(access, "read", path);
1097
+ need(access, "post", path);
1098
+ const { created } = await ensureChannel(whoOf(caller), path);
1099
+ return {
1100
+ channel: { path },
1101
+ created
1102
+ };
1103
+ },
1042
1104
  async actors(caller, _input, signal) {
1043
1105
  const who = whoOf(caller);
1044
1106
  const listed = (await all(who, await onBoard(who, "/actors"), "actors", {}, signal)).map((actor) => ({
@@ -1155,10 +1217,10 @@ function createVercel(options, environment) {
1155
1217
  };
1156
1218
  },
1157
1219
  async upload(caller, { data, name, type }) {
1158
- checkCaller("upload", caller);
1220
+ const who = whoOf(caller);
1221
+ await checkUploader(caller, who);
1159
1222
  if (data.byteLength === 0) throw new HttpError(400, "bad_request", "the file is empty");
1160
1223
  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
1224
  const bytes = new Uint8Array(data);
1163
1225
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
1164
1226
  const started = await api(who, "POST", await onBoard(who, "/attachments"), { body: {
@@ -1178,12 +1240,37 @@ function createVercel(options, environment) {
1178
1240
  const { attachment } = await api(who, "POST", await onBoard(who, `/attachments/${encodeURIComponent(started.attachment.id)}/complete`));
1179
1241
  return { attachment: fileOf(attachment) };
1180
1242
  },
1243
+ async uploadStart(caller, { name, type, size, sha256 }) {
1244
+ const who = whoOf(caller);
1245
+ await checkUploader(caller, who);
1246
+ 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");
1247
+ const started = await api(who, "POST", await onBoard(who, "/attachments"), { body: {
1248
+ operationId: operationId(),
1249
+ name: fileName(name ?? null),
1250
+ type: mediaType(type ?? null),
1251
+ size,
1252
+ sha256
1253
+ } });
1254
+ return {
1255
+ id: started.attachment.id,
1256
+ url: started.upload.url,
1257
+ headers: started.upload.headers,
1258
+ expiresAt: iso(started.upload.expiresAt)
1259
+ };
1260
+ },
1261
+ async uploadFinish(caller, { id }) {
1262
+ const who = whoOf(caller);
1263
+ await checkUploader(caller, who);
1264
+ const { attachment } = await api(who, "POST", await onBoard(who, `/attachments/${encodeURIComponent(id)}/complete`));
1265
+ return { attachment: fileOf(attachment) };
1266
+ },
1181
1267
  async attachment(caller, { id }, signal) {
1182
1268
  const who = whoOf(caller);
1183
1269
  const access = accessOf(caller);
1184
1270
  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");
1271
+ if (access && !found.attachment.messageId) {
1272
+ if (!await isProject(caller, who)) throw notFound(`no attachment: ${id}`, "attachment_not_found");
1273
+ } else if (access) {
1187
1274
  const { message } = await readableMessage(who, access, found.attachment.messageId, signal).catch((error) => {
1188
1275
  if (error instanceof HttpError && error.status === 404) throw notFound(`no attachment: ${id}`, "attachment_not_found");
1189
1276
  throw error;
@@ -1205,7 +1292,7 @@ function createVercel(options, environment) {
1205
1292
  if (input.sync?.length) throw notSupported("sync");
1206
1293
  const who = whoOf(caller);
1207
1294
  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");
1295
+ if (input.attachments?.length) await checkOwner("post files", caller, who);
1209
1296
  const labeled = Object.keys(input.labels ?? {}).length > 0;
1210
1297
  const clientId = input.id !== void 0 ? await postId(caller, input.id) : void 0;
1211
1298
  let made;
@@ -1228,15 +1315,25 @@ function createVercel(options, environment) {
1228
1315
  const { thread, message: to, channel } = await readableMessage(who, access, input.re, signal);
1229
1316
  need(access, "post", channel.name);
1230
1317
  if (labeled) need(access, "label", channel.name);
1318
+ const after = input.after === void 0 ? void 0 : locate(input.after);
1319
+ if (after !== void 0 && after?.threadId !== thread.id) {
1320
+ const landed = clientId ? await api(who, "GET", await onBoard(who, `/posts/${encodeURIComponent(clientId)}`), { signal }).catch(() => null) : null;
1321
+ if (landed?.thread.id === thread.id) return { message: await view(who, landed.message, channel.name) };
1322
+ throw badRequest(`after: ${input.after} isn't in ${input.re}'s thread`, "invalid_after");
1323
+ }
1231
1324
  made = await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(thread.id)}/messages`), {
1232
1325
  body: {
1233
1326
  operationId: operationId(),
1234
1327
  text: input.text,
1235
1328
  ...to.sequence > 1 ? { replyTo: to.sequence } : {},
1236
1329
  ...clientId ? { clientId } : {},
1237
- ...input.attachments?.length ? { attachments: input.attachments } : {}
1330
+ ...input.attachments?.length ? { attachments: input.attachments } : {},
1331
+ ...after ? { after: after.sequence } : {}
1238
1332
  },
1239
1333
  signal
1334
+ }).catch((error) => {
1335
+ if (error instanceof HttpError && error.code === "newer_message") throw new HttpError(409, "newer_message", `a message came after ${input.after}`);
1336
+ throw error;
1240
1337
  });
1241
1338
  if (input.labels && Object.keys(input.labels).length) {
1242
1339
  const labeled = await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(thread.id)}/messages/${made.message.sequence}/labels`), {
@@ -1268,27 +1365,35 @@ function createVercel(options, environment) {
1268
1365
  if (access && !access.readable(channel.name)) throw notFound(`no post of yours has id ${id}`, "post_not_found");
1269
1366
  return { message: await view(who, found.message, channel.name) };
1270
1367
  },
1271
- async edit(caller, { id, text }, signal) {
1272
- checkCaller("edit", caller);
1368
+ async edit(caller, { id, text, revision }, signal) {
1273
1369
  const who = whoOf(caller);
1370
+ await checkOwner("edit", caller, who);
1274
1371
  const at = locate(id);
1275
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);
1276
1375
  await api(who, "PATCH", await onBoard(who, `/threads/${encodeURIComponent(at.threadId)}/messages/${at.sequence}`), {
1277
1376
  body: {
1278
1377
  operationId: operationId(),
1279
- text
1378
+ text,
1379
+ ...revision !== void 0 ? { revision } : {}
1280
1380
  },
1281
1381
  signal
1282
1382
  });
1283
1383
  return { ok: true };
1284
1384
  },
1285
- async delete(caller, { id }, signal) {
1286
- checkCaller("delete", caller);
1385
+ async delete(caller, { id, revision }, signal) {
1287
1386
  const who = whoOf(caller);
1387
+ await checkOwner("delete", caller, who);
1288
1388
  const at = locate(id);
1289
1389
  if (!at) throw notFound(`message not found: ${id}`, "message_not_found");
1390
+ const access = accessOf(caller);
1391
+ if (access) need(access, "post", (await readableMessage(who, access, id, signal)).channel.name);
1290
1392
  await api(who, "POST", await onBoard(who, `/threads/${encodeURIComponent(at.threadId)}/messages/${at.sequence}/delete`), {
1291
- body: { operationId: operationId() },
1393
+ body: {
1394
+ operationId: operationId(),
1395
+ ...revision !== void 0 ? { revision } : {}
1396
+ },
1292
1397
  signal
1293
1398
  });
1294
1399
  return { ok: true };
@@ -1310,7 +1415,7 @@ function createVercel(options, environment) {
1310
1415
  });
1311
1416
  return { ok: true };
1312
1417
  },
1313
- async label(caller, { id, set, unset }, signal) {
1418
+ async label(caller, { id, set, unset, if: expected }, signal) {
1314
1419
  checkCaller("label", caller);
1315
1420
  const who = whoOf(caller);
1316
1421
  const at = locate(id);
@@ -1321,12 +1426,143 @@ function createVercel(options, environment) {
1321
1426
  body: {
1322
1427
  operationId: operationId(),
1323
1428
  ...set ? { set } : {},
1324
- ...unset ? { unset } : {}
1429
+ ...unset ? { unset } : {},
1430
+ ...expected && Object.keys(expected).length ? { if: expected } : {}
1325
1431
  },
1326
1432
  signal
1327
1433
  });
1328
1434
  return { ok: true };
1329
1435
  },
1436
+ async grants(caller, _input, signal) {
1437
+ checkCaller("grants", caller);
1438
+ const who = whoOf(caller);
1439
+ return api(who, "GET", await onBoard(who, "/grants"), signal ? { signal } : {});
1440
+ },
1441
+ async createGrant(caller, input, signal) {
1442
+ checkCaller("createGrant", caller);
1443
+ const who = whoOf(caller);
1444
+ return api(who, "POST", await onBoard(who, "/grants"), {
1445
+ body: {
1446
+ operationId: operationId(),
1447
+ ...input
1448
+ },
1449
+ ...signal ? { signal } : {}
1450
+ });
1451
+ },
1452
+ async revokeGrant(caller, { id }, signal) {
1453
+ checkCaller("revokeGrant", caller);
1454
+ const who = whoOf(caller);
1455
+ return api(who, "POST", await onBoard(who, `/grants/${encodeURIComponent(id)}/revoke`), {
1456
+ body: { operationId: operationId() },
1457
+ ...signal ? { signal } : {}
1458
+ });
1459
+ },
1460
+ async tokens(caller, _input, signal) {
1461
+ checkCaller("tokens", caller);
1462
+ const who = whoOf(caller);
1463
+ return api(who, "GET", await onBoard(who, "/tokens"), signal ? { signal } : {});
1464
+ },
1465
+ async createToken(caller, input, signal) {
1466
+ checkCaller("createToken", caller);
1467
+ const who = whoOf(caller);
1468
+ return api(who, "POST", await onBoard(who, "/tokens"), {
1469
+ body: {
1470
+ operationId: operationId(),
1471
+ ...input
1472
+ },
1473
+ ...signal ? { signal } : {}
1474
+ });
1475
+ },
1476
+ async revokeToken(caller, { id }, signal) {
1477
+ checkCaller("revokeToken", caller);
1478
+ const who = whoOf(caller);
1479
+ return api(who, "POST", await onBoard(who, `/tokens/${encodeURIComponent(id)}/revoke`), {
1480
+ body: { operationId: operationId() },
1481
+ ...signal ? { signal } : {}
1482
+ });
1483
+ },
1484
+ async schedule(caller, input, signal) {
1485
+ if (input.attachments?.length) throw notSupported("files on scheduled messages");
1486
+ const who = whoOf(caller);
1487
+ await checkOwner("schedule", caller, who);
1488
+ const access = accessOf(caller);
1489
+ const labels = input.labels && Object.keys(input.labels).length ? input.labels : void 0;
1490
+ let target;
1491
+ if (input.channel !== void 0) {
1492
+ need(access, "post", input.channel);
1493
+ if (labels) need(access, "label", input.channel);
1494
+ target = { channelId: (await channelToPost(who, input.channel)).id };
1495
+ } else {
1496
+ if (labels) throw notSupported("labels on a scheduled reply");
1497
+ const { thread, channel } = await readableMessage(who, access, input.re, signal);
1498
+ need(access, "post", channel.name);
1499
+ target = { threadId: thread.id };
1500
+ }
1501
+ const ms = (value) => Math.round(duration(value));
1502
+ const firstAt = input.at !== void 0 ? Date.parse(input.at) : Date.now() + ms(input.in ?? input.every);
1503
+ const until = input.until !== void 0 ? Date.parse(input.until) : input.for !== void 0 ? firstAt + ms(input.for) : void 0;
1504
+ const { schedule } = await api(who, "POST", await onBoard(who, "/schedules"), {
1505
+ body: {
1506
+ operationId: operationId(),
1507
+ ...target,
1508
+ text: input.text,
1509
+ ...labels ? { labels } : {},
1510
+ firstAt,
1511
+ ...input.every !== void 0 ? { every: ms(input.every) } : {},
1512
+ ...until !== void 0 ? { until } : {},
1513
+ ...input.times !== void 0 ? { times: input.times } : {}
1514
+ },
1515
+ signal
1516
+ });
1517
+ return { schedule: await scheduleView(who, schedule) };
1518
+ },
1519
+ async schedules(caller, query, signal) {
1520
+ const who = whoOf(caller);
1521
+ const access = accessOf(caller);
1522
+ const filter = { state: query.all ? void 0 : "pending" };
1523
+ if (query.re !== void 0) {
1524
+ const found = await readableMessage(who, access, query.re, signal).catch((error) => {
1525
+ if (error instanceof HttpError && error.status === 404) return null;
1526
+ throw error;
1527
+ });
1528
+ if (!found) return { schedules: [] };
1529
+ filter.threadId = found.thread.id;
1530
+ }
1531
+ if (query.channel !== void 0) {
1532
+ const channel = await channelNamed(who, query.channel);
1533
+ if (!channel) return { schedules: [] };
1534
+ filter.channelId = channel.id;
1535
+ }
1536
+ const me = query.by === "me" ? await actorOf(who) : void 0;
1537
+ const by = me ?? query.by;
1538
+ if (by?.startsWith("vercel:")) filter.createdBy = by;
1539
+ const created = (actor) => by === void 0 || (me ? actor.id === me : [
1540
+ actor.id,
1541
+ principalId(actor.principal),
1542
+ actor.profile?.name
1543
+ ].includes(by));
1544
+ const listed = await all(who, await onBoard(who, "/schedules"), "schedules", filter, signal);
1545
+ return { schedules: (await Promise.all(listed.filter((schedule) => created(schedule.createdBy)).map((schedule) => scheduleView(who, schedule)))).filter((schedule) => access === null || access.readable(schedule.channel)) };
1546
+ },
1547
+ async cancelSchedule(caller, { id }, signal) {
1548
+ const who = whoOf(caller);
1549
+ await checkOwner("cancelSchedule", caller, who);
1550
+ const access = accessOf(caller);
1551
+ if (access) {
1552
+ const found = (await all(who, await onBoard(who, "/schedules"), "schedules", {}, signal)).find((schedule) => schedule.id === id);
1553
+ const channel = found && await channelById(who, found.channelId);
1554
+ if (!channel || !access.readable(channel.name)) throw notFound(`no schedule: ${id}`, "schedule_not_found");
1555
+ need(access, "post", channel.name);
1556
+ }
1557
+ const { schedule, canceled } = await api(who, "POST", await onBoard(who, `/schedules/${encodeURIComponent(id)}/cancel`), {
1558
+ body: { operationId: operationId() },
1559
+ signal
1560
+ });
1561
+ return {
1562
+ schedule: await scheduleView(who, schedule),
1563
+ canceled
1564
+ };
1565
+ },
1330
1566
  async events(caller, query, signal) {
1331
1567
  checkCaller("events", caller);
1332
1568
  const who = whoOf(caller);
@@ -1399,6 +1635,7 @@ function createVercel(options, environment) {
1399
1635
  }])),
1400
1636
  routes: runner.routes,
1401
1637
  keeps: false,
1638
+ sendsSchedulesHere: false,
1402
1639
  bind(next) {
1403
1640
  if (binding) throw new Error("vercel: this backend already serves a board: create one vercel() per board()");
1404
1641
  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,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-BaSEUdwg.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-CmY9yZ9m.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-BaSEUdwg.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-Ba8JDtqW.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-DLUzfibJ.mjs";
2
+ import "./errors-2vGs49Db.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-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 };