@vercel/board 0.28.0 → 0.29.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/dist/{a2-B0_VlbtC.mjs → a2-HUbQJ0RD.mjs} +16 -192
- package/dist/access.d.mts +1 -1
- package/dist/access.mjs +3 -3
- package/dist/{actors-BLXVp4oZ.mjs → actors-DcJhFbwJ.mjs} +2 -2
- package/dist/{auth-lTkOgvcE.mjs → auth-JO22iIwr.mjs} +1 -1
- package/dist/{backend-ZKYTe3xa.d.mts → backend-CPC_vyOo.d.mts} +5 -5
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +1 -1
- package/dist/backend-a2/contract.mjs +3 -3
- package/dist/backend-a2/store-memory.d.mts +1 -1
- package/dist/backend-a2/store-memory.mjs +2 -2
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +1 -1
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +1 -1
- package/dist/backend-a2/store-redis.d.mts +1 -1
- package/dist/backend-a2/store-redis.mjs +1 -1
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +2 -2
- package/dist/backend-a2.d.mts +8 -5
- package/dist/backend-a2.mjs +2 -1
- package/dist/{backend-http-BvNFrvcY.mjs → backend-http-Bmp0IgD3.mjs} +1 -1
- package/dist/{backend-http-E7YU0qhn.d.mts → backend-http-CqfKhrnn.d.mts} +5 -3
- package/dist/{backend-http-CfLVYqmj.mjs → backend-http-CzIJMm58.mjs} +3 -3
- 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-CIRObA0t.d.mts → backend-vercel-4tvcPKqX.d.mts} +7 -3
- package/dist/{backend-vercel-D87pIp2k.mjs → backend-vercel-BNauAs7w.mjs} +68 -10
- package/dist/{backend-vercel-DTrDiSk5.mjs → backend-vercel-DQc_TaHp.mjs} +1 -1
- 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-DdbpJqN1.mjs +182 -0
- package/dist/{blob-B-9AvYis.d.mts → blob-DmiQjrYy.d.mts} +2 -2
- package/dist/{chat-provider-DsNEBQiA.d.mts → chat-provider-BWHbChcQ.d.mts} +5 -3
- package/dist/{chat-provider-DnHibqnM.mjs → chat-provider-COtsU5jB.mjs} +2 -2
- 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-xLxeDz0f.d.mts → client-BqAjk0Q4.d.mts} +8 -1
- package/dist/{commands-BFookFtn.mjs → commands-C3R1AWgm.mjs} +4 -1
- package/dist/conformance.d.mts +1 -1
- package/dist/{engine-Bfe6pcXb.mjs → engine-DQxXk7dX.mjs} +20 -12
- package/dist/{errors-CB4tnZGz.d.mts → errors-DDktVeUa.d.mts} +1 -1
- package/dist/{events-CewD4kw7.mjs → events-qf5XG5VQ.mjs} +2 -2
- package/dist/{exports-CCzw2Vdw.mjs → exports-BG-0Wyn-.mjs} +9 -9
- package/dist/exports-mhAYiU4i.d.mts +5 -0
- package/dist/{external-BV_cUncs.mjs → external-D3p5T57R.mjs} +4 -2
- package/dist/{folds-Dyjput8M.mjs → folds-BDMSxBUf.mjs} +2 -2
- package/dist/{folds-jS9wlh9_.d.mts → folds-ZkHM4M-v.d.mts} +2 -0
- package/dist/{index-C05it0mC.d.mts → index-D3VvNfh_.d.mts} +2 -2
- package/dist/index.d.mts +5 -5
- package/dist/{index.default-DmKxH_E6.mjs → index.default-rB8sJ9RX.mjs} +1 -1
- package/dist/index.mjs +4 -4
- package/dist/index.node.d.mts +5 -5
- package/dist/index.node.mjs +2 -2
- package/dist/inspect/app.js +3 -3
- package/dist/inspect/engine.js +5 -5
- package/dist/{main-eci7w-aP.mjs → main-CVBqe3VO.mjs} +11 -8
- package/dist/{middleware-ChKM1zuu.mjs → middleware-CXjEeef4.mjs} +1 -1
- package/dist/{project-DMDST9kl.mjs → project-CBXlLftU.mjs} +1 -1
- package/dist/{provider-poIkXapd.mjs → provider-BaVmi4Hi.mjs} +3 -1
- package/dist/{provider-Cdg1qadh.d.mts → provider-DmSTp9lI.d.mts} +9 -2
- package/dist/{redis-tables-DN9PfIDi.mjs → redis-tables-BJQVUspu.mjs} +2 -2
- package/dist/{reducers-BcmSs4qf.mjs → reducers-DJzVpXwM.mjs} +3 -3
- package/dist/{schemas-Be08nyfW.mjs → schemas-DBmUO6pr.mjs} +2 -1
- package/dist/{serve-B09an9DQ.mjs → serve-G9RuaDSr.mjs} +4 -3
- package/dist/{server-DTX0prGr.mjs → server-BixV_byP.mjs} +4 -4
- package/dist/{serving-CvOWEym2.mjs → serving-B75Y4BF-.mjs} +1 -1
- package/dist/{sql-U9WTxrwJ.mjs → sql-D4Z_efJX.mjs} +2 -2
- package/dist/{sqlite-db-Ds4majtT.mjs → sqlite-db-Bz2JkfCU.mjs} +1 -1
- package/dist/sync-github.d.mts +4 -2
- package/dist/sync-github.mjs +4 -14
- package/dist/sync-kapso.d.mts +3 -2
- package/dist/sync-kapso.mjs +3 -3
- package/dist/sync-linear.d.mts +12 -2
- package/dist/sync-linear.mjs +189 -9
- package/dist/sync-slack.d.mts +41 -5
- package/dist/sync-slack.mjs +70 -11
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.chat.mjs +1 -1
- package/dist/sync.d.mts +2 -2
- package/dist/sync.mjs +3 -3
- package/dist/tags-BpFo6q0_.mjs +14 -0
- package/dist/{targets-C-wdR5wP.mjs → targets-Cgjs29T0.mjs} +1 -1
- package/dist/{tokens-BuCU-61R.mjs → tokens-CC3izSNr.mjs} +1 -1
- package/dist/{types-aCAf6lGU.d.mts → types-No0WdhE5.d.mts} +1 -1
- package/dist/{watchers-hash-7DJU9b-n.mjs → watchers-hash-BDhLVkV5.mjs} +7 -5
- package/docs/02-sync/03-slack.md +6 -2
- package/docs/02-sync/07-linear.md +28 -6
- package/docs/04-reference/01-cli.md +4 -2
- package/package.json +1 -1
- package/dist/exports-D22M68rk.d.mts +0 -5
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { r as IMPORT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
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
4
|
import { o as sha256, r as derivedId } from "./wait-until-Dw4XrIDQ.mjs";
|
|
4
5
|
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-
|
|
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
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}$/;
|
|
@@ -22,9 +24,16 @@ const UNKEPT = {
|
|
|
22
24
|
schedule: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
|
|
23
25
|
schedules: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
|
|
24
26
|
cancelSchedule: ["no_scheduler", "Vercel's boards don't schedule messages yet"],
|
|
25
|
-
|
|
26
|
-
|
|
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"]
|
|
27
29
|
};
|
|
30
|
+
/** A file as the board names it. */
|
|
31
|
+
const fileOf = ({ id, name, type, size }) => ({
|
|
32
|
+
id,
|
|
33
|
+
name,
|
|
34
|
+
type,
|
|
35
|
+
size
|
|
36
|
+
});
|
|
28
37
|
const notSupported = (what) => new HttpError(501, "not_supported", `Vercel's boards don't support ${what} yet`);
|
|
29
38
|
/** What a caller board() verified may do. Null for this deployment's own code, which may do what its credentials may. */
|
|
30
39
|
const accessOf = (caller) => "identity" in caller ? caller.identity.principal.access : null;
|
|
@@ -89,6 +98,7 @@ function createVercel(options, environment) {
|
|
|
89
98
|
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
99
|
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
100
|
const doFetch = options.fetch ?? environment.fetch();
|
|
101
|
+
const filesFetch = options.files ?? fetch;
|
|
92
102
|
let binding = null;
|
|
93
103
|
/** This board's own code's names, which a caller here never takes as its profile. */
|
|
94
104
|
const own = /* @__PURE__ */ new Set();
|
|
@@ -318,7 +328,7 @@ function createVercel(options, environment) {
|
|
|
318
328
|
editedAt: message.editedAt === void 0 ? null : iso(message.editedAt),
|
|
319
329
|
deleted: message.deletedAt !== void 0,
|
|
320
330
|
undelivered: {},
|
|
321
|
-
attachments: [],
|
|
331
|
+
attachments: message.attachments ?? [],
|
|
322
332
|
links: [],
|
|
323
333
|
version: message.version,
|
|
324
334
|
author: await nameOf(who, message.createdBy)
|
|
@@ -560,7 +570,7 @@ function createVercel(options, environment) {
|
|
|
560
570
|
by,
|
|
561
571
|
origin: null,
|
|
562
572
|
from: null,
|
|
563
|
-
attachments:
|
|
573
|
+
attachments: message.attachments,
|
|
564
574
|
links: []
|
|
565
575
|
};
|
|
566
576
|
break;
|
|
@@ -1144,12 +1154,58 @@ function createVercel(options, environment) {
|
|
|
1144
1154
|
})
|
|
1145
1155
|
};
|
|
1146
1156
|
},
|
|
1157
|
+
async upload(caller, { data, name, type }) {
|
|
1158
|
+
checkCaller("upload", caller);
|
|
1159
|
+
if (data.byteLength === 0) throw new HttpError(400, "bad_request", "the file is empty");
|
|
1160
|
+
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
|
+
const bytes = new Uint8Array(data);
|
|
1163
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
1164
|
+
const started = await api(who, "POST", await onBoard(who, "/attachments"), { body: {
|
|
1165
|
+
operationId: operationId(),
|
|
1166
|
+
name: fileName(name),
|
|
1167
|
+
type: mediaType(type),
|
|
1168
|
+
size: data.byteLength,
|
|
1169
|
+
sha256: btoa(String.fromCharCode(...digest))
|
|
1170
|
+
} });
|
|
1171
|
+
const put = await filesFetch(started.upload.url, {
|
|
1172
|
+
method: started.upload.method,
|
|
1173
|
+
headers: started.upload.headers,
|
|
1174
|
+
body: bytes,
|
|
1175
|
+
redirect: "error"
|
|
1176
|
+
});
|
|
1177
|
+
if (!put.ok) throw new HttpError(502, "upload_failed", `the upload to Vercel's storage answered ${put.status}`);
|
|
1178
|
+
const { attachment } = await api(who, "POST", await onBoard(who, `/attachments/${encodeURIComponent(started.attachment.id)}/complete`));
|
|
1179
|
+
return { attachment: fileOf(attachment) };
|
|
1180
|
+
},
|
|
1181
|
+
async attachment(caller, { id }, signal) {
|
|
1182
|
+
const who = whoOf(caller);
|
|
1183
|
+
const access = accessOf(caller);
|
|
1184
|
+
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");
|
|
1187
|
+
const { message } = await readableMessage(who, access, found.attachment.messageId, signal).catch((error) => {
|
|
1188
|
+
if (error instanceof HttpError && error.status === 404) throw notFound(`no attachment: ${id}`, "attachment_not_found");
|
|
1189
|
+
throw error;
|
|
1190
|
+
});
|
|
1191
|
+
if (message.deletedAt !== void 0) throw notFound(`no attachment: ${id}`, "attachment_not_found");
|
|
1192
|
+
}
|
|
1193
|
+
const response = await filesFetch(found.url, {
|
|
1194
|
+
redirect: "error",
|
|
1195
|
+
...signal ? { signal } : {}
|
|
1196
|
+
});
|
|
1197
|
+
if (!response.ok || !response.body) throw new HttpError(502, "download_failed", `Vercel's storage answered ${response.status}`);
|
|
1198
|
+
return {
|
|
1199
|
+
attachment: fileOf(found.attachment),
|
|
1200
|
+
data: response.body
|
|
1201
|
+
};
|
|
1202
|
+
},
|
|
1147
1203
|
async post(caller, input, signal) {
|
|
1148
1204
|
checkCaller("post", caller);
|
|
1149
|
-
if (input.attachments?.length) throw new HttpError(501, "no_files", "Vercel's boards keep no files yet");
|
|
1150
1205
|
if (input.sync?.length) throw notSupported("sync");
|
|
1151
1206
|
const who = whoOf(caller);
|
|
1152
1207
|
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");
|
|
1153
1209
|
const labeled = Object.keys(input.labels ?? {}).length > 0;
|
|
1154
1210
|
const clientId = input.id !== void 0 ? await postId(caller, input.id) : void 0;
|
|
1155
1211
|
let made;
|
|
@@ -1163,7 +1219,8 @@ function createVercel(options, environment) {
|
|
|
1163
1219
|
channelId: channel.id,
|
|
1164
1220
|
text: input.text,
|
|
1165
1221
|
...input.labels && Object.keys(input.labels).length ? { labels: input.labels } : {},
|
|
1166
|
-
...clientId ? { clientId } : {}
|
|
1222
|
+
...clientId ? { clientId } : {},
|
|
1223
|
+
...input.attachments?.length ? { attachments: input.attachments } : {}
|
|
1167
1224
|
},
|
|
1168
1225
|
signal
|
|
1169
1226
|
});
|
|
@@ -1176,7 +1233,8 @@ function createVercel(options, environment) {
|
|
|
1176
1233
|
operationId: operationId(),
|
|
1177
1234
|
text: input.text,
|
|
1178
1235
|
...to.sequence > 1 ? { replyTo: to.sequence } : {},
|
|
1179
|
-
...clientId ? { clientId } : {}
|
|
1236
|
+
...clientId ? { clientId } : {},
|
|
1237
|
+
...input.attachments?.length ? { attachments: input.attachments } : {}
|
|
1180
1238
|
},
|
|
1181
1239
|
signal
|
|
1182
1240
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as HttpError } from "./errors-CusN4x2l.mjs";
|
|
2
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-
|
|
3
|
+
import { t as createVercel } from "./backend-vercel-BNauAs7w.mjs";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { decodeJwt } from "jose";
|
|
6
6
|
import { getVercelOidcToken } from "@vercel/oidc";
|
|
@@ -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-4tvcPKqX.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-BNauAs7w.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-4tvcPKqX.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-DQc_TaHp.mjs";
|
|
2
2
|
export { credentialSource, handTeam, vercel };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
|
+
import { i as Attachment } from "./schemas-DBmUO6pr.mjs";
|
|
3
|
+
import { r as badRequest, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
//#region src/server/blob.ts
|
|
8
|
+
/** Files in memory, for tests: forgets everything when the process ends. */
|
|
9
|
+
function memoryBlob() {
|
|
10
|
+
const files = /* @__PURE__ */ new Map();
|
|
11
|
+
return {
|
|
12
|
+
put: async (key, data, contentType) => void files.set(key, {
|
|
13
|
+
data: data.slice(),
|
|
14
|
+
contentType
|
|
15
|
+
}),
|
|
16
|
+
get: async (key) => files.get(key) ?? null
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Files on disk, for development and `vb serve`: under `dir`, `.a2/attachments` by default. */
|
|
20
|
+
function fsBlob(dir = ".a2/attachments") {
|
|
21
|
+
const path = (key) => join(dir, ...key.split("/"));
|
|
22
|
+
return {
|
|
23
|
+
async put(key, data, contentType) {
|
|
24
|
+
await mkdir(dirname(path(key)), { recursive: true });
|
|
25
|
+
await writeFile(path(key), data);
|
|
26
|
+
await writeFile(`${path(key)}.type`, contentType);
|
|
27
|
+
},
|
|
28
|
+
async get(key) {
|
|
29
|
+
try {
|
|
30
|
+
const [data, contentType] = await Promise.all([readFile(path(key)), readFile(`${path(key)}.type`, "utf8")]);
|
|
31
|
+
return {
|
|
32
|
+
data: new Uint8Array(data),
|
|
33
|
+
contentType
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const dataKey = (id) => `board/attachments/${id}`;
|
|
42
|
+
/** A direct upload the board signed, until it's finished: what it's called, and who may finish it. */
|
|
43
|
+
const ticketKey = (id) => `board/uploads/${id}.json`;
|
|
44
|
+
const metaKey = (id) => `board/attachments/${id}.json`;
|
|
45
|
+
/** `shot.png`, as the board stores a name: no paths, no control characters. */
|
|
46
|
+
function fileName(name) {
|
|
47
|
+
return (name ?? "").split(/[\\/]/).pop().replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 255) || "file";
|
|
48
|
+
}
|
|
49
|
+
/** `image/png`, or `application/octet-stream` when the type isn't one. */
|
|
50
|
+
function mediaType(type) {
|
|
51
|
+
const value = (type ?? "").split(";")[0].trim().toLowerCase();
|
|
52
|
+
return /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/.test(value) && value.length <= 255 ? value : "application/octet-stream";
|
|
53
|
+
}
|
|
54
|
+
/** A board without a blob store keeps no files: every file route says so. */
|
|
55
|
+
const noBlob = () => new HttpError(501, "no_files", "this board keeps no files: pass `blob` to `a2()`");
|
|
56
|
+
/** 128 random bits, base64url, drawn again when they'd start with `-`, which a command line reads as a flag. */
|
|
57
|
+
function attachmentId() {
|
|
58
|
+
for (;;) {
|
|
59
|
+
const id = randomBytes(16).toString("base64url");
|
|
60
|
+
if (!id.startsWith("-")) return id;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 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.
|
|
66
|
+
* `max` is the most it can be: `ATTACHMENT_BYTES_MAX` unless the board's own sync imports it.
|
|
67
|
+
*/
|
|
68
|
+
async function writeUpload(blob, input, max = ATTACHMENT_BYTES_MAX) {
|
|
69
|
+
if (!blob) throw noBlob();
|
|
70
|
+
if (input.data.byteLength === 0) throw badRequest("the file is empty");
|
|
71
|
+
if (input.data.byteLength > max) throw new HttpError(413, "too_large", `attachments are at most ${max / 1024 / 1024} MB`);
|
|
72
|
+
const attachment = {
|
|
73
|
+
id: attachmentId(),
|
|
74
|
+
name: fileName(input.name),
|
|
75
|
+
type: mediaType(input.type),
|
|
76
|
+
size: input.data.byteLength
|
|
77
|
+
};
|
|
78
|
+
await blob.put(dataKey(attachment.id), input.data, attachment.type);
|
|
79
|
+
const meta = input.uploadedBy === void 0 ? attachment : {
|
|
80
|
+
...attachment,
|
|
81
|
+
uploadedBy: input.uploadedBy
|
|
82
|
+
};
|
|
83
|
+
await blob.put(metaKey(attachment.id), new TextEncoder().encode(JSON.stringify(meta)), "application/json");
|
|
84
|
+
return attachment;
|
|
85
|
+
}
|
|
86
|
+
async function bytes(data) {
|
|
87
|
+
return data instanceof Uint8Array ? data : new Uint8Array(await new Response(data).arrayBuffer());
|
|
88
|
+
}
|
|
89
|
+
const attachmentNotFound = (id) => notFound(`no attachment: ${id}`, "attachment_not_found");
|
|
90
|
+
/**
|
|
91
|
+
* An upload's metadata, as the board recorded it, never what a request says,
|
|
92
|
+
* and who uploaded it: null for files the board imported itself, or stored
|
|
93
|
+
* before it recorded that.
|
|
94
|
+
*/
|
|
95
|
+
async function readUpload(blob, id) {
|
|
96
|
+
if (!blob) throw noBlob();
|
|
97
|
+
const found = Attachment.shape.id.safeParse(id).success ? await blob.get(metaKey(id)) : null;
|
|
98
|
+
if (!found) throw attachmentNotFound(id);
|
|
99
|
+
const meta = JSON.parse(new TextDecoder().decode(await bytes(found.data)));
|
|
100
|
+
return {
|
|
101
|
+
attachment: Attachment.parse(meta),
|
|
102
|
+
uploadedBy: typeof meta.uploadedBy === "string" ? meta.uploadedBy : null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** An attachment's bytes as the store keeps them, a stream or the bytes, for board() to serve. */
|
|
106
|
+
async function attachmentBody(blob, id) {
|
|
107
|
+
if (!blob) throw noBlob();
|
|
108
|
+
const found = await blob.get(dataKey(id));
|
|
109
|
+
if (!found) throw attachmentNotFound(id);
|
|
110
|
+
return found.data;
|
|
111
|
+
}
|
|
112
|
+
/** How long a direct upload's URL takes bytes: long enough for 100 MB on a slow link. */
|
|
113
|
+
const DIRECT_UPLOAD_TTL_MS = 18e5;
|
|
114
|
+
const noDirectUploads = () => new HttpError(501, "no_direct_uploads", `this board's blob store can't take direct uploads, so files are at most ${ATTACHMENT_BYTES_MAX / 1024 / 1024} MB`);
|
|
115
|
+
/**
|
|
116
|
+
* Starts a direct upload: a fresh ID, and a URL in the store to `PUT` its bytes to. The board
|
|
117
|
+
* keeps what the request said the file is until `finishUpload` checks the store has it.
|
|
118
|
+
*/
|
|
119
|
+
async function startUpload(blob, input) {
|
|
120
|
+
if (!blob) throw noBlob();
|
|
121
|
+
if (!blob.uploadUrl || !blob.head) throw noDirectUploads();
|
|
122
|
+
if (input.size > 104857600) throw new HttpError(413, "too_large", `direct uploads are at most ${DIRECT_UPLOAD_BYTES_MAX / 1024 / 1024} MB`);
|
|
123
|
+
const expires = new Date(Date.now() + DIRECT_UPLOAD_TTL_MS);
|
|
124
|
+
const ticket = {
|
|
125
|
+
id: attachmentId(),
|
|
126
|
+
name: fileName(input.name),
|
|
127
|
+
type: mediaType(input.type),
|
|
128
|
+
size: input.size,
|
|
129
|
+
uploadedBy: input.uploadedBy,
|
|
130
|
+
expiresAt: expires.toISOString()
|
|
131
|
+
};
|
|
132
|
+
await blob.put(ticketKey(ticket.id), new TextEncoder().encode(JSON.stringify(ticket)), "application/json");
|
|
133
|
+
const { url, headers } = await blob.uploadUrl(dataKey(ticket.id), {
|
|
134
|
+
contentType: ticket.type,
|
|
135
|
+
maxBytes: ticket.size,
|
|
136
|
+
expiresAt: expires
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
id: ticket.id,
|
|
140
|
+
url,
|
|
141
|
+
headers: {
|
|
142
|
+
...headers,
|
|
143
|
+
"content-type": ticket.type
|
|
144
|
+
},
|
|
145
|
+
expiresAt: ticket.expiresAt
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 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.
|
|
153
|
+
*/
|
|
154
|
+
async function finishUpload(blob, id, principal) {
|
|
155
|
+
if (!blob) throw noBlob();
|
|
156
|
+
const found = Attachment.shape.id.safeParse(id).success ? await blob.get(ticketKey(id)) : null;
|
|
157
|
+
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");
|
|
159
|
+
if (!blob.head) throw noDirectUploads();
|
|
160
|
+
const stored = await blob.head(dataKey(id));
|
|
161
|
+
if (!stored) throw new HttpError(409, "not_uploaded", `nothing's been uploaded for ${id} yet: PUT the file to its URL first`);
|
|
162
|
+
if (stored.size !== ticket.size) throw badRequest(`the store has ${stored.size} bytes for ${id}, but its upload said ${ticket.size}`, "size_mismatch");
|
|
163
|
+
const attachment = {
|
|
164
|
+
id,
|
|
165
|
+
name: ticket.name,
|
|
166
|
+
type: ticket.type,
|
|
167
|
+
size: stored.size
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
await blob.put(metaKey(id), new TextEncoder().encode(JSON.stringify({
|
|
171
|
+
...attachment,
|
|
172
|
+
uploadedBy: principal
|
|
173
|
+
})), "application/json");
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const done = await blob.get(metaKey(id)).catch(() => null);
|
|
176
|
+
if (!done) throw error;
|
|
177
|
+
return Attachment.parse(JSON.parse(new TextDecoder().decode(await bytes(done.data))));
|
|
178
|
+
}
|
|
179
|
+
return attachment;
|
|
180
|
+
}
|
|
181
|
+
//#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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { d as ExternalFile, f as ExternalLink, l as ExternalAuthor, t as Actor } from "./folds-
|
|
2
|
-
import { dt as SourceJoin, mt as SourceRead } from "./client-
|
|
3
|
-
import { M as SyncRoot, O as ProviderEvent, a as ProviderOptions, c as ReceivedEvent, d as ResolvedTarget, i as ProviderFile, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, t as FetchedLink, y as SyncFn } from "./provider-
|
|
1
|
+
import { d as ExternalFile, f as ExternalLink, l as ExternalAuthor, t as Actor } from "./folds-ZkHM4M-v.mjs";
|
|
2
|
+
import { dt as SourceJoin, mt as SourceRead } from "./client-BqAjk0Q4.mjs";
|
|
3
|
+
import { M as SyncRoot, O as ProviderEvent, a as ProviderOptions, c as ReceivedEvent, d as ResolvedTarget, i as ProviderFile, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, t as FetchedLink, y as SyncFn } from "./provider-DmSTp9lI.mjs";
|
|
4
4
|
import { Adapter, Author, FileUpload, Message, ReactionEvent } from "chat";
|
|
5
5
|
//#region src/sync/types.d.ts
|
|
6
6
|
declare function externalAuthor(platform: string, author: Author): ExternalAuthor;
|
|
@@ -45,6 +45,7 @@ type ChatTransport = Omit<ProviderOptions, 'uploadsFiles'> & {
|
|
|
45
45
|
/** A channel or thread, read on demand without syncing it. */
|
|
46
46
|
read?(target: string, options: {
|
|
47
47
|
limit: number;
|
|
48
|
+
after: string | null;
|
|
48
49
|
}): Promise<SourceRead>;
|
|
49
50
|
/** Adds the app to a channel so `read` can read it. */
|
|
50
51
|
join?(target: string): Promise<SourceJoin>;
|
|
@@ -126,6 +127,7 @@ declare abstract class ChatSyncProvider<E extends ProviderEvent = ProviderEvent>
|
|
|
126
127
|
url(thread: string, message: string): Promise<string | null>;
|
|
127
128
|
read(target: string, options: {
|
|
128
129
|
limit: number;
|
|
130
|
+
after: string | null;
|
|
129
131
|
}): Promise<SourceRead>;
|
|
130
132
|
join(target: string): Promise<SourceJoin>;
|
|
131
133
|
fetchFile(file: ExternalFile): Promise<Uint8Array | null>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import "./external-
|
|
2
|
-
import { n as ResolvedTarget, r as SyncProvider } from "./provider-
|
|
1
|
+
import "./external-D3p5T57R.mjs";
|
|
2
|
+
import { n as ResolvedTarget, r as SyncProvider } from "./provider-BaVmi4Hi.mjs";
|
|
3
3
|
import { n as userTokensOf } from "./users-yryHKFMo.mjs";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { Chat, stringifyMarkdown } from "chat";
|
package/dist/cli-commands.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as option, i as groups, n as filters, o as options, r as globalOptions, s as usage, t as commands } from "./commands-
|
|
1
|
+
import { a as option, i as groups, n as filters, o as options, r as globalOptions, s as usage, t as commands } from "./commands-C3R1AWgm.mjs";
|
|
2
2
|
export { commands, filters, globalOptions, groups, option, options, usage };
|
package/dist/cli-embedded.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { d as Board } from "./backend-
|
|
2
|
-
import "./index-
|
|
1
|
+
import { d as Board } from "./backend-CPC_vyOo.mjs";
|
|
2
|
+
import "./index-D3VvNfh_.mjs";
|
|
3
3
|
//#region src/cli/failure.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* What kind of failure it is, which fixes the exit code: 2 for `usage` and `missing_input`, 130
|
package/dist/cli-embedded.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as commandLine, c as failureLines, i as mainWith, l as formatFailure, n as commandWord, o as exitCode, r as main, s as failureDocument, t as Failed } from "./main-
|
|
1
|
+
import { a as commandLine, c as failureLines, i as mainWith, l as formatFailure, n as commandWord, o as exitCode, r as main, s as failureDocument, t as Failed } from "./main-CVBqe3VO.mjs";
|
|
2
2
|
//#region src/cli/embedded.ts
|
|
3
3
|
/**
|
|
4
4
|
* Board's CLI as `appName` names it in help, usage, and errors (`vercel board`). With no board
|
package/dist/cli.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
3
|
-
import "./schemas-
|
|
4
|
-
import { a as option } from "./commands-
|
|
3
|
+
import "./schemas-DBmUO6pr.mjs";
|
|
4
|
+
import { a as option } from "./commands-C3R1AWgm.mjs";
|
|
5
5
|
import { l as projectDir } from "./node-Zd_C-gxP.mjs";
|
|
6
|
-
import { r as main, u as who } from "./main-
|
|
6
|
+
import { r as main, u as who } from "./main-CVBqe3VO.mjs";
|
|
7
7
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
8
8
|
import { appendFile, readFile } from "node:fs/promises";
|
|
9
9
|
import { join } from "node:path";
|
|
@@ -661,7 +661,7 @@ const local = {
|
|
|
661
661
|
if (args.length > 0) throw usage("serve takes no arguments: pass its port with --port, and its directory with --dir.");
|
|
662
662
|
const port = portFlag();
|
|
663
663
|
const root = projectDir();
|
|
664
|
-
const { localHandle, serveLocal } = await import("./serve-
|
|
664
|
+
const { localHandle, serveLocal } = await import("./serve-G9RuaDSr.mjs");
|
|
665
665
|
const handle = flags.handle ?? process.env.BOARD_HANDLE;
|
|
666
666
|
const local = await serveLocal({
|
|
667
667
|
dir: flags.dir ?? join(root, ".board"),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as AccessSummary } from "./access-CV96SYgr.mjs";
|
|
2
|
-
import { a as ExternalCopy, g as BoardEvent, o as Message, p as Link, r as Channel, t as Actor, u as ExternalEvent, x as Attachment } from "./folds-
|
|
2
|
+
import { a as ExternalCopy, g as BoardEvent, o as Message, p as Link, r as Channel, t as Actor, u as ExternalEvent, x as Attachment } from "./folds-ZkHM4M-v.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
//#region src/api/routes.d.ts
|
|
5
5
|
/**
|
|
@@ -171,6 +171,7 @@ declare const AckBody: z.ZodObject<{
|
|
|
171
171
|
declare const SourceReadQuery: z.ZodObject<{
|
|
172
172
|
target: z.ZodString;
|
|
173
173
|
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
174
|
+
after: z.ZodOptional<z.ZodString>;
|
|
174
175
|
}, z.core.$strip>;
|
|
175
176
|
/** A channel to add the board's app to, so it can be read on demand: `#name`, an ID, or a link into it. */
|
|
176
177
|
declare const SourceJoinBody: z.ZodObject<{
|
|
@@ -495,6 +496,7 @@ declare const InboxPushBody: z.ZodObject<{
|
|
|
495
496
|
type: z.ZodString;
|
|
496
497
|
title: z.ZodString;
|
|
497
498
|
url: z.ZodNullable<z.ZodString>;
|
|
499
|
+
body: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
498
500
|
labels: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
499
501
|
unset: z.ZodArray<z.ZodString>;
|
|
500
502
|
owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -911,6 +913,8 @@ type SourceRead = {
|
|
|
911
913
|
messages: SourceMessage[];
|
|
912
914
|
/** The platform has more than `limit`: older messages in a channel, later replies in a thread. */
|
|
913
915
|
more: boolean;
|
|
916
|
+
/** Where to pick up, as `after`, when the platform can page past `limit`. */
|
|
917
|
+
next?: string | null;
|
|
914
918
|
};
|
|
915
919
|
/** A channel the board's app joined, or was already in (`joined: false`). */
|
|
916
920
|
type SourceJoin = {
|
|
@@ -1226,6 +1230,7 @@ declare const routes: {
|
|
|
1226
1230
|
type: z.ZodString;
|
|
1227
1231
|
title: z.ZodString;
|
|
1228
1232
|
url: z.ZodNullable<z.ZodString>;
|
|
1233
|
+
body: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1229
1234
|
labels: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
1230
1235
|
unset: z.ZodArray<z.ZodString>;
|
|
1231
1236
|
owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -1535,6 +1540,7 @@ declare const routes: {
|
|
|
1535
1540
|
readonly query: z.ZodObject<{
|
|
1536
1541
|
target: z.ZodString;
|
|
1537
1542
|
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
1543
|
+
after: z.ZodOptional<z.ZodString>;
|
|
1538
1544
|
}, z.core.$strip>;
|
|
1539
1545
|
};
|
|
1540
1546
|
/** Adds the app to a public channel so it can be read, where that sync runs and opted in with `join`. Full access only. */
|
|
@@ -1930,6 +1936,7 @@ declare function createBoardClient(options: ClientOptions): {
|
|
|
1930
1936
|
*/
|
|
1931
1937
|
readSource: (key: string, target: string, opts?: {
|
|
1932
1938
|
limit?: number;
|
|
1939
|
+
after?: string;
|
|
1933
1940
|
}) => Promise<SourceRead>;
|
|
1934
1941
|
/** Adds the app to a public channel so `readSource` can read it, where the sync opted in with `join`. Full access only. */
|
|
1935
1942
|
joinSource: (key: string, target: string) => Promise<SourceJoin>;
|
|
@@ -590,10 +590,13 @@ const commands = [
|
|
|
590
590
|
required: true,
|
|
591
591
|
description: "a channel ('#general'), a thread, or a message link"
|
|
592
592
|
}],
|
|
593
|
-
options: [option("limit", "messages to read")],
|
|
593
|
+
options: [option("limit", "messages to read"), option("after", "where the last read left off: the cursor it printed")],
|
|
594
594
|
examples: [{
|
|
595
595
|
name: "read a Slack channel",
|
|
596
596
|
value: "sources slack read '#general'"
|
|
597
|
+
}, {
|
|
598
|
+
name: "read a Linear project's issues",
|
|
599
|
+
value: "sources linear read https://linear.app/acme/project/checkout-a1b2c3"
|
|
597
600
|
}]
|
|
598
601
|
}, {
|
|
599
602
|
name: "join",
|
package/dist/conformance.d.mts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { a as typeOf, i as fileSize, r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
|
-
import { C as toStorable, c as ChannelPath, m as MESSAGE_TEXT_MAX } from "./schemas-
|
|
3
|
-
import { c as externalMessageKey, l as isThreadTarget, s as channelOf } from "./external-
|
|
2
|
+
import { C as toStorable, c as ChannelPath, m as MESSAGE_TEXT_MAX } from "./schemas-DBmUO6pr.mjs";
|
|
3
|
+
import { c as externalMessageKey, l as isThreadTarget, s as channelOf } from "./external-D3p5T57R.mjs";
|
|
4
4
|
import { t as canonicalEmoji } from "./emoji-CTLpvH-A.mjs";
|
|
5
5
|
import { r as derivedId, t as waitUntil } from "./wait-until-Dw4XrIDQ.mjs";
|
|
6
6
|
import { r as badRequest, s as notFound, t as HttpError } from "./errors-CusN4x2l.mjs";
|
|
7
7
|
import { n as replaceMentions } from "./mentions-CB6ShY4R.mjs";
|
|
8
|
-
import { t as DroppedError } from "./middleware-
|
|
8
|
+
import { t as DroppedError } from "./middleware-CXjEeef4.mjs";
|
|
9
9
|
import { t as afterRetry } from "./attempts-HXcskN1w.mjs";
|
|
10
|
-
import { a as channelMissing, i as UnreadableError, n as ResolvedTarget, o as routesFor, t as ReceivedEvent } from "./provider-
|
|
11
|
-
import { a as immutable, n as SyncOverrides } from "./events-
|
|
10
|
+
import { a as channelMissing, i as UnreadableError, n as ResolvedTarget, o as routesFor, t as ReceivedEvent } from "./provider-BaVmi4Hi.mjs";
|
|
11
|
+
import { a as immutable, n as SyncOverrides } from "./events-qf5XG5VQ.mjs";
|
|
12
12
|
import { t as displayText } from "./bots-CGkO76qH.mjs";
|
|
13
13
|
//#region src/sync/engine.ts
|
|
14
14
|
/** `files`: a message with files may have no text. */
|
|
@@ -872,7 +872,7 @@ function createSyncEngine(entries) {
|
|
|
872
872
|
const root = await messageOf(rootId);
|
|
873
873
|
const createdHere = root?.origin?.message === `lifecycle:${meta.id}`;
|
|
874
874
|
const desired = labels((createdHere ? rootDraft({
|
|
875
|
-
text: event
|
|
875
|
+
text: lifecycleText(event),
|
|
876
876
|
labels: event.labels,
|
|
877
877
|
author: event.author,
|
|
878
878
|
raw
|
|
@@ -883,7 +883,7 @@ function createSyncEngine(entries) {
|
|
|
883
883
|
if (root && !root.deleted && (Object.keys(set).length || unset.length)) await setLabels(event.author, set, unset);
|
|
884
884
|
if (event.retitled && root && !root.deleted && root.from === key && root.origin?.thread === event.thread) {
|
|
885
885
|
const text = clampText(overrides.text ?? rootDraft({
|
|
886
|
-
text: event
|
|
886
|
+
text: lifecycleText(event),
|
|
887
887
|
labels: root.labels,
|
|
888
888
|
author: event.author,
|
|
889
889
|
raw
|
|
@@ -916,7 +916,7 @@ function createSyncEngine(entries) {
|
|
|
916
916
|
message: null
|
|
917
917
|
};
|
|
918
918
|
const draft = rootDraft({
|
|
919
|
-
text: event
|
|
919
|
+
text: lifecycleText(event),
|
|
920
920
|
labels: event.labels,
|
|
921
921
|
author: event.author,
|
|
922
922
|
raw
|
|
@@ -1289,14 +1289,14 @@ function createSyncEngine(entries) {
|
|
|
1289
1289
|
postedAsPerson = true;
|
|
1290
1290
|
return asPerson;
|
|
1291
1291
|
}
|
|
1292
|
-
return platform.post(native(target), "channel", body);
|
|
1292
|
+
if (root.attachments.length === 0 || !platform.startsThreadsWithFiles) return platform.post(native(target), "channel", body);
|
|
1293
1293
|
}
|
|
1294
1294
|
try {
|
|
1295
1295
|
const sent = await platform.post(native(target), "channel", {
|
|
1296
1296
|
...body,
|
|
1297
1297
|
files: await messageFiles(root)
|
|
1298
1298
|
});
|
|
1299
|
-
uploaded = sent.files ?? [];
|
|
1299
|
+
uploaded = mediaOnly ? sent.files ?? [] : sent.files;
|
|
1300
1300
|
return sent;
|
|
1301
1301
|
} catch (error) {
|
|
1302
1302
|
if (!platform.refusesFiles(error) && !keepsNoFiles(error)) throw error;
|
|
@@ -1500,10 +1500,13 @@ function createSyncEngine(entries) {
|
|
|
1500
1500
|
return 403;
|
|
1501
1501
|
};
|
|
1502
1502
|
/** A platform channel or thread, read for the caller: nothing is synced, stored, or joined. */
|
|
1503
|
-
async function readSource(key, target, limit) {
|
|
1503
|
+
async function readSource(key, target, limit, after) {
|
|
1504
1504
|
const platform = platformOf(key);
|
|
1505
1505
|
try {
|
|
1506
|
-
return await platform.read(target, {
|
|
1506
|
+
return await platform.read(target, {
|
|
1507
|
+
limit,
|
|
1508
|
+
after
|
|
1509
|
+
});
|
|
1507
1510
|
} catch (error) {
|
|
1508
1511
|
if (error instanceof UnreadableError) throw new HttpError(unreadableStatus(error.code), error.code, error.message);
|
|
1509
1512
|
throw badRequest(`can't read ${target} from ${key}: ${describe(error)}`, "platform_error");
|
|
@@ -1542,5 +1545,10 @@ function createSyncEngine(entries) {
|
|
|
1542
1545
|
joinSource
|
|
1543
1546
|
};
|
|
1544
1547
|
}
|
|
1548
|
+
/** A lifecycle root's text: the title, its link, and what the PR or issue says. */
|
|
1549
|
+
function lifecycleText(event) {
|
|
1550
|
+
const head = event.url ? `${event.title}\n${event.url}` : event.title;
|
|
1551
|
+
return event.body ? `${head}\n\n${event.body}` : head;
|
|
1552
|
+
}
|
|
1545
1553
|
//#endregion
|
|
1546
1554
|
export { createSyncEngine };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { d as LabelKey, p as Labels } from "./schemas-
|
|
2
|
-
import { n as ExternalEvent, r as ExternalLocation } from "./external-
|
|
1
|
+
import { d as LabelKey, p as Labels } from "./schemas-DBmUO6pr.mjs";
|
|
2
|
+
import { n as ExternalEvent, r as ExternalLocation } from "./external-D3p5T57R.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
//#region src/sync/events.ts
|
|
5
5
|
const EventBase = z.object({
|