@vercel/board 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{a2-HUbQJ0RD.mjs → a2-D_28Pdip.mjs} +190 -76
- package/dist/access-CQ39gqEV.mjs +226 -0
- package/dist/access.d.mts +1 -1
- package/dist/access.mjs +3 -3
- package/dist/{actors-DcJhFbwJ.mjs → actors-xPiamLWk.mjs} +2 -2
- package/dist/{auth-JO22iIwr.mjs → auth-BPinwfN0.mjs} +3 -226
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +2 -2
- 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 +17 -7
- package/dist/backend-a2.mjs +2 -2
- package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-16SbvnAK.mjs} +2 -2
- package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-3PoBYQoo.d.mts} +13 -3
- package/dist/{backend-http-CzIJMm58.mjs → backend-http-BPaNYLWj.mjs} +2 -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-CPC_vyOo.d.mts → backend-n3hajfrD.d.mts} +11 -5
- package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-C4Kee2-K.mjs} +203 -31
- package/dist/{backend-vercel-DQc_TaHp.mjs → backend-vercel-Kd8nvW5A.mjs} +3 -3
- package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-qznuDxnN.d.mts} +16 -5
- 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-CqILvdmr.d.mts} +2 -2
- package/dist/{blob-DdbpJqN1.mjs → blob-DQK5prXT.mjs} +1 -1
- package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-D6jVT4wS.d.mts} +3 -3
- 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-CjAatpV4.d.mts} +60 -10
- package/dist/{commands-C3R1AWgm.mjs → commands-B5DHjZKK.mjs} +50 -9
- package/dist/{commands-D6gvrOh3.d.mts → commands-BtH69yqt.d.mts} +17 -0
- package/dist/{conformance-DlnfuzEB.mjs → conformance-C7fwIvmN.mjs} +258 -18
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-DQxXk7dX.mjs → engine-DkdkXQEy.mjs} +2 -2
- package/dist/{errors-DDktVeUa.d.mts → errors-87TVtXeb.d.mts} +1 -1
- package/dist/{client-D0Px7KzF.mjs → errors-C_5rB8Iz.mjs} +104 -4
- package/dist/{exports-BG-0Wyn-.mjs → exports-CG0P8Qi4.mjs} +54 -28
- package/dist/exports-CrGA5_yN.d.mts +5 -0
- package/dist/{folds-ZkHM4M-v.d.mts → folds-8wRRXjAt.d.mts} +2 -0
- package/dist/{folds-BDMSxBUf.mjs → folds-DGYkMW19.mjs} +1 -0
- package/dist/{index-D3VvNfh_.d.mts → index-BygAXP7t.d.mts} +2 -2
- package/dist/index.d.mts +6 -6
- package/dist/{index.default-rB8sJ9RX.mjs → index.default-DhkgpfR2.mjs} +1 -1
- package/dist/index.mjs +5 -6
- package/dist/index.node.d.mts +6 -6
- package/dist/index.node.mjs +3 -4
- package/dist/inspect/app.js +29 -29
- 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-D2s-QBOK.mjs} +70 -23
- package/dist/{middleware-CXjEeef4.mjs → middleware-DlJQYiwV.mjs} +1 -1
- package/dist/{node-Zd_C-gxP.mjs → node-D8Rtg1su.mjs} +1 -1
- package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
- package/dist/{project-CBXlLftU.mjs → project-C_2xs57L.mjs} +1 -1
- package/dist/{provider-DmSTp9lI.d.mts → provider-Bm46LAdp.d.mts} +2 -2
- package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-BWJZHUGg.mjs} +40 -24
- 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-8jqLewgw.mjs} +3 -3
- 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-lPoBDEds.mjs} +7 -7
- package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
- package/dist/{server-BixV_byP.mjs → server-D82a4uMe.mjs} +4 -4
- package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
- package/dist/{serving-B75Y4BF-.mjs → serving-DKny38LK.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-Bj0fk6dn.mjs} +1 -1
- package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-Dc8G78XV.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-github.d.mts +2 -2
- package/dist/sync-kapso.d.mts +2 -2
- package/dist/sync-linear.d.mts +3 -3
- package/dist/sync-linear.mjs +4 -2
- package/dist/sync-slack.d.mts +3 -3
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.d.mts +2 -2
- package/dist/{tokens-CC3izSNr.mjs → tokens-C5_yGRlL.mjs} +2 -2
- package/dist/{types-No0WdhE5.d.mts → types-shZmGZ-q.d.mts} +2 -2
- package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-B_5b0-ZG.mjs} +46 -17
- package/docs/01-board/04-messages.md +93 -0
- package/docs/01-board/08-middleware.md +5 -2
- package/docs/04-reference/01-cli.md +23 -10
- package/docs/04-reference/02-board.md +20 -5
- package/docs/04-reference/03-http.md +17 -6
- package/docs/04-reference/05-conformance.md +14 -3
- package/package.json +2 -2
- package/dist/errors-CusN4x2l.mjs +0 -88
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { u as BoardError } from "./errors-C_5rB8Iz.mjs";
|
|
2
|
+
import { r as compileAccess } from "./access-CQ39gqEV.mjs";
|
|
2
3
|
//#region src/conformance/index.ts
|
|
3
4
|
var Mismatch = class extends Error {};
|
|
4
5
|
const show = (value) => JSON.stringify(value) ?? String(value);
|
|
@@ -41,6 +42,31 @@ const needRoot = (ctx) => {
|
|
|
41
42
|
reply: ctx.reply
|
|
42
43
|
};
|
|
43
44
|
};
|
|
45
|
+
/** The sync key the sync checks register, a platform of their own, so nothing reaches a real one. */
|
|
46
|
+
const SYNC_KEY = "conformance";
|
|
47
|
+
/**
|
|
48
|
+
* Registers a sync on `SYNC_KEY` for a check, and says where to put the token's registration back
|
|
49
|
+
* after: or why it can't, since registering replaces the token's watchers.
|
|
50
|
+
*/
|
|
51
|
+
async function registerSync(ctx) {
|
|
52
|
+
const registered = (await ctx.board.watchers()).find((registration) => registration.owner === ctx.me.principal);
|
|
53
|
+
if (registered && (Object.keys(registered.watch).length > 0 || (registered.sync?.length ?? 0) > 0)) return { skip: "this token has watchers or a sync registered here, which registering would replace" };
|
|
54
|
+
const where = wokenAt(registered);
|
|
55
|
+
try {
|
|
56
|
+
await ctx.board.register({
|
|
57
|
+
url: null,
|
|
58
|
+
watch: {},
|
|
59
|
+
sync: [{
|
|
60
|
+
key: SYNC_KEY,
|
|
61
|
+
platform: SYNC_KEY
|
|
62
|
+
}]
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof BoardError && error.status === 403) return { skip: "this token can't register a sync here" };
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
return { where };
|
|
69
|
+
}
|
|
44
70
|
async function post(ctx, text, extra = {}) {
|
|
45
71
|
const message = await ctx.board.post(ctx.channel, {
|
|
46
72
|
text,
|
|
@@ -51,6 +77,15 @@ async function post(ctx, text, extra = {}) {
|
|
|
51
77
|
ctx.posted.push(message.id);
|
|
52
78
|
return message;
|
|
53
79
|
}
|
|
80
|
+
async function reply(ctx, re, text, extra = {}) {
|
|
81
|
+
const message = await ctx.board.reply(re, {
|
|
82
|
+
text,
|
|
83
|
+
noSync: true,
|
|
84
|
+
...extra
|
|
85
|
+
});
|
|
86
|
+
ctx.posted.push(message.id);
|
|
87
|
+
return message;
|
|
88
|
+
}
|
|
54
89
|
const CHECKS = [
|
|
55
90
|
{
|
|
56
91
|
group: "core",
|
|
@@ -80,6 +115,31 @@ const CHECKS = [
|
|
|
80
115
|
check((await ctx.board.thread(root.id)).replies.some((m) => m.id === reply.id), `the thread's replies should include ${reply.id}`);
|
|
81
116
|
}
|
|
82
117
|
},
|
|
118
|
+
{
|
|
119
|
+
group: "core",
|
|
120
|
+
name: "replies after a message only while it's the thread's newest, one of any that race",
|
|
121
|
+
run: async (ctx) => {
|
|
122
|
+
const root = await post(ctx, "conformance: a thread to reply after");
|
|
123
|
+
const first = await reply(ctx, root.id, "conformance: after the root", { after: root.id });
|
|
124
|
+
await rejects(reply(ctx, root.id, "conformance: late", { after: root.id }), 409, "newer_message", "a reply after the root, once another came after it");
|
|
125
|
+
same((await ctx.board.thread(root.id)).replies.length, 1, "the replies after a refused one");
|
|
126
|
+
const deleted = await reply(ctx, root.id, "conformance: deleted", { after: first.id });
|
|
127
|
+
await ctx.board.delete(deleted.id);
|
|
128
|
+
await rejects(reply(ctx, root.id, "conformance: after a deleted one", { after: first.id }), 409, "newer_message", "a reply after a message a deleted one came after");
|
|
129
|
+
const racing = await Promise.allSettled([
|
|
130
|
+
1,
|
|
131
|
+
2,
|
|
132
|
+
3
|
|
133
|
+
].map((n) => reply(ctx, root.id, `conformance: racing ${n}`, { after: deleted.id })));
|
|
134
|
+
same(racing.filter((result) => result.status === "fulfilled").length, 1, "the replies that landed of three racing after the same message");
|
|
135
|
+
for (const result of racing) {
|
|
136
|
+
if (result.status === "fulfilled") continue;
|
|
137
|
+
const error = result.reason;
|
|
138
|
+
check(error instanceof BoardError && error.status === 409 && error.code === "newer_message", `a reply that lost the race should be 409 newer_message, got ${error instanceof BoardError ? `${error.status} ${error.code}` : String(error)}`);
|
|
139
|
+
}
|
|
140
|
+
same((await ctx.board.thread(root.id)).replies.length, 3, "the replies after the race, a deleted one included");
|
|
141
|
+
}
|
|
142
|
+
},
|
|
83
143
|
{
|
|
84
144
|
group: "core",
|
|
85
145
|
name: "posts once for a client ID, however many times it is sent",
|
|
@@ -116,6 +176,78 @@ const CHECKS = [
|
|
|
116
176
|
same((await version()).labels.state, void 0, "the label unset");
|
|
117
177
|
}
|
|
118
178
|
},
|
|
179
|
+
{
|
|
180
|
+
group: "core",
|
|
181
|
+
name: "labels a message only while its labels are as the write says, one of any that race",
|
|
182
|
+
run: async (ctx) => {
|
|
183
|
+
const message = await post(ctx, "conformance: a message to claim");
|
|
184
|
+
const labels = async () => (await ctx.board.thread(message.id)).message.labels;
|
|
185
|
+
const racing = await Promise.allSettled([
|
|
186
|
+
1,
|
|
187
|
+
2,
|
|
188
|
+
3
|
|
189
|
+
].map((n) => ctx.board.label(message.id, {
|
|
190
|
+
set: { owner: `agent-${n}` },
|
|
191
|
+
if: { owner: null }
|
|
192
|
+
})));
|
|
193
|
+
const landed = racing.flatMap((result, n) => result.status === "fulfilled" ? [`agent-${n + 1}`] : []);
|
|
194
|
+
same(landed.length, 1, "the claims that landed of three racing where no one had one");
|
|
195
|
+
for (const result of racing) {
|
|
196
|
+
if (result.status === "fulfilled") continue;
|
|
197
|
+
const error = result.reason;
|
|
198
|
+
check(error instanceof BoardError && error.status === 409 && error.code === "label_conflict", `a claim that lost the race should be 409 label_conflict, got ${error instanceof BoardError ? `${error.status} ${error.code}` : String(error)}`);
|
|
199
|
+
}
|
|
200
|
+
same((await labels()).owner, landed[0], "the owner, after the race");
|
|
201
|
+
await rejects(ctx.board.label(message.id, {
|
|
202
|
+
set: { owner: "someone" },
|
|
203
|
+
if: { owner: "no-one" }
|
|
204
|
+
}), 409, "label_conflict", "a label write from a value the label doesn't have");
|
|
205
|
+
same((await labels()).owner, landed[0], "the owner, after a write from the wrong value");
|
|
206
|
+
await ctx.board.label(message.id, {
|
|
207
|
+
unset: ["owner"],
|
|
208
|
+
if: { owner: landed[0] }
|
|
209
|
+
});
|
|
210
|
+
same((await labels()).owner, void 0, "the owner, released from the value it had");
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
group: "core",
|
|
215
|
+
name: "edits and deletes a message only at the revision it was read at, which only edits and deletes change",
|
|
216
|
+
run: async (ctx) => {
|
|
217
|
+
const message = await post(ctx, "conformance: a message to edit");
|
|
218
|
+
const read = async () => (await ctx.board.thread(message.id)).message;
|
|
219
|
+
const first = (await read()).revision;
|
|
220
|
+
check(typeof first === "number", `a message's revision should be a number, got ${show(first)}`);
|
|
221
|
+
await ctx.board.react(message.id, "eyes");
|
|
222
|
+
await ctx.board.label(message.id, { set: { state: "open" } });
|
|
223
|
+
same((await read()).revision, first, "the revision after a reaction and a label");
|
|
224
|
+
await ctx.board.edit(message.id, "conformance: edited", { revision: first });
|
|
225
|
+
const second = (await read()).revision;
|
|
226
|
+
check(second !== first, `an edit should change the revision from ${first}`);
|
|
227
|
+
await rejects(ctx.board.edit(message.id, "conformance: stale", { revision: first }), 409, "revision_conflict", "an edit at a revision an edit came after");
|
|
228
|
+
await rejects(ctx.board.delete(message.id, { revision: first }), 409, "revision_conflict", "a delete at a revision an edit came after");
|
|
229
|
+
const after = await read();
|
|
230
|
+
check(after.text === "conformance: edited" && !after.deleted, `refused writes should leave it as it was, got ${show({
|
|
231
|
+
text: after.text,
|
|
232
|
+
deleted: after.deleted
|
|
233
|
+
})}`);
|
|
234
|
+
const racing = await Promise.allSettled([
|
|
235
|
+
1,
|
|
236
|
+
2,
|
|
237
|
+
3
|
|
238
|
+
].map((n) => ctx.board.edit(message.id, `conformance: racing ${n}`, { revision: second })));
|
|
239
|
+
same(racing.filter((result) => result.status === "fulfilled").length, 1, "the edits that landed of three racing at the same revision");
|
|
240
|
+
for (const result of racing) {
|
|
241
|
+
if (result.status === "fulfilled") continue;
|
|
242
|
+
const error = result.reason;
|
|
243
|
+
check(error instanceof BoardError && error.status === 409 && error.code === "revision_conflict", `an edit that lost the race should be 409 revision_conflict, got ${error instanceof BoardError ? `${error.status} ${error.code}` : String(error)}`);
|
|
244
|
+
}
|
|
245
|
+
const last = (await read()).revision;
|
|
246
|
+
await ctx.board.delete(message.id, { revision: last });
|
|
247
|
+
check((await read()).deleted, "a delete at the revision it has should delete it");
|
|
248
|
+
await rejects(ctx.board.edit(message.id, "conformance: too late", { revision: last }), 409, "revision_conflict", "an edit at a revision a delete came after");
|
|
249
|
+
}
|
|
250
|
+
},
|
|
119
251
|
{
|
|
120
252
|
group: "core",
|
|
121
253
|
name: "matches a label on the message or its thread's root",
|
|
@@ -261,6 +393,24 @@ const CHECKS = [
|
|
|
261
393
|
same(canceled.state, "canceled", "the canceled schedule");
|
|
262
394
|
}
|
|
263
395
|
},
|
|
396
|
+
{
|
|
397
|
+
group: "schedules",
|
|
398
|
+
name: "keeps noSync on a schedule, so its sends stay off every platform",
|
|
399
|
+
run: async (ctx) => {
|
|
400
|
+
const { root } = needRoot(ctx);
|
|
401
|
+
const schedule = await ctx.board.schedule({ re: root.id }, {
|
|
402
|
+
text: "conformance: later, on the board only",
|
|
403
|
+
noSync: true,
|
|
404
|
+
in: "1d"
|
|
405
|
+
});
|
|
406
|
+
try {
|
|
407
|
+
same(schedule.noSync, true, "the schedule's noSync");
|
|
408
|
+
same((await ctx.board.schedules({ re: root.id })).find((s) => s.id === schedule.id)?.noSync, true, "the listed schedule's noSync");
|
|
409
|
+
} finally {
|
|
410
|
+
await ctx.board.cancelSchedule(schedule.id).catch(() => {});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
},
|
|
264
414
|
{
|
|
265
415
|
group: "attachments",
|
|
266
416
|
name: "takes a file, posts it, and gives it back",
|
|
@@ -341,7 +491,7 @@ const CHECKS = [
|
|
|
341
491
|
group: "sync",
|
|
342
492
|
name: "keeps sync's records: an import as a platform author, its lookups by platform ID, a receipt, and where it's synced",
|
|
343
493
|
run: async (ctx) => {
|
|
344
|
-
const key =
|
|
494
|
+
const key = SYNC_KEY;
|
|
345
495
|
const thread = `${key}:${ctx.labels.conformance}:1`;
|
|
346
496
|
const author = {
|
|
347
497
|
id: `${key}:user:${ctx.labels.conformance}`,
|
|
@@ -354,22 +504,9 @@ const CHECKS = [
|
|
|
354
504
|
thread,
|
|
355
505
|
message: "1"
|
|
356
506
|
};
|
|
357
|
-
const registered =
|
|
358
|
-
if (
|
|
359
|
-
const where =
|
|
360
|
-
try {
|
|
361
|
-
await ctx.board.register({
|
|
362
|
-
url: null,
|
|
363
|
-
watch: {},
|
|
364
|
-
sync: [{
|
|
365
|
-
key,
|
|
366
|
-
platform: key
|
|
367
|
-
}]
|
|
368
|
-
});
|
|
369
|
-
} catch (error) {
|
|
370
|
-
if (error instanceof BoardError && error.status === 403) return "this token can't register a sync here";
|
|
371
|
-
throw error;
|
|
372
|
-
}
|
|
507
|
+
const registered = await registerSync(ctx);
|
|
508
|
+
if ("skip" in registered) return registered.skip;
|
|
509
|
+
const { where } = registered;
|
|
373
510
|
let imported;
|
|
374
511
|
try {
|
|
375
512
|
imported = await ctx.board.syncImport({
|
|
@@ -477,6 +614,109 @@ const CHECKS = [
|
|
|
477
614
|
}).catch(() => {});
|
|
478
615
|
}
|
|
479
616
|
}
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
group: "sync",
|
|
620
|
+
name: "queues a platform thread's events in its sync's inbox once, and leases each after the one before is acked",
|
|
621
|
+
run: async (ctx) => {
|
|
622
|
+
const registered = await registerSync(ctx);
|
|
623
|
+
if ("skip" in registered) return registered.skip;
|
|
624
|
+
try {
|
|
625
|
+
const run = ctx.labels.conformance;
|
|
626
|
+
const author = {
|
|
627
|
+
id: `${SYNC_KEY}:user:${run}`,
|
|
628
|
+
handle: "conformance",
|
|
629
|
+
name: "Conformance",
|
|
630
|
+
avatar: null,
|
|
631
|
+
isBot: false
|
|
632
|
+
};
|
|
633
|
+
const thread = `${SYNC_KEY}:${run}:inbox`;
|
|
634
|
+
const eventOf = (message) => ({
|
|
635
|
+
id: `conformance-inbox-${run}-${message}`,
|
|
636
|
+
event: {
|
|
637
|
+
kind: "message",
|
|
638
|
+
channel: `${SYNC_KEY}:${run}`,
|
|
639
|
+
thread,
|
|
640
|
+
subthread: null,
|
|
641
|
+
message,
|
|
642
|
+
isRoot: message === "1",
|
|
643
|
+
text: `conformance: inbox event ${message}`,
|
|
644
|
+
context: null,
|
|
645
|
+
author
|
|
646
|
+
},
|
|
647
|
+
raw: {
|
|
648
|
+
conformance: run,
|
|
649
|
+
message
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
const pushed = [eventOf("1"), eventOf("2")];
|
|
653
|
+
await ctx.board.inboxPush(SYNC_KEY, pushed);
|
|
654
|
+
await ctx.board.inboxPush(SYNC_KEY, [pushed[0]]);
|
|
655
|
+
const lease = async () => (await ctx.board.inboxNext(SYNC_KEY, { limit: 50 })).filter((item) => item.thread === thread);
|
|
656
|
+
for (const { event, raw } of pushed) {
|
|
657
|
+
const leased = await lease();
|
|
658
|
+
same(leased.length, 1, `the events leased from the platform thread, for message ${event.message}`);
|
|
659
|
+
const item = leased[0];
|
|
660
|
+
same(item.event.kind === "message" ? item.event.message : null, event.message, "the platform message leased next");
|
|
661
|
+
same(JSON.stringify(item.raw), JSON.stringify(raw), "the leased event's raw payload");
|
|
662
|
+
await ctx.board.inboxAck(SYNC_KEY, item.id, { lease: item.lease });
|
|
663
|
+
}
|
|
664
|
+
same((await lease()).length, 0, "the events leased once both are acked");
|
|
665
|
+
} finally {
|
|
666
|
+
await ctx.board.register({
|
|
667
|
+
...registered.where,
|
|
668
|
+
watch: {}
|
|
669
|
+
}).catch(() => {});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
group: "sync",
|
|
675
|
+
name: "hands a sync each message change as a match of its @sync watcher",
|
|
676
|
+
run: async (ctx) => {
|
|
677
|
+
const access = compileAccess(ctx.me.can);
|
|
678
|
+
if (access.anywhere("sync") && !access.can("sync", ctx.channel)) return `this token can't sync in ${ctx.channel}`;
|
|
679
|
+
const registered = await registerSync(ctx);
|
|
680
|
+
if ("skip" in registered) return registered.skip;
|
|
681
|
+
try {
|
|
682
|
+
const message = await post(ctx, "conformance: for a sync");
|
|
683
|
+
const deadline = Date.now() + 1e4;
|
|
684
|
+
let match;
|
|
685
|
+
while (!match && Date.now() < deadline) match = (await ctx.board.matches({ wait: 5 })).find((m) => m.watcher === "@sync" && m.event.message?.id === message.id);
|
|
686
|
+
check(match, "a pull should hand the sync a match for the post, as its @sync watcher's");
|
|
687
|
+
same(match.event.type, "message.posted", "the sync's match");
|
|
688
|
+
await ctx.board.ack(match.id, { lease: match.lease });
|
|
689
|
+
} finally {
|
|
690
|
+
await ctx.board.register({
|
|
691
|
+
...registered.where,
|
|
692
|
+
watch: {}
|
|
693
|
+
}).catch(() => {});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
group: "sync",
|
|
699
|
+
name: "keeps noSync on a post and a reply, so sync sends neither",
|
|
700
|
+
run: async (ctx) => {
|
|
701
|
+
const { root, reply } = needRoot(ctx);
|
|
702
|
+
const thread = await ctx.board.thread(root.id);
|
|
703
|
+
same(thread.message.noSync, true, "a post's noSync, read back");
|
|
704
|
+
same(thread.replies.find((m) => m.id === reply.id)?.noSync, true, "a reply's noSync, read back");
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
group: "sync",
|
|
709
|
+
name: "finds no one for a platform account nobody linked",
|
|
710
|
+
run: async (ctx) => {
|
|
711
|
+
let found;
|
|
712
|
+
try {
|
|
713
|
+
found = await ctx.board.syncFind({ account: `${SYNC_KEY}:user:${ctx.labels.conformance}` });
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (error instanceof BoardError && error.status === 403) return "this token can't look up a sync here";
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
same(found.person, null, "who an account nobody linked is");
|
|
719
|
+
}
|
|
480
720
|
}
|
|
481
721
|
];
|
|
482
722
|
/** Runs the checks against `board`, in order, and reports each. It never throws for what the board answers. */
|
package/dist/conformance.d.mts
CHANGED
package/dist/conformance.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as conformance } from "./conformance-
|
|
1
|
+
import { t as conformance } from "./conformance-C7fwIvmN.mjs";
|
|
2
2
|
export { conformance };
|
|
@@ -2,10 +2,10 @@ import { a as typeOf, i as fileSize, r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTE
|
|
|
2
2
|
import { C as toStorable, c as ChannelPath, m as MESSAGE_TEXT_MAX } from "./schemas-DBmUO6pr.mjs";
|
|
3
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
|
+
import { r as badRequest, s as notFound, t as HttpError } from "./errors-C_5rB8Iz.mjs";
|
|
5
6
|
import { r as derivedId, t as waitUntil } from "./wait-until-Dw4XrIDQ.mjs";
|
|
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-DlJQYiwV.mjs";
|
|
9
9
|
import { t as afterRetry } from "./attempts-HXcskN1w.mjs";
|
|
10
10
|
import { a as channelMissing, i as UnreadableError, n as ResolvedTarget, o as routesFor, t as ReceivedEvent } from "./provider-BaVmi4Hi.mjs";
|
|
11
11
|
import { a as immutable, n as SyncOverrides } from "./events-qf5XG5VQ.mjs";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { a as typeOf, n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
2
|
import { r as normalizeEmoji } from "./emoji-CTLpvH-A.mjs";
|
|
3
|
+
import { ZodError } from "zod";
|
|
3
4
|
//#region src/client/sse.ts
|
|
4
5
|
/** Parses a text/event-stream body into messages. */
|
|
5
6
|
async function* parseSse(body, signal) {
|
|
@@ -230,10 +231,13 @@ function createBoardClient(options) {
|
|
|
230
231
|
const size = input.data instanceof Blob ? input.data.size : input.data.byteLength;
|
|
231
232
|
if (size > 4194304) {
|
|
232
233
|
if (size > 104857600) throw new BoardError("too_large", `Attachments are at most ${DIRECT_UPLOAD_BYTES_MAX / 1024 / 1024} MB.`, 413);
|
|
234
|
+
const bytes = input.data instanceof Blob ? await input.data.arrayBuffer() : input.data;
|
|
235
|
+
const sha256 = btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))));
|
|
233
236
|
const ticket = await call("POST", "/attachments/uploads", { body: {
|
|
234
237
|
name: input.name,
|
|
235
238
|
type,
|
|
236
|
-
size
|
|
239
|
+
size,
|
|
240
|
+
sha256
|
|
237
241
|
} }).catch((error) => {
|
|
238
242
|
if (error instanceof BoardError && error.status === 404 && error.code === "not_found") throw new BoardError("too_large", `This board takes files of at most ${ATTACHMENT_BYTES_MAX / 1024 / 1024} MB.`, 413);
|
|
239
243
|
throw error;
|
|
@@ -260,13 +264,24 @@ function createBoardClient(options) {
|
|
|
260
264
|
},
|
|
261
265
|
/** An attachment's bytes, as the board serves them: `content-type` is the file's type. */
|
|
262
266
|
download: (id, opts = {}) => raw("GET", `/attachments/${encodeURIComponent(id)}`, opts.signal ? { signal: opts.signal } : {}),
|
|
263
|
-
|
|
264
|
-
|
|
267
|
+
/**
|
|
268
|
+
* `revision` is the message's as you read it: if it's been edited or deleted since, nothing
|
|
269
|
+
* changes, and it throws a 409 `revision_conflict`.
|
|
270
|
+
*/
|
|
271
|
+
edit: (id, text, opts = {}) => call("POST", `/messages/${path(id)}/edit`, { body: {
|
|
272
|
+
text,
|
|
273
|
+
...opts
|
|
274
|
+
} }),
|
|
275
|
+
delete: (id, opts = {}) => call("POST", `/messages/${path(id)}/delete`, { body: { ...opts } }),
|
|
265
276
|
/** `emoji` is a short name, `:name:`, or the character itself. */
|
|
266
277
|
react: (id, emoji, opts = {}) => call("POST", `/messages/${path(id)}/reactions`, { body: {
|
|
267
278
|
emoji: normalizeEmoji(emoji),
|
|
268
279
|
...opts
|
|
269
280
|
} }),
|
|
281
|
+
/**
|
|
282
|
+
* `if` is what the labels must be now, a value or null for one that mustn't be set: if one
|
|
283
|
+
* isn't, nothing changes, and it throws a 409 `label_conflict`.
|
|
284
|
+
*/
|
|
270
285
|
label: (id, change) => call("POST", `/messages/${path(id)}/labels`, { body: change }),
|
|
271
286
|
/**
|
|
272
287
|
* Shows typing in the platform threads the message's thread is synced to
|
|
@@ -405,4 +420,89 @@ async function toError(response) {
|
|
|
405
420
|
return new BoardError("http_error", `The board answered ${`${response.status} ${response.statusText}`.trim()}.`, response.status);
|
|
406
421
|
}
|
|
407
422
|
//#endregion
|
|
408
|
-
|
|
423
|
+
//#region src/server/errors.ts
|
|
424
|
+
var HttpError = class extends Error {
|
|
425
|
+
status;
|
|
426
|
+
code;
|
|
427
|
+
/** What fixes it, when there's one (`BoardError`'s `hint`). */
|
|
428
|
+
hint;
|
|
429
|
+
/** `handle` for a handle's own error on its way through: everything else is the board's. */
|
|
430
|
+
origin;
|
|
431
|
+
constructor(status, code, message, options = {}) {
|
|
432
|
+
super(message);
|
|
433
|
+
this.status = status;
|
|
434
|
+
this.code = code;
|
|
435
|
+
this.hint = options.hint;
|
|
436
|
+
this.origin = options.origin ?? "board";
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
/** A request a host's own verifier rejected with `response`, which the board answers with as it is. */
|
|
440
|
+
var RejectedError = class extends HttpError {
|
|
441
|
+
response;
|
|
442
|
+
constructor(response) {
|
|
443
|
+
super(response.status, "rejected", `rejected with ${response.status}`);
|
|
444
|
+
this.response = response;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
const badRequest = (message, code = "invalid_request") => new HttpError(400, code, message);
|
|
448
|
+
const unauthorized = (message) => new HttpError(401, "unauthorized", message);
|
|
449
|
+
const forbidden = (message) => new HttpError(403, "forbidden", message);
|
|
450
|
+
const notFound = (message, code = "not_found") => new HttpError(404, code, message);
|
|
451
|
+
const duplicateId = () => new HttpError(409, "duplicate_id", "that id was already used for a different write");
|
|
452
|
+
const json = (body, status = 200, headers) => new Response(JSON.stringify(body), {
|
|
453
|
+
status,
|
|
454
|
+
headers: {
|
|
455
|
+
"content-type": "application/json",
|
|
456
|
+
...headers
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
/**
|
|
460
|
+
* The issue a request is told about. A union that matched no option says only "Invalid input", so
|
|
461
|
+
* it's the first issue of the option that came closest, the one with the fewest, at its own path.
|
|
462
|
+
*/
|
|
463
|
+
function reported(issue) {
|
|
464
|
+
if (issue.code !== "invalid_union" || issue.errors.length === 0) return issue;
|
|
465
|
+
const first = issue.errors.reduce((best, option) => option.length < best.length ? option : best)[0];
|
|
466
|
+
return first ? reported({
|
|
467
|
+
...first,
|
|
468
|
+
path: [...issue.path, ...first.path]
|
|
469
|
+
}) : issue;
|
|
470
|
+
}
|
|
471
|
+
/** Every failure becomes `{ error: { code, message, hint? } }` with a matching status. */
|
|
472
|
+
function toErrorResponse(error) {
|
|
473
|
+
if (error instanceof RejectedError) return error.response;
|
|
474
|
+
if (error instanceof HttpError) {
|
|
475
|
+
const response = json({ error: {
|
|
476
|
+
code: error.code,
|
|
477
|
+
message: error.message,
|
|
478
|
+
...error.hint !== void 0 ? { hint: error.hint } : {}
|
|
479
|
+
} }, error.status);
|
|
480
|
+
if (error.origin === "handle") handleErrors.add(response);
|
|
481
|
+
return response;
|
|
482
|
+
}
|
|
483
|
+
if (error instanceof ZodError) {
|
|
484
|
+
const issue = error.issues[0] && reported(error.issues[0]);
|
|
485
|
+
const path = issue?.path.length ? `${issue.path.join(".")}: ` : "";
|
|
486
|
+
return json({ error: {
|
|
487
|
+
code: "invalid_request",
|
|
488
|
+
message: `${path}${issue?.message ?? "invalid request"}`
|
|
489
|
+
} }, 400);
|
|
490
|
+
}
|
|
491
|
+
const code = error?.code;
|
|
492
|
+
if (code === "INVALID_PAYLOAD") return json({ error: {
|
|
493
|
+
code: "invalid_request",
|
|
494
|
+
message: error.message
|
|
495
|
+
} }, 400);
|
|
496
|
+
if (code === "PARTIAL_DUPLICATE_BATCH" || code === "DUPLICATE_EVENT_ID") return toErrorResponse(duplicateId());
|
|
497
|
+
if (code === "STORE_UNAVAILABLE" || code === "STORE_NOT_CONFIGURED") return json({ error: {
|
|
498
|
+
code: "unavailable",
|
|
499
|
+
message: error.message
|
|
500
|
+
} }, 503);
|
|
501
|
+
console.error("[board]", error);
|
|
502
|
+
return json({ error: {
|
|
503
|
+
code: "internal",
|
|
504
|
+
message: "internal error"
|
|
505
|
+
} }, 500);
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
export { forbidden as a, toErrorResponse as c, createBoardClient as d, duration as f, duplicateId as i, unauthorized as l, RejectedError as n, json as o, badRequest as r, notFound as s, HttpError as t, BoardError as u };
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
2
|
import "./schemas-DBmUO6pr.mjs";
|
|
3
|
-
import { A as
|
|
4
|
-
import {
|
|
3
|
+
import { A as ScheduleBody, B as SyncImportBody, C as LinkedQuery, D as ReactBody, E as PostBody, F as SyncAccountBody, G as TypingBody, H as SyncRootsQuery, I as SyncBody, K as UploadStartBody, L as SyncCodeBody, M as SignInQuery, N as SourceJoinBody, O as Ref, P as SourceReadQuery, R as SyncCopiesQuery, S as LabelBody, T as MessagesQuery, U as SyncUnlinkBody, V as SyncReceiptBody, W as ThreadsQuery, _ as IdentityUnlinkBody, a as METHODS, b as InboxPushBody, c as queryObject, d as VERSION, f as SYNC, g as EventsQuery, h as EditBody, j as SchedulesQuery, k as RegisterBody, l as readCapped, m as DeleteBody, o as createRouter, p as AckBody, s as openapi, u as readJson, v as InboxAckBody, w as MatchesQuery, x as InboxQuery, y as InboxNextQuery, z as SyncFindQuery } from "./watchers-hash-B_5b0-ZG.mjs";
|
|
4
|
+
import { a as forbidden, c as toErrorResponse, d as createBoardClient, l as unauthorized, n as RejectedError, o as json, r as badRequest, s as notFound, t as HttpError, u as BoardError } from "./errors-C_5rB8Iz.mjs";
|
|
5
5
|
import { r as derivedId } from "./wait-until-Dw4XrIDQ.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import { d as issuersOf, g as verifiesTokens, l as hostPrincipal, o as createAuthority, s as fromTokens } from "./auth-
|
|
8
|
-
import {
|
|
9
|
-
import { r as runMiddleware, t as DroppedError } from "./middleware-CXjEeef4.mjs";
|
|
6
|
+
import { i as resolveActor, r as claimsOf, t as KNOWN_AGENTS } from "./actors-xPiamLWk.mjs";
|
|
7
|
+
import { d as issuersOf, g as verifiesTokens, l as hostPrincipal, o as createAuthority, s as fromTokens } from "./auth-BPinwfN0.mjs";
|
|
8
|
+
import { r as runMiddleware, t as DroppedError } from "./middleware-DlJQYiwV.mjs";
|
|
10
9
|
import { n as syncTarget, r as withEmailMentions, t as mentionsEmails } from "./targets-Cgjs29T0.mjs";
|
|
11
10
|
//#region src/server/policy.ts
|
|
12
11
|
/**
|
|
@@ -216,7 +215,6 @@ async function inputOf(name, request, url, params) {
|
|
|
216
215
|
case "message":
|
|
217
216
|
case "posted":
|
|
218
217
|
case "synced":
|
|
219
|
-
case "delete":
|
|
220
218
|
case "cancelSchedule":
|
|
221
219
|
case "attachment":
|
|
222
220
|
case "uploadFinish": return { id };
|
|
@@ -237,6 +235,10 @@ async function inputOf(name, request, url, params) {
|
|
|
237
235
|
id,
|
|
238
236
|
...EditBody.parse(await body())
|
|
239
237
|
};
|
|
238
|
+
case "delete": return {
|
|
239
|
+
id,
|
|
240
|
+
...DeleteBody.parse(await body())
|
|
241
|
+
};
|
|
240
242
|
case "react": return {
|
|
241
243
|
id,
|
|
242
244
|
...ReactBody.parse(await body())
|
|
@@ -513,6 +515,25 @@ async function createServe(context) {
|
|
|
513
515
|
}));
|
|
514
516
|
}
|
|
515
517
|
/**
|
|
518
|
+
* A schedule, on a backend whose sends post elsewhere: its message through the middleware once,
|
|
519
|
+
* as the caller's post of it, since its text and labels never change and no send comes back here.
|
|
520
|
+
*/
|
|
521
|
+
function screenSchedule(caller, input, signal) {
|
|
522
|
+
const { channel, re, text, labels, noSync, attachments } = input;
|
|
523
|
+
return screenPost(caller, {
|
|
524
|
+
...channel !== void 0 ? { channel } : {},
|
|
525
|
+
...re !== void 0 ? { re } : {},
|
|
526
|
+
text,
|
|
527
|
+
...labels ? { labels } : {},
|
|
528
|
+
...noSync !== void 0 ? { noSync } : {},
|
|
529
|
+
...attachments ? { attachments } : {}
|
|
530
|
+
}, signal, (screened) => backend.schedule(caller, {
|
|
531
|
+
...input,
|
|
532
|
+
text: screened.text,
|
|
533
|
+
...screened.labels ? { labels: screened.labels } : {}
|
|
534
|
+
}, signal));
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
516
537
|
* The caller's own message, still there, where it may post: what an edit of it changes. One it
|
|
517
538
|
* can't read may still be its own, as board()'s own code finds it, only if it was written there
|
|
518
539
|
* without reading too: one its principal wrote where it reads (a repo's workflows share one) is
|
|
@@ -709,22 +730,27 @@ async function createServe(context) {
|
|
|
709
730
|
switch (name) {
|
|
710
731
|
case "post": return importPost(caller, input, signal);
|
|
711
732
|
case "edit": {
|
|
712
|
-
const { id, text } = input;
|
|
733
|
+
const { id, text, revision } = input;
|
|
713
734
|
return change({
|
|
714
735
|
op: "edit",
|
|
715
736
|
author,
|
|
716
737
|
from: null,
|
|
717
738
|
message: id,
|
|
718
739
|
text: await withEmails(text),
|
|
719
|
-
source
|
|
740
|
+
source,
|
|
741
|
+
...revision !== void 0 ? { revision } : {}
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
case "delete": {
|
|
745
|
+
const { id, revision } = input;
|
|
746
|
+
return change({
|
|
747
|
+
op: "delete",
|
|
748
|
+
author,
|
|
749
|
+
from: null,
|
|
750
|
+
message: id,
|
|
751
|
+
...revision !== void 0 ? { revision } : {}
|
|
720
752
|
});
|
|
721
753
|
}
|
|
722
|
-
case "delete": return change({
|
|
723
|
-
op: "delete",
|
|
724
|
-
author,
|
|
725
|
-
from: null,
|
|
726
|
-
message: input.id
|
|
727
|
-
});
|
|
728
754
|
case "react": {
|
|
729
755
|
const { id, emoji, remove } = input;
|
|
730
756
|
return change({
|
|
@@ -737,14 +763,15 @@ async function createServe(context) {
|
|
|
737
763
|
});
|
|
738
764
|
}
|
|
739
765
|
case "label": {
|
|
740
|
-
const { id, set, unset } = input;
|
|
766
|
+
const { id, set, unset, if: expected } = input;
|
|
741
767
|
return change({
|
|
742
768
|
op: "labels",
|
|
743
769
|
author,
|
|
744
770
|
from: null,
|
|
745
771
|
message: id,
|
|
746
772
|
...set ? { set } : {},
|
|
747
|
-
...unset ? { unset } : {}
|
|
773
|
+
...unset ? { unset } : {},
|
|
774
|
+
...expected ? { if: expected } : {}
|
|
748
775
|
});
|
|
749
776
|
}
|
|
750
777
|
default: return invoke(name, me, input, signal);
|
|
@@ -780,7 +807,8 @@ async function createServe(context) {
|
|
|
780
807
|
...input.labels ? { labels: input.labels } : {},
|
|
781
808
|
...input.noSync ? { noSync: true } : {},
|
|
782
809
|
...input.attachments?.length ? { attachments: input.attachments } : {},
|
|
783
|
-
...input.id !== void 0 ? { id: input.id } : {}
|
|
810
|
+
...input.id !== void 0 ? { id: input.id } : {},
|
|
811
|
+
...input.after !== void 0 ? { after: input.after } : {}
|
|
784
812
|
}, signal));
|
|
785
813
|
for (const target of targets) await backend.syncReceipt(me, {
|
|
786
814
|
kind: "thread",
|
|
@@ -796,27 +824,25 @@ async function createServe(context) {
|
|
|
796
824
|
switch (name) {
|
|
797
825
|
case "post": return post(caller, input, signal);
|
|
798
826
|
case "edit": {
|
|
799
|
-
const
|
|
827
|
+
const edit = input;
|
|
800
828
|
const send = (body) => screenEdit(caller, body, signal, (patched) => backend.edit(caller, patched, signal));
|
|
801
|
-
if (!engine || !mentionsEmails(text) || !await mayPost(caller, { re: id }, signal)) return send(
|
|
802
|
-
id,
|
|
803
|
-
text
|
|
804
|
-
});
|
|
829
|
+
if (!engine || !mentionsEmails(edit.text) || !await mayPost(caller, { re: edit.id }, signal)) return send(edit);
|
|
805
830
|
return send({
|
|
806
|
-
|
|
807
|
-
text: await withEmails(text)
|
|
831
|
+
...edit,
|
|
832
|
+
text: await withEmails(edit.text)
|
|
808
833
|
});
|
|
809
834
|
}
|
|
810
835
|
case "syncImport": return syncImport(caller, input, signal);
|
|
811
836
|
case "schedule": {
|
|
812
837
|
const body = input;
|
|
838
|
+
const schedule = (made) => backend.sendsSchedulesHere === false ? screenSchedule(caller, made, signal) : backend.schedule(caller, made, signal);
|
|
813
839
|
const allowed = engine && mentionsEmails(body.text) ? await mayPost(caller, body, signal) : null;
|
|
814
|
-
if (!allowed) return
|
|
840
|
+
if (!allowed) return schedule(body);
|
|
815
841
|
const text = await withEmails(body.text);
|
|
816
|
-
const answer = await
|
|
842
|
+
const answer = await schedule({
|
|
817
843
|
...body,
|
|
818
844
|
text
|
|
819
|
-
}
|
|
845
|
+
});
|
|
820
846
|
const { access } = allowed;
|
|
821
847
|
return text !== body.text && access !== null && !access.readable(answer.schedule.channel) ? {
|
|
822
848
|
...answer,
|