@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.
- package/README.md +8 -17
- package/dist/{a2-HUbQJ0RD.mjs → a2-DngWY2Eb.mjs} +741 -415
- package/dist/access-CY5xVMIK.mjs +242 -0
- package/dist/{access-CV96SYgr.d.mts → access-DPcrKIH_.d.mts} +6 -1
- package/dist/access.d.mts +4 -4
- package/dist/access.mjs +5 -4
- package/dist/{actors-DcJhFbwJ.mjs → actors-Bm-ojWWT.mjs} +2 -2
- package/dist/{auth-JO22iIwr.mjs → auth-CyO2aw5n.mjs} +126 -236
- package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
- package/dist/{backend-CPC_vyOo.d.mts → backend-Bob-5CM7.d.mts} +93 -42
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +38 -98
- package/dist/backend-a2/contract.mjs +1 -1
- package/dist/backend-a2/scheduler-local.d.mts +1 -1
- package/dist/backend-a2/scheduler-local.mjs +1 -1
- package/dist/backend-a2/scheduler-qstash.d.mts +3 -3
- package/dist/backend-a2/scheduler-qstash.mjs +8 -8
- package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
- package/dist/backend-a2/scheduler-vercel.mjs +6 -6
- package/dist/backend-a2/store-memory.d.mts +2 -2
- package/dist/backend-a2/store-memory.mjs +3 -3
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +34 -15
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +6 -6
- package/dist/backend-a2/store-redis.d.mts +2 -2
- package/dist/backend-a2/store-redis.mjs +5 -5
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +3 -3
- package/dist/backend-a2.d.mts +143 -242
- package/dist/backend-a2.mjs +2 -2
- package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-BRrRbjRx.d.mts} +59 -45
- package/dist/{backend-http-CzIJMm58.mjs → backend-http-CvUKOn-m.mjs} +11 -10
- package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-D8mUzuKQ.mjs} +2 -2
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-vercel-DQc_TaHp.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
- package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-BaSEUdwg.d.mts} +62 -47
- package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-CmY9yZ9m.mjs} +279 -42
- package/dist/backend-vercel.d.mts +1 -1
- package/dist/backend-vercel.mjs +1 -1
- package/dist/backend-vercel.node.d.mts +1 -1
- package/dist/backend-vercel.node.mjs +1 -1
- package/dist/{blob-DmiQjrYy.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
- package/dist/{blob-DdbpJqN1.mjs → blob-WbB2DJKn.mjs} +30 -11
- package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-BChYrrA7.d.mts} +32 -28
- package/dist/{chat-provider-COtsU5jB.mjs → chat-provider-DkFp2hrh.mjs} +105 -60
- package/dist/cli-commands.d.mts +1 -1
- package/dist/cli-commands.mjs +1 -1
- package/dist/cli-embedded.d.mts +2 -2
- package/dist/cli-embedded.mjs +1 -1
- package/dist/cli.mjs +4 -4
- package/dist/{client-BqAjk0Q4.d.mts → client-COfYrgfV.d.mts} +240 -227
- package/dist/{commands-D6gvrOh3.d.mts → commands-DK4W3HCJ.d.mts} +47 -10
- package/dist/{commands-C3R1AWgm.mjs → commands-L1JSGz2Z.mjs} +212 -54
- package/dist/conformance-DK43HiHf.mjs +1000 -0
- package/dist/conformance.d.mts +8 -2
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-DQxXk7dX.mjs → engine-DurYIRt8.mjs} +29 -210
- package/dist/{errors-DDktVeUa.d.mts → errors-2vGs49Db.d.mts} +1 -1
- package/dist/{client-D0Px7KzF.mjs → errors-CJXhhzvT.mjs} +132 -27
- package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
- package/dist/{exports-BG-0Wyn-.mjs → exports-CWIdN0Ub.mjs} +195 -103
- package/dist/exports-FBOQWT7O.d.mts +5 -0
- package/dist/{folds-BDMSxBUf.mjs → folds-Cu9Q9zyQ.mjs} +3 -22
- package/dist/{folds-ZkHM4M-v.d.mts → folds-DLUzfibJ.d.mts} +15 -40
- package/dist/{index-D3VvNfh_.d.mts → index-Ho-ayyQL.d.mts} +2 -2
- package/dist/index.d.mts +7 -7
- package/dist/{index.default-rB8sJ9RX.mjs → index.default-BUcwDaAe.mjs} +1 -1
- package/dist/index.mjs +5 -6
- package/dist/index.node.d.mts +7 -7
- package/dist/index.node.mjs +3 -4
- package/dist/inspect/app.js +62 -61
- package/dist/inspect/engine.js +3 -3
- package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
- package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
- package/dist/{main-CVBqe3VO.mjs → main-Dz-8komG.mjs} +251 -127
- package/dist/{middleware-CXjEeef4.mjs → middleware-BavYSDUa.mjs} +1 -1
- package/dist/{node-Zd_C-gxP.mjs → node-BgeGcwX0.mjs} +7 -3
- package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
- package/dist/{project-CBXlLftU.mjs → project-CQ4VUibA.mjs} +1 -1
- package/dist/{provider-DmSTp9lI.d.mts → provider-gynDUoyW.d.mts} +13 -43
- package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-DHf85ixI.mjs} +40 -42
- package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
- package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
- package/dist/{reducers-DJzVpXwM.mjs → reducers-DcENwtwZ.mjs} +74 -77
- package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
- package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
- package/dist/{serve-G9RuaDSr.mjs → serve-CTmow-YH.mjs} +7 -7
- package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
- package/dist/{server-BixV_byP.mjs → server-DG8D6VV0.mjs} +4 -4
- package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
- package/dist/{serving-B75Y4BF-.mjs → serving-DBFRo6U_.mjs} +1 -1
- package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
- package/dist/{sql-D4Z_efJX.mjs → sql-CT5xVuGr.mjs} +2 -8
- package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
- package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
- package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
- package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
- package/dist/store-memory-4IBl0Ztu.mjs +2 -0
- package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
- package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
- package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
- package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
- package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
- package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
- package/dist/{sync-kapso.d.mts → sync-chat-sdk.d.mts} +33 -37
- package/dist/sync-chat-sdk.mjs +44 -0
- package/dist/sync-github.d.mts +5 -18
- package/dist/sync-github.mjs +4 -70
- package/dist/sync-linear.d.mts +3 -3
- package/dist/sync-linear.mjs +6 -5
- package/dist/sync-slack.d.mts +7 -7
- package/dist/sync-slack.mjs +33 -54
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +2 -4
- package/dist/{tokens-CC3izSNr.mjs → tokens-Dnyd_y1n.mjs} +15 -6
- package/dist/{types-No0WdhE5.d.mts → types-CmrFjpml.d.mts} +2 -4
- package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-BjwlnGD9.mjs} +124 -115
- package/docs/01-board/01-mounting.md +2 -2
- package/docs/01-board/03-identity.md +5 -6
- package/docs/01-board/04-messages.md +96 -2
- package/docs/01-board/05-reading.md +2 -1
- package/docs/01-board/06-watchers.md +8 -6
- package/docs/01-board/08-middleware.md +6 -3
- package/docs/01-board/09-access.md +145 -14
- package/docs/02-sync/01-overview.md +19 -26
- package/docs/02-sync/02-functions.md +8 -9
- package/docs/02-sync/03-slack.md +5 -5
- package/docs/02-sync/04-github.md +2 -2
- package/docs/02-sync/06-providers.md +91 -62
- package/docs/03-guides/01-coding-agents.md +1 -1
- package/docs/03-guides/03-ci.md +7 -4
- package/docs/04-reference/01-cli.md +109 -51
- package/docs/04-reference/02-board.md +76 -28
- package/docs/04-reference/03-http.md +76 -38
- package/docs/04-reference/04-log.md +8 -11
- package/docs/04-reference/05-conformance.md +47 -7
- package/docs/index.md +2 -2
- package/package.json +7 -10
- package/sql/postgres/board.sql +1 -1
- package/sql/postgres/schema.sql +1 -1
- package/dist/conformance-DlnfuzEB.mjs +0 -556
- package/dist/errors-CusN4x2l.mjs +0 -88
- package/dist/events-qf5XG5VQ.mjs +0 -75
- package/dist/exports-mhAYiU4i.d.mts +0 -5
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
- package/dist/store-memory-ZLiZbH3R.mjs +0 -2
- package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
- package/dist/sync-kapso.mjs +0 -89
- package/dist/sync.chat.d.mts +0 -2
- package/dist/sync.chat.mjs +0 -2
- package/dist/users-yryHKFMo.mjs +0 -42
- 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 {
|
|
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
|
|
6
|
-
import {
|
|
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,
|
|
94
|
-
* changes work; what the API keeps none of yet (
|
|
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
|
-
/**
|
|
196
|
-
async function
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
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: {
|
|
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-
|
|
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
|
package/dist/backend-vercel.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as createVercel } from "./backend-vercel-
|
|
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-
|
|
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-
|
|
1
|
+
import { n as handTeam, r as vercel, t as credentialSource } from "./backend-vercel-Ba8JDtqW.mjs";
|
|
2
2
|
export { credentialSource, handTeam, vercel };
|
|
@@ -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-
|
|
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
|
|
151
|
-
* the same attachment. It never reads the record before writing it: a store with a
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 };
|