@vercel/board 0.30.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-D_28Pdip.mjs → a2-DngWY2Eb.mjs} +573 -361
- package/dist/{access-CQ39gqEV.mjs → access-CY5xVMIK.mjs} +19 -3
- 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-xPiamLWk.mjs → actors-Bm-ojWWT.mjs} +2 -2
- package/dist/{auth-BPinwfN0.mjs → auth-CyO2aw5n.mjs} +127 -14
- package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
- package/dist/{backend-n3hajfrD.d.mts → backend-Bob-5CM7.d.mts} +86 -41
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +37 -97
- package/dist/backend-a2/contract.mjs +1 -1
- 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 +131 -240
- package/dist/backend-a2.mjs +2 -2
- package/dist/{backend-http-3PoBYQoo.d.mts → backend-http-BRrRbjRx.d.mts} +49 -45
- package/dist/{backend-http-BPaNYLWj.mjs → backend-http-CvUKOn-m.mjs} +11 -9
- package/dist/{backend-http-16SbvnAK.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-Kd8nvW5A.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
- package/dist/{backend-vercel-qznuDxnN.d.mts → backend-vercel-BaSEUdwg.d.mts} +49 -45
- package/dist/{backend-vercel-C4Kee2-K.mjs → backend-vercel-CmY9yZ9m.mjs} +80 -15
- 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-CqILvdmr.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
- package/dist/{blob-DQK5prXT.mjs → blob-WbB2DJKn.mjs} +30 -11
- package/dist/{chat-provider-D6jVT4wS.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-CjAatpV4.d.mts → client-COfYrgfV.d.mts} +184 -221
- package/dist/{commands-BtH69yqt.d.mts → commands-DK4W3HCJ.d.mts} +30 -10
- package/dist/{commands-B5DHjZKK.mjs → commands-L1JSGz2Z.mjs} +162 -45
- package/dist/{conformance-C7fwIvmN.mjs → conformance-DK43HiHf.mjs} +224 -20
- package/dist/conformance.d.mts +8 -2
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-DkdkXQEy.mjs → engine-DurYIRt8.mjs} +29 -210
- package/dist/{errors-87TVtXeb.d.mts → errors-2vGs49Db.d.mts} +1 -1
- package/dist/{errors-C_5rB8Iz.mjs → errors-CJXhhzvT.mjs} +28 -23
- package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
- package/dist/{exports-CG0P8Qi4.mjs → exports-CWIdN0Ub.mjs} +146 -80
- package/dist/exports-FBOQWT7O.d.mts +5 -0
- package/dist/{folds-DGYkMW19.mjs → folds-Cu9Q9zyQ.mjs} +2 -22
- package/dist/{folds-8wRRXjAt.d.mts → folds-DLUzfibJ.d.mts} +13 -40
- package/dist/{index-BygAXP7t.d.mts → index-Ho-ayyQL.d.mts} +2 -2
- package/dist/index.d.mts +7 -7
- package/dist/{index.default-DhkgpfR2.mjs → index.default-BUcwDaAe.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.node.d.mts +7 -7
- package/dist/index.node.mjs +3 -3
- package/dist/inspect/app.js +62 -61
- package/dist/inspect/engine.js +3 -3
- package/dist/{main-D2s-QBOK.mjs → main-Dz-8komG.mjs} +189 -112
- package/dist/{middleware-DlJQYiwV.mjs → middleware-BavYSDUa.mjs} +1 -1
- package/dist/{node-D8Rtg1su.mjs → node-BgeGcwX0.mjs} +7 -3
- package/dist/{project-C_2xs57L.mjs → project-CQ4VUibA.mjs} +1 -1
- package/dist/{provider-Bm46LAdp.d.mts → provider-gynDUoyW.d.mts} +13 -43
- package/dist/{redis-tables-BWJZHUGg.mjs → redis-tables-DHf85ixI.mjs} +1 -19
- package/dist/{reducers-8jqLewgw.mjs → reducers-DcENwtwZ.mjs} +72 -75
- package/dist/{serve-lPoBDEds.mjs → serve-CTmow-YH.mjs} +6 -6
- package/dist/{server-D82a4uMe.mjs → server-DG8D6VV0.mjs} +4 -4
- package/dist/{serving-DKny38LK.mjs → serving-DBFRo6U_.mjs} +1 -1
- package/dist/{sql-Bj0fk6dn.mjs → sql-CT5xVuGr.mjs} +2 -8
- package/dist/{sqlite-db-Dc8G78XV.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
- 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 +2 -3
- 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-C5_yGRlL.mjs → tokens-Dnyd_y1n.mjs} +15 -6
- package/dist/{types-shZmGZ-q.d.mts → types-CmrFjpml.d.mts} +1 -3
- package/dist/{watchers-hash-B_5b0-ZG.mjs → watchers-hash-BjwlnGD9.mjs} +85 -105
- 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 +3 -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 +1 -1
- 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 +86 -41
- package/docs/04-reference/02-board.md +56 -23
- package/docs/04-reference/03-http.md +59 -32
- package/docs/04-reference/04-log.md +8 -11
- package/docs/04-reference/05-conformance.md +35 -6
- package/docs/index.md +2 -2
- package/package.json +6 -9
- package/sql/postgres/board.sql +1 -1
- package/sql/postgres/schema.sql +1 -1
- package/dist/events-qf5XG5VQ.mjs +0 -75
- package/dist/exports-CrGA5_yN.d.mts +0 -5
- 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,14 +1,13 @@
|
|
|
1
1
|
import { a as typeOf, i as fileSize, r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
2
|
import { C as toStorable, c as ChannelPath, m as MESSAGE_TEXT_MAX } from "./schemas-DBmUO6pr.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { 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-
|
|
5
|
+
import { r as badRequest, s as notFound, t as HttpError } from "./errors-CJXhhzvT.mjs";
|
|
6
6
|
import { r as derivedId, t as waitUntil } from "./wait-until-Dw4XrIDQ.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-BavYSDUa.mjs";
|
|
9
9
|
import { t as afterRetry } from "./attempts-HXcskN1w.mjs";
|
|
10
|
-
import { a as
|
|
11
|
-
import { a as immutable, n as SyncOverrides } from "./events-qf5XG5VQ.mjs";
|
|
10
|
+
import { a as immutable, d as routesFor, l as UnreadableError, n as SyncOverrides, o as ReceivedEvent, s as ResolvedTarget, u as channelMissing } from "./events-lujPA3gQ.mjs";
|
|
12
11
|
import { t as displayText } from "./bots-CGkO76qH.mjs";
|
|
13
12
|
//#region src/sync/engine.ts
|
|
14
13
|
/** `files`: a message with files may have no text. */
|
|
@@ -44,18 +43,6 @@ const linkKey = (link) => JSON.stringify([
|
|
|
44
43
|
]);
|
|
45
44
|
/** Whether two link lists say the same, whatever order their keys were written in. */
|
|
46
45
|
const sameLinks = (a, b) => a.length === b.length && a.every((link, i) => linkKey(link) === linkKey(b[i]));
|
|
47
|
-
/**
|
|
48
|
-
* A one-time code for linking an account: a platform message that is only a live
|
|
49
|
-
* code is claimed, not imported.
|
|
50
|
-
*/
|
|
51
|
-
const CODE = /^\s*`?(vb-[a-z0-9]{4}-[a-z0-9]{4})`?\s*$/i;
|
|
52
|
-
/**
|
|
53
|
-
* How long a board message written by someone who posts as themselves may still be
|
|
54
|
-
* on its way out: their copy comes back through the webhook looking like any of
|
|
55
|
-
* their messages, and until its receipt lands, it's held back rather than imported twice.
|
|
56
|
-
*/
|
|
57
|
-
const ECHO_WINDOW_MS = 6e4;
|
|
58
|
-
const ECHO_ATTEMPTS = 6;
|
|
59
46
|
/** Tries per file before it's noted instead: a retry repeats the whole message, so keep it short. */
|
|
60
47
|
const FILE_ATTEMPTS = 3;
|
|
61
48
|
/** How long a webhook waits for the board's tables before it lets an event into the inbox anyway: platforms resend after about 3 seconds. */
|
|
@@ -153,23 +140,16 @@ function createSyncEngine(entries) {
|
|
|
153
140
|
/** The actors sync renders with, as the board has them now. */
|
|
154
141
|
async function actorsNow() {
|
|
155
142
|
const { actors } = await call("actors", {});
|
|
156
|
-
const state = {
|
|
157
|
-
|
|
158
|
-
|
|
143
|
+
const state = { byId: {} };
|
|
144
|
+
for (const actor of actors) state.byId[actor.id] = {
|
|
145
|
+
id: actor.id,
|
|
146
|
+
kind: actor.kind,
|
|
147
|
+
handle: actor.handle,
|
|
148
|
+
name: actor.name,
|
|
149
|
+
avatar: actor.avatar
|
|
159
150
|
};
|
|
160
|
-
for (const { person, ...actor } of actors) {
|
|
161
|
-
state.byId[actor.id] = {
|
|
162
|
-
id: actor.id,
|
|
163
|
-
kind: actor.kind,
|
|
164
|
-
handle: actor.handle,
|
|
165
|
-
name: actor.name,
|
|
166
|
-
avatar: actor.avatar
|
|
167
|
-
};
|
|
168
|
-
if (person) state.links[actor.id] = person.id;
|
|
169
|
-
}
|
|
170
151
|
return state;
|
|
171
152
|
}
|
|
172
|
-
const linkedTo = (actors, person) => Object.entries(actors.links).filter(([, to]) => to === person).map(([actor]) => actor);
|
|
173
153
|
/** How long a name that didn't resolve (say, a Slack channel that doesn't exist yet) waits before another try. */
|
|
174
154
|
const RETRY_MS = 6e4;
|
|
175
155
|
const resolutions = /* @__PURE__ */ new Map();
|
|
@@ -331,10 +311,11 @@ function createSyncEngine(entries) {
|
|
|
331
311
|
* message the board has (it was synced) in that same channel is that board message; otherwise
|
|
332
312
|
* it's what the platform sent about it (Slack's unfurl, which the platform checked against who
|
|
333
313
|
* posted the link), what an earlier version of this message had, or what the platform says when
|
|
334
|
-
* asked
|
|
335
|
-
* readers may not read that one. A link
|
|
314
|
+
* asked about it and `linker`, the platform message that links it. A board copy in another channel
|
|
315
|
+
* is never one: sync reads every channel, and the link's readers may not read that one. A link
|
|
316
|
+
* nobody can read says why.
|
|
336
317
|
*/
|
|
337
|
-
async function resolveLinks(platform, links, channel, previous = []) {
|
|
318
|
+
async function resolveLinks(platform, links, linker, channel, previous = []) {
|
|
338
319
|
if (links.length === 0) return [];
|
|
339
320
|
const resolved = [];
|
|
340
321
|
for (const link of links) {
|
|
@@ -375,7 +356,7 @@ function createSyncEngine(entries) {
|
|
|
375
356
|
resolved.push(kept);
|
|
376
357
|
continue;
|
|
377
358
|
}
|
|
378
|
-
const fetched = await platform.fetchLink(link).catch((error) => ({
|
|
359
|
+
const fetched = await platform.fetchLink(link, linker).catch((error) => ({
|
|
379
360
|
snapshot: null,
|
|
380
361
|
unavailable: describe(error)
|
|
381
362
|
}));
|
|
@@ -466,9 +447,7 @@ function createSyncEngine(entries) {
|
|
|
466
447
|
if (copied !== null) {
|
|
467
448
|
const origin = (await messageOf(copied))?.origin;
|
|
468
449
|
if (!origin || origin.message !== event.message || (origin.subthread ?? origin.thread) !== (event.subthread ?? event.thread)) return;
|
|
469
|
-
}
|
|
470
|
-
const code = event.author.isBot ? void 0 : CODE.exec(event.text)?.[1];
|
|
471
|
-
if (code && await claimCode(key, event, code)) return;
|
|
450
|
+
}
|
|
472
451
|
}
|
|
473
452
|
const author = "author" in event ? event.author : {
|
|
474
453
|
id: `${platform.platform}:system`,
|
|
@@ -559,86 +538,6 @@ function createSyncEngine(entries) {
|
|
|
559
538
|
}
|
|
560
539
|
};
|
|
561
540
|
}
|
|
562
|
-
/**
|
|
563
|
-
* Whether a board message by the person behind `event`'s author (an account they
|
|
564
|
-
* signed in with) may be on its way there as them, without a receipt yet: then
|
|
565
|
-
* `event` could be that very copy. In a thread the board knows (`thread`), that's
|
|
566
|
-
* any of the person's messages still going to it; anywhere else, a new thread of
|
|
567
|
-
* theirs still going to `event`'s channel, whose platform thread the board learns
|
|
568
|
-
* only from the receipt.
|
|
569
|
-
*/
|
|
570
|
-
async function deliveringAsPerson(thread, event) {
|
|
571
|
-
const person = (await find({ account: event.author.id })).person;
|
|
572
|
-
if (!person || person.method !== "consent") return false;
|
|
573
|
-
const now = Date.now();
|
|
574
|
-
const pending = (message, target, copies) => message.by.principal === person.id && message.by.profile === null && message.from === null && !message.deleted && !message.noSync && now - Date.parse(message.createdAt) < ECHO_WINDOW_MS && !copies[target] && !message.undelivered[target];
|
|
575
|
-
if (thread !== void 0) {
|
|
576
|
-
const synced = await syncOf(thread);
|
|
577
|
-
if (!synced) return false;
|
|
578
|
-
const targets = synced.targets.filter((target) => synced.external[target] === event.thread);
|
|
579
|
-
const messages = await threadOf(synced.root);
|
|
580
|
-
const copies = await copiesOf(messages.map((message) => message.id));
|
|
581
|
-
return messages.some((message) => targets.some((target) => pending(message, target, copies[message.id] ?? {})));
|
|
582
|
-
}
|
|
583
|
-
const { roots } = await call("syncRoots", {
|
|
584
|
-
by: person.id,
|
|
585
|
-
since: now - ECHO_WINDOW_MS
|
|
586
|
-
});
|
|
587
|
-
const copies = await copiesOf(roots.map((root) => root.id));
|
|
588
|
-
for (const root of roots) {
|
|
589
|
-
const synced = await syncOf(root.thread);
|
|
590
|
-
if (synced?.targets.includes(event.channel) && !synced.external[event.channel] && pending(root, event.channel, copies[root.id] ?? {})) return true;
|
|
591
|
-
}
|
|
592
|
-
return false;
|
|
593
|
-
}
|
|
594
|
-
/** A live code, posted from `event`'s account through the entry it was issued for: link them. */
|
|
595
|
-
async function claimCode(key, event, code) {
|
|
596
|
-
try {
|
|
597
|
-
await call("syncAccount", {
|
|
598
|
-
sync: key,
|
|
599
|
-
account: event.author,
|
|
600
|
-
code,
|
|
601
|
-
message: externalMessageKey(event.subthread ?? event.thread, event.message)
|
|
602
|
-
});
|
|
603
|
-
} catch (error) {
|
|
604
|
-
if (missing(error)) return false;
|
|
605
|
-
throw error;
|
|
606
|
-
}
|
|
607
|
-
await platformOf(key).react(native(event.subthread ?? event.thread), event.message, "check", true).catch((error) => console.warn(`[board] couldn't confirm a linking code on ${key}:`, describe(error)));
|
|
608
|
-
return true;
|
|
609
|
-
}
|
|
610
|
-
/** Whether entry `key` signs people in: its sign-in links the account, and lets the board post as them. */
|
|
611
|
-
const signsIn = (key) => platformOf(key).users !== null;
|
|
612
|
-
/**
|
|
613
|
-
* A sign-in on entry `key` for `subject`, the person a code was issued to: the platform's URL,
|
|
614
|
-
* which sends them to `callback` once they've approved.
|
|
615
|
-
*/
|
|
616
|
-
async function signIn(key, subject, callback) {
|
|
617
|
-
const platform = platformOf(key);
|
|
618
|
-
if (!platform.users) throw badRequest(`${key} can't sign people in: link it with a code`);
|
|
619
|
-
return platform.users.authorize(subject, callback ? { callback } : {});
|
|
620
|
-
}
|
|
621
|
-
/** The account `subject` signed in with on entry `key`, or null while they haven't. */
|
|
622
|
-
async function signedIn(key, subject) {
|
|
623
|
-
const platform = platformOf(key);
|
|
624
|
-
if (!platform.users) throw badRequest(`${key} can't sign people in: link it with a code`);
|
|
625
|
-
const token = await platform.users.token(subject);
|
|
626
|
-
if (!token) return null;
|
|
627
|
-
const account = await platform.whoAmI(token);
|
|
628
|
-
if (!account.id.startsWith(`${platform.platform}:user:`)) throw badRequest(`${key} named an account that isn't a ${platform.platform} user`);
|
|
629
|
-
return account;
|
|
630
|
-
}
|
|
631
|
-
/** Links the account `subject` signed in with to them, by the code they carried through: null while they haven't. */
|
|
632
|
-
async function redeemSignIn(key, subject, code) {
|
|
633
|
-
const account = await signedIn(key, subject);
|
|
634
|
-
if (!account) return null;
|
|
635
|
-
return (await call("syncAccount", {
|
|
636
|
-
sync: key,
|
|
637
|
-
account,
|
|
638
|
-
code,
|
|
639
|
-
method: "consent"
|
|
640
|
-
})).link;
|
|
641
|
-
}
|
|
642
541
|
async function applyIncoming(key, item, meta, channel, overrides) {
|
|
643
542
|
const { event, raw } = item;
|
|
644
543
|
const source = originOf(key, event);
|
|
@@ -729,7 +628,10 @@ function createSyncEngine(entries) {
|
|
|
729
628
|
attachments: [],
|
|
730
629
|
notes: []
|
|
731
630
|
};
|
|
732
|
-
const links = await resolveLinks(platform, event.links ?? [],
|
|
631
|
+
const links = await resolveLinks(platform, event.links ?? [], {
|
|
632
|
+
thread: event.subthread ?? event.thread,
|
|
633
|
+
message: event.message
|
|
634
|
+
}, synced?.channel ?? channel);
|
|
733
635
|
const base = {
|
|
734
636
|
...links.length ? { links } : {},
|
|
735
637
|
author: event.author,
|
|
@@ -833,7 +735,7 @@ function createSyncEngine(entries) {
|
|
|
833
735
|
text,
|
|
834
736
|
source: from
|
|
835
737
|
}));
|
|
836
|
-
const links = stored ? await resolveLinks(platform, event.links ?? [], stored.channel, stored.links) : [];
|
|
738
|
+
const links = stored ? await resolveLinks(platform, event.links ?? [], origin, stored.channel, stored.links) : [];
|
|
837
739
|
if (stored && !sameLinks(links, stored.links)) await call("syncImport", {
|
|
838
740
|
op: "links",
|
|
839
741
|
author,
|
|
@@ -959,63 +861,10 @@ function createSyncEngine(entries) {
|
|
|
959
861
|
return null;
|
|
960
862
|
});
|
|
961
863
|
}
|
|
962
|
-
/**
|
|
963
|
-
* How `platform` mentions an actor: its own account, its person's, or any account
|
|
964
|
-
* its person linked there. An agent is just itself.
|
|
965
|
-
*/
|
|
864
|
+
/** How `platform` mentions an actor: natively when it's the platform's own account, or null. */
|
|
966
865
|
function nativeMention(platform, actor, actors) {
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
return account ? platform.mention(account) : null;
|
|
970
|
-
};
|
|
971
|
-
const person = actors.links[actor] ?? actor;
|
|
972
|
-
const direct = native(actor) ?? (person === actor ? null : native(person));
|
|
973
|
-
if (direct) return direct;
|
|
974
|
-
for (const id of linkedTo(actors, person)) {
|
|
975
|
-
const linked = native(id);
|
|
976
|
-
if (linked) return linked;
|
|
977
|
-
}
|
|
978
|
-
return null;
|
|
979
|
-
}
|
|
980
|
-
/**
|
|
981
|
-
* A person's own token on `platform`, to post `message` as them: only for what they
|
|
982
|
-
* wrote on the board themselves, not their agents, and null when they haven't
|
|
983
|
-
* signed in. A token that can't be had is the bot's turn, never a failed delivery.
|
|
984
|
-
* A new post (`linked`) goes as them only once they've linked the account they signed
|
|
985
|
-
* in with, so its echo is recognized as theirs and `vb identity unlink` stops it. Their
|
|
986
|
-
* copies already out there stay theirs to change.
|
|
987
|
-
*/
|
|
988
|
-
async function personToken(platform, message, linked) {
|
|
989
|
-
if (!platform.users || message.from !== null || message.by.profile !== null || !message.by.principal.startsWith("vercel:user:")) return null;
|
|
990
|
-
if (linked) {
|
|
991
|
-
const { accounts } = await find({ person: message.by.principal });
|
|
992
|
-
if (!accounts.some((account) => account.method === "consent" && account.id.startsWith(`${platform.platform}:user:`))) return null;
|
|
993
|
-
}
|
|
994
|
-
try {
|
|
995
|
-
return await platform.users.token(message.by.principal);
|
|
996
|
-
} catch (error) {
|
|
997
|
-
console.warn(`[board] no ${platform.platform} token for ${message.by.principal}; posting #${message.id} as the app:`, describe(error));
|
|
998
|
-
return null;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
/** What a message says as its author: no name in front, since it's theirs. */
|
|
1002
|
-
function renderAsPerson(target, message, actors) {
|
|
1003
|
-
const platform = platformOf(keyOf(target));
|
|
1004
|
-
const markdown = textFor(platform, message, actors, displayText(message.text, message.by.actor));
|
|
1005
|
-
return { markdown: message.attachments.length > 0 && !platform.uploadsFiles ? `${markdown}\n${attachmentNotes(message)}` : markdown };
|
|
1006
|
-
}
|
|
1007
|
-
/** Posts as the message's author when they've signed in; null to post as the app. */
|
|
1008
|
-
async function postAsPerson(target, place, kind, message, actors) {
|
|
1009
|
-
const platform = platformOf(keyOf(target));
|
|
1010
|
-
if (!message.text.trim()) return null;
|
|
1011
|
-
const token = await personToken(platform, message, true);
|
|
1012
|
-
if (!token) return null;
|
|
1013
|
-
try {
|
|
1014
|
-
return await platform.postAs(token, native(place), kind, renderAsPerson(target, message, actors));
|
|
1015
|
-
} catch (error) {
|
|
1016
|
-
console.warn(`[board] couldn't post #${message.id} to ${target} as its author; posting as the app:`, describe(error));
|
|
1017
|
-
return null;
|
|
1018
|
-
}
|
|
866
|
+
const account = actors.byId[actor];
|
|
867
|
+
return account ? platform.mention(account) : null;
|
|
1019
868
|
}
|
|
1020
869
|
/** A board message as markdown for a platform, which converts it to its own format: by `renderer`, or the provider's own. */
|
|
1021
870
|
function render(key, target, message, actors, renderer) {
|
|
@@ -1087,16 +936,6 @@ function createSyncEngine(entries) {
|
|
|
1087
936
|
files = null;
|
|
1088
937
|
}
|
|
1089
938
|
else {
|
|
1090
|
-
const asPerson = await postAsPerson(target, place, "thread", message, actors);
|
|
1091
|
-
if (asPerson) {
|
|
1092
|
-
const external = {
|
|
1093
|
-
thread: place,
|
|
1094
|
-
message: asPerson.message,
|
|
1095
|
-
asUser: true
|
|
1096
|
-
};
|
|
1097
|
-
await recordDelivery(message, target, external);
|
|
1098
|
-
return external;
|
|
1099
|
-
}
|
|
1100
939
|
if (!body.markdown.trim()) throw badRequest("a renderer must return text unless the message has media");
|
|
1101
940
|
sentId = (await platform.post(native(place), "thread", body)).message;
|
|
1102
941
|
}
|
|
@@ -1115,8 +954,7 @@ function createSyncEngine(entries) {
|
|
|
1115
954
|
target,
|
|
1116
955
|
external: {
|
|
1117
956
|
thread: copy.thread,
|
|
1118
|
-
message: copy.message
|
|
1119
|
-
...copy.asUser ? { asUser: true } : {}
|
|
957
|
+
message: copy.message
|
|
1120
958
|
},
|
|
1121
959
|
...copy.files !== void 0 ? { files: copy.files } : {}
|
|
1122
960
|
});
|
|
@@ -1281,14 +1119,8 @@ function createSyncEngine(entries) {
|
|
|
1281
1119
|
const mediaOnly = !body.markdown.trim() && root.attachments.length > 0 && platform.uploadsFiles;
|
|
1282
1120
|
if (!body.markdown.trim() && !mediaOnly) throw badRequest("a renderer must return text unless the message has media");
|
|
1283
1121
|
let uploaded;
|
|
1284
|
-
let postedAsPerson = false;
|
|
1285
1122
|
const post = async () => {
|
|
1286
1123
|
if (!mediaOnly) {
|
|
1287
|
-
const asPerson = await postAsPerson(target, target, "channel", root, actors);
|
|
1288
|
-
if (asPerson) {
|
|
1289
|
-
postedAsPerson = true;
|
|
1290
|
-
return asPerson;
|
|
1291
|
-
}
|
|
1292
1124
|
if (root.attachments.length === 0 || !platform.startsThreadsWithFiles) return platform.post(native(target), "channel", body);
|
|
1293
1125
|
}
|
|
1294
1126
|
try {
|
|
@@ -1309,8 +1141,7 @@ function createSyncEngine(entries) {
|
|
|
1309
1141
|
const external = {
|
|
1310
1142
|
thread: externalThread,
|
|
1311
1143
|
message: sent.message,
|
|
1312
|
-
...uploaded !== void 0 ? { files: uploaded } : {}
|
|
1313
|
-
...postedAsPerson ? { asUser: true } : {}
|
|
1144
|
+
...uploaded !== void 0 ? { files: uploaded } : {}
|
|
1314
1145
|
};
|
|
1315
1146
|
await recordDelivery(root, target, external);
|
|
1316
1147
|
await deliverFiles(root, target, external, actors, renderer);
|
|
@@ -1381,17 +1212,9 @@ function createSyncEngine(entries) {
|
|
|
1381
1212
|
await offer(keyOf(target), event, message, target, async (renderer) => {
|
|
1382
1213
|
const platform = platformOf(keyOf(target));
|
|
1383
1214
|
const thread = native(copy.thread);
|
|
1384
|
-
|
|
1385
|
-
const token = theirs ? await personToken(platform, message, false) : null;
|
|
1386
|
-
if (theirs && !token) {
|
|
1387
|
-
console.warn(`[board] skipped ${event.type} on ${target}: #${message.id} was posted as its author, and their token is gone`);
|
|
1388
|
-
return;
|
|
1389
|
-
}
|
|
1390
|
-
if (event.type === "message.edited") if (token) await platform.editAs(token, thread, copy.message, renderAsPerson(target, message, actors));
|
|
1391
|
-
else await platform.edit(thread, copy.message, renderFor(target, message, actors, copy.files === null, renderer));
|
|
1215
|
+
if (event.type === "message.edited") await platform.edit(thread, copy.message, renderFor(target, message, actors, copy.files === null, renderer));
|
|
1392
1216
|
else if (event.type === "message.deleted") {
|
|
1393
|
-
|
|
1394
|
-
else await platform.delete(thread, copy.message);
|
|
1217
|
+
await platform.delete(thread, copy.message);
|
|
1395
1218
|
if (copy.files?.length) await platform.deleteFiles(copy.files);
|
|
1396
1219
|
} else if (event.type === "reaction.added") await platform.react(thread, copy.message, canonicalEmoji(event.payload.emoji), true);
|
|
1397
1220
|
else await platform.react(thread, copy.message, canonicalEmoji(event.payload.emoji), false);
|
|
@@ -1536,10 +1359,6 @@ function createSyncEngine(entries) {
|
|
|
1536
1359
|
findByEmail,
|
|
1537
1360
|
handleWebhook,
|
|
1538
1361
|
inboxChannel,
|
|
1539
|
-
signsIn,
|
|
1540
|
-
signIn,
|
|
1541
|
-
signedIn,
|
|
1542
|
-
redeemSignIn,
|
|
1543
1362
|
typing,
|
|
1544
1363
|
readSource,
|
|
1545
1364
|
joinSource
|
|
@@ -179,11 +179,12 @@ function createBoardClient(options) {
|
|
|
179
179
|
discovery: () => call("GET", "/", { auth: false }),
|
|
180
180
|
me: () => call("GET", "/me"),
|
|
181
181
|
channels: async () => (await call("GET", "/channels")).channels,
|
|
182
|
+
/**
|
|
183
|
+
* Makes a channel before anything posts to it, as a first post would. Needs `read` and `post`
|
|
184
|
+
* there. `created` is false when it was there already; it's left as it is.
|
|
185
|
+
*/
|
|
186
|
+
createChannel: (path) => call("POST", "/channels", { body: { path } }),
|
|
182
187
|
actors: async () => (await call("GET", "/actors")).actors,
|
|
183
|
-
/** Your linked platform accounts, and the sync entries you haven't linked (`unlinked`). */
|
|
184
|
-
identity: () => call("GET", "/identity"),
|
|
185
|
-
/** Unlinks one of your accounts, by actor ID or handle. History keeps its messages as they were. */
|
|
186
|
-
unlinkAccount: (account) => call("POST", "/identity/unlink", { body: { actor: account } }),
|
|
187
188
|
messages: (query = {}) => call("GET", `/messages${queryString(query)}`),
|
|
188
189
|
thread: (id) => call("GET", `/messages/${path(id)}`),
|
|
189
190
|
/** Your own post with the client ID you gave it, as posting it answered: 404 `post_not_found` when there's none. */
|
|
@@ -222,6 +223,26 @@ function createBoardClient(options) {
|
|
|
222
223
|
/** Stops a schedule: yours, or any with manage in its channel. `canceled` is false when it was already done or canceled. */
|
|
223
224
|
cancelSchedule: (id) => call("POST", `/schedules/${path(id)}/cancel`, { body: {} }),
|
|
224
225
|
/**
|
|
226
|
+
* Lets in callers with an OIDC token from `issuer` that carries every one of `claims`, to do
|
|
227
|
+
* what `can` says (read, post, react, and label everywhere when left out). Setting the same
|
|
228
|
+
* issuer and claims again changes its `can`: `created` says which. Needs manage in every channel.
|
|
229
|
+
*/
|
|
230
|
+
createGrant: (input) => call("POST", "/grants", { body: input }),
|
|
231
|
+
/** The board's grants. Needs manage in every channel. */
|
|
232
|
+
grants: async () => (await call("GET", "/grants")).grants,
|
|
233
|
+
/** `revoked` is false when it was already gone. Its principals' next requests are refused. */
|
|
234
|
+
revokeGrant: (id) => call("POST", `/grants/${path(id)}/revoke`, { body: {} }),
|
|
235
|
+
/**
|
|
236
|
+
* A token that acts as you, narrowed to `can`, writing as `profile` and `session` when they're
|
|
237
|
+
* given: for a widget, a sandbox, a script. `secret` shows only here. It lives 30 days, or
|
|
238
|
+
* `expires`, at most 90.
|
|
239
|
+
*/
|
|
240
|
+
createToken: (input) => call("POST", "/tokens", { body: input }),
|
|
241
|
+
/** Your live tokens, or everyone's with manage in every channel. Never their secrets. */
|
|
242
|
+
tokens: async () => (await call("GET", "/tokens")).tokens,
|
|
243
|
+
/** It and every token minted with it stop. `revoked` is false when it was already revoked. */
|
|
244
|
+
revokeToken: (id) => call("POST", `/tokens/${path(id)}/revoke`, { body: {} }),
|
|
245
|
+
/**
|
|
225
246
|
* Stores a file on the board, for a post or reply's `attachments`. Up to 4 MB goes in one
|
|
226
247
|
* request; bigger, up to 100 MB, goes straight to the board's blob store on a URL the board
|
|
227
248
|
* signs, where its store can (`no_direct_uploads` where it can't).
|
|
@@ -284,9 +305,8 @@ function createBoardClient(options) {
|
|
|
284
305
|
*/
|
|
285
306
|
label: (id, change) => call("POST", `/messages/${path(id)}/labels`, { body: change }),
|
|
286
307
|
/**
|
|
287
|
-
* Shows typing in the platform threads the message's thread is synced to
|
|
288
|
-
*
|
|
289
|
-
* nothing goes in the log. Answers the external threads that took it.
|
|
308
|
+
* Shows typing in the platform threads the message's thread is synced to.
|
|
309
|
+
* Ephemeral: nothing goes in the log. Answers the external threads that took it.
|
|
290
310
|
* `status` replaces "Typing…" on Slack ("running tests…").
|
|
291
311
|
*/
|
|
292
312
|
typing: async (id, status) => (await call("POST", `/messages/${path(id)}/typing`, { body: status ? { status } : {} })).targets,
|
|
@@ -330,31 +350,16 @@ function createBoardClient(options) {
|
|
|
330
350
|
syncReceipt: (body) => call("POST", "/sync/receipts", { body }),
|
|
331
351
|
/** A board thread's sync, by its key. */
|
|
332
352
|
syncThread: (thread) => call("GET", `/sync/threads/${path(thread)}`),
|
|
333
|
-
/** What platform IDs are on the board: a thread, a message
|
|
353
|
+
/** What platform IDs are on the board: a thread, or a message. */
|
|
334
354
|
syncFind: (query, opts = {}) => call("GET", `/sync/find${queryString(query)}`, opts.signal ? { signal: opts.signal } : {}),
|
|
335
355
|
/** Messages' copies on platforms, by message and target. */
|
|
336
356
|
syncCopies: async (messages) => (await call("GET", `/sync/copies?${messages.map((id) => `message=${encodeURIComponent(id)}`).join("&")}`)).copies,
|
|
337
|
-
/** A person's roots since `since`. */
|
|
338
|
-
syncRoots: async (by, since) => (await call("GET", `/sync/roots${queryString({
|
|
339
|
-
by,
|
|
340
|
-
since: since instanceof Date ? since.getTime() : since
|
|
341
|
-
})}`)).roots,
|
|
342
357
|
/** Pushes platform events into your inbox for sync key `key`; each platform ID lands once. */
|
|
343
358
|
inboxPush: (key, events) => call("POST", `/inbox/${path(key)}`, { body: { events } }),
|
|
344
359
|
/** Your next inbox events for `key`, leased to you until you ack them, one platform thread at a time. */
|
|
345
360
|
inboxNext: async (key, opts = {}) => (await call("GET", `/inbox/${path(key)}/next${queryString(opts)}`)).items,
|
|
346
361
|
/** How an inbox event went: done, or an error to try it again. */
|
|
347
362
|
inboxAck: (key, id, body) => call("POST", `/inbox/${path(key)}/${path(id)}/ack`, { body }),
|
|
348
|
-
/**
|
|
349
|
-
* A one-time code for linking your account on sync entry `sync`, to post from that account where
|
|
350
|
-
* the board syncs, and `signIn` when the entry signs people in: open it (only you) and approve.
|
|
351
|
-
* Only people get one, each for themselves. `identity()` shows the link once it's made.
|
|
352
|
-
*/
|
|
353
|
-
syncCode: (sync) => call("POST", "/sync/codes", { body: { sync } }),
|
|
354
|
-
/** Links a platform account to whoever a code was issued to: one posted from it, or carried through its sign-in. */
|
|
355
|
-
syncAccount: (body) => call("POST", "/sync/accounts", { body }),
|
|
356
|
-
/** Unlinks a platform account: your own, or anyone's with `sync`. */
|
|
357
|
-
syncUnlink: (account) => call("POST", "/sync/accounts/unlink", { body: { account } }),
|
|
358
363
|
/** Live events. Reconnects from the last seen index whenever the stream closes. */
|
|
359
364
|
async *events(query = {}, opts = {}) {
|
|
360
365
|
let after = query.after;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as LabelKey, p as Labels } from "./schemas-DBmUO6pr.mjs";
|
|
2
|
+
import { n as ExternalEvent, r as ExternalLocation } from "./external-D3p5T57R.mjs";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
//#region src/sync/fn.ts
|
|
4
5
|
const ROUTES = Symbol.for("board.syncRoutes");
|
|
@@ -122,8 +123,6 @@ var SyncProvider = class {
|
|
|
122
123
|
uploadsFiles;
|
|
123
124
|
startsThreadsWithFiles;
|
|
124
125
|
fn;
|
|
125
|
-
/** People's tokens, when the adapter has them: messages they write post as them. */
|
|
126
|
-
users;
|
|
127
126
|
options;
|
|
128
127
|
constructor(options, sync) {
|
|
129
128
|
if (typeof sync !== "function") throw new Error(`${options.platform} needs a sync function: what each event does, like channels({ '#dev': 'dev' })`);
|
|
@@ -131,23 +130,12 @@ var SyncProvider = class {
|
|
|
131
130
|
const routes = routesOf(sync);
|
|
132
131
|
if (routes) routed.set(this, routes);
|
|
133
132
|
this.options = options;
|
|
134
|
-
this.users = options.users ?? null;
|
|
135
133
|
this.platform = options.platform;
|
|
136
134
|
this.canStartThreads = options.canStartThreads ?? false;
|
|
137
135
|
this.singleReaction = options.singleReaction ?? false;
|
|
138
136
|
this.uploadsFiles = options.uploadsFiles ?? false;
|
|
139
137
|
this.startsThreadsWithFiles = this.uploadsFiles && (options.startsThreadsWithFiles ?? false);
|
|
140
138
|
}
|
|
141
|
-
/** The platform account a person's token belongs to. */
|
|
142
|
-
async whoAmI(_token) {
|
|
143
|
-
throw new Error(`${this.platform} can't post as a person`);
|
|
144
|
-
}
|
|
145
|
-
/** `post`, as the person whose token this is. */
|
|
146
|
-
async postAs(_token, _target, _kind, _body) {
|
|
147
|
-
throw new Error(`${this.platform} can't post as a person`);
|
|
148
|
-
}
|
|
149
|
-
async editAs(_token, _thread, _message, _body) {}
|
|
150
|
-
async deleteAs(_token, _thread, _message) {}
|
|
151
139
|
async findByEmail(_email) {
|
|
152
140
|
return null;
|
|
153
141
|
}
|
|
@@ -164,8 +152,12 @@ var SyncProvider = class {
|
|
|
164
152
|
async permalink(_event) {
|
|
165
153
|
return null;
|
|
166
154
|
}
|
|
167
|
-
/**
|
|
168
|
-
|
|
155
|
+
/**
|
|
156
|
+
* A linked message the platform didn't describe, read from the platform. `linker` is the
|
|
157
|
+
* platform message that links it, by its own IDs: what the platform says about the link may be
|
|
158
|
+
* there by now.
|
|
159
|
+
*/
|
|
160
|
+
async fetchLink(_link, _linker) {
|
|
169
161
|
return {
|
|
170
162
|
snapshot: null,
|
|
171
163
|
unavailable: `${this.platform} can't read it`
|
|
@@ -223,4 +215,75 @@ var SyncProvider = class {
|
|
|
223
215
|
}
|
|
224
216
|
};
|
|
225
217
|
//#endregion
|
|
226
|
-
|
|
218
|
+
//#region src/sync/events.ts
|
|
219
|
+
const EventBase = z.object({
|
|
220
|
+
id: z.string().min(1),
|
|
221
|
+
platform: z.string().min(1),
|
|
222
|
+
key: z.string().min(1),
|
|
223
|
+
...ExternalLocation.shape,
|
|
224
|
+
raw: z.unknown()
|
|
225
|
+
});
|
|
226
|
+
const eventTypes = {
|
|
227
|
+
message: "message",
|
|
228
|
+
edited: "message.edited",
|
|
229
|
+
deleted: "message.deleted",
|
|
230
|
+
reaction: "reaction.added",
|
|
231
|
+
lifecycle: "lifecycle"
|
|
232
|
+
};
|
|
233
|
+
function envelope(platform, key, event, raw, id) {
|
|
234
|
+
return {
|
|
235
|
+
...event,
|
|
236
|
+
...EventBase.parse({
|
|
237
|
+
id,
|
|
238
|
+
platform,
|
|
239
|
+
key,
|
|
240
|
+
channel: event.channel,
|
|
241
|
+
thread: event.thread,
|
|
242
|
+
subthread: "subthread" in event ? event.subthread : null,
|
|
243
|
+
raw
|
|
244
|
+
}),
|
|
245
|
+
type: event.kind === "lifecycle" ? event.type : event.kind === "reaction" && !event.added ? "reaction.removed" : eventTypes[event.kind]
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function eventSchema(platform, messageType) {
|
|
249
|
+
const [message, edited, deleted, reaction, lifecycle] = ExternalEvent.options;
|
|
250
|
+
const common = {
|
|
251
|
+
...EventBase.shape,
|
|
252
|
+
platform: z.literal(platform)
|
|
253
|
+
};
|
|
254
|
+
return z.discriminatedUnion("kind", [
|
|
255
|
+
message.extend({
|
|
256
|
+
...common,
|
|
257
|
+
type: z.literal(messageType)
|
|
258
|
+
}),
|
|
259
|
+
edited.extend({
|
|
260
|
+
...common,
|
|
261
|
+
type: z.literal("message.edited")
|
|
262
|
+
}),
|
|
263
|
+
deleted.extend({
|
|
264
|
+
...common,
|
|
265
|
+
type: z.literal("message.deleted")
|
|
266
|
+
}),
|
|
267
|
+
reaction.extend({
|
|
268
|
+
...common,
|
|
269
|
+
type: z.enum(["reaction.added", "reaction.removed"])
|
|
270
|
+
}),
|
|
271
|
+
lifecycle.extend(common)
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
const SyncOverrides = z.object({
|
|
275
|
+
root: z.custom((value) => typeof value === "function").optional(),
|
|
276
|
+
text: z.string().optional(),
|
|
277
|
+
labels: Labels.optional(),
|
|
278
|
+
unset: z.array(LabelKey).optional(),
|
|
279
|
+
noSync: z.boolean().optional()
|
|
280
|
+
}).strict();
|
|
281
|
+
function immutable(value) {
|
|
282
|
+
if (value && typeof value === "object") {
|
|
283
|
+
for (const item of Object.values(value)) immutable(item);
|
|
284
|
+
Object.freeze(value);
|
|
285
|
+
}
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
//#endregion
|
|
289
|
+
export { immutable as a, SyncProvider as c, routesFor as d, channels as f, withRoutes as g, render as h, eventSchema as i, UnreadableError as l, mirror as m, SyncOverrides as n, ReceivedEvent as o, into as p, envelope as r, ResolvedTarget as s, EventBase as t, channelMissing as u };
|