@vercel/board 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -17
- package/dist/{a2-HUbQJ0RD.mjs → a2-DngWY2Eb.mjs} +741 -415
- package/dist/access-CY5xVMIK.mjs +242 -0
- package/dist/{access-CV96SYgr.d.mts → access-DPcrKIH_.d.mts} +6 -1
- package/dist/access.d.mts +4 -4
- package/dist/access.mjs +5 -4
- package/dist/{actors-DcJhFbwJ.mjs → actors-Bm-ojWWT.mjs} +2 -2
- package/dist/{auth-JO22iIwr.mjs → auth-CyO2aw5n.mjs} +126 -236
- package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
- package/dist/{backend-CPC_vyOo.d.mts → backend-Bob-5CM7.d.mts} +93 -42
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +38 -98
- package/dist/backend-a2/contract.mjs +1 -1
- package/dist/backend-a2/scheduler-local.d.mts +1 -1
- package/dist/backend-a2/scheduler-local.mjs +1 -1
- package/dist/backend-a2/scheduler-qstash.d.mts +3 -3
- package/dist/backend-a2/scheduler-qstash.mjs +8 -8
- package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
- package/dist/backend-a2/scheduler-vercel.mjs +6 -6
- package/dist/backend-a2/store-memory.d.mts +2 -2
- package/dist/backend-a2/store-memory.mjs +3 -3
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +34 -15
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +6 -6
- package/dist/backend-a2/store-redis.d.mts +2 -2
- package/dist/backend-a2/store-redis.mjs +5 -5
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +3 -3
- package/dist/backend-a2.d.mts +143 -242
- package/dist/backend-a2.mjs +2 -2
- package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-BRrRbjRx.d.mts} +59 -45
- package/dist/{backend-http-CzIJMm58.mjs → backend-http-CvUKOn-m.mjs} +11 -10
- package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-D8mUzuKQ.mjs} +2 -2
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-vercel-DQc_TaHp.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
- package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-BaSEUdwg.d.mts} +62 -47
- package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-CmY9yZ9m.mjs} +279 -42
- package/dist/backend-vercel.d.mts +1 -1
- package/dist/backend-vercel.mjs +1 -1
- package/dist/backend-vercel.node.d.mts +1 -1
- package/dist/backend-vercel.node.mjs +1 -1
- package/dist/{blob-DmiQjrYy.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
- package/dist/{blob-DdbpJqN1.mjs → blob-WbB2DJKn.mjs} +30 -11
- package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-BChYrrA7.d.mts} +32 -28
- package/dist/{chat-provider-COtsU5jB.mjs → chat-provider-DkFp2hrh.mjs} +105 -60
- package/dist/cli-commands.d.mts +1 -1
- package/dist/cli-commands.mjs +1 -1
- package/dist/cli-embedded.d.mts +2 -2
- package/dist/cli-embedded.mjs +1 -1
- package/dist/cli.mjs +4 -4
- package/dist/{client-BqAjk0Q4.d.mts → client-COfYrgfV.d.mts} +240 -227
- package/dist/{commands-D6gvrOh3.d.mts → commands-DK4W3HCJ.d.mts} +47 -10
- package/dist/{commands-C3R1AWgm.mjs → commands-L1JSGz2Z.mjs} +212 -54
- package/dist/conformance-DK43HiHf.mjs +1000 -0
- package/dist/conformance.d.mts +8 -2
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-DQxXk7dX.mjs → engine-DurYIRt8.mjs} +29 -210
- package/dist/{errors-DDktVeUa.d.mts → errors-2vGs49Db.d.mts} +1 -1
- package/dist/{client-D0Px7KzF.mjs → errors-CJXhhzvT.mjs} +132 -27
- package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
- package/dist/{exports-BG-0Wyn-.mjs → exports-CWIdN0Ub.mjs} +195 -103
- package/dist/exports-FBOQWT7O.d.mts +5 -0
- package/dist/{folds-BDMSxBUf.mjs → folds-Cu9Q9zyQ.mjs} +3 -22
- package/dist/{folds-ZkHM4M-v.d.mts → folds-DLUzfibJ.d.mts} +15 -40
- package/dist/{index-D3VvNfh_.d.mts → index-Ho-ayyQL.d.mts} +2 -2
- package/dist/index.d.mts +7 -7
- package/dist/{index.default-rB8sJ9RX.mjs → index.default-BUcwDaAe.mjs} +1 -1
- package/dist/index.mjs +5 -6
- package/dist/index.node.d.mts +7 -7
- package/dist/index.node.mjs +3 -4
- package/dist/inspect/app.js +62 -61
- package/dist/inspect/engine.js +3 -3
- package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
- package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
- package/dist/{main-CVBqe3VO.mjs → main-Dz-8komG.mjs} +251 -127
- package/dist/{middleware-CXjEeef4.mjs → middleware-BavYSDUa.mjs} +1 -1
- package/dist/{node-Zd_C-gxP.mjs → node-BgeGcwX0.mjs} +7 -3
- package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
- package/dist/{project-CBXlLftU.mjs → project-CQ4VUibA.mjs} +1 -1
- package/dist/{provider-DmSTp9lI.d.mts → provider-gynDUoyW.d.mts} +13 -43
- package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-DHf85ixI.mjs} +40 -42
- package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
- package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
- package/dist/{reducers-DJzVpXwM.mjs → reducers-DcENwtwZ.mjs} +74 -77
- package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
- package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
- package/dist/{serve-G9RuaDSr.mjs → serve-CTmow-YH.mjs} +7 -7
- package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
- package/dist/{server-BixV_byP.mjs → server-DG8D6VV0.mjs} +4 -4
- package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
- package/dist/{serving-B75Y4BF-.mjs → serving-DBFRo6U_.mjs} +1 -1
- package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
- package/dist/{sql-D4Z_efJX.mjs → sql-CT5xVuGr.mjs} +2 -8
- package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
- package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
- package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
- package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
- package/dist/store-memory-4IBl0Ztu.mjs +2 -0
- package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
- package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
- package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
- package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
- package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
- package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
- package/dist/{sync-kapso.d.mts → sync-chat-sdk.d.mts} +33 -37
- package/dist/sync-chat-sdk.mjs +44 -0
- package/dist/sync-github.d.mts +5 -18
- package/dist/sync-github.mjs +4 -70
- package/dist/sync-linear.d.mts +3 -3
- package/dist/sync-linear.mjs +6 -5
- package/dist/sync-slack.d.mts +7 -7
- package/dist/sync-slack.mjs +33 -54
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +2 -4
- package/dist/{tokens-CC3izSNr.mjs → tokens-Dnyd_y1n.mjs} +15 -6
- package/dist/{types-No0WdhE5.d.mts → types-CmrFjpml.d.mts} +2 -4
- package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-BjwlnGD9.mjs} +124 -115
- package/docs/01-board/01-mounting.md +2 -2
- package/docs/01-board/03-identity.md +5 -6
- package/docs/01-board/04-messages.md +96 -2
- package/docs/01-board/05-reading.md +2 -1
- package/docs/01-board/06-watchers.md +8 -6
- package/docs/01-board/08-middleware.md +6 -3
- package/docs/01-board/09-access.md +145 -14
- package/docs/02-sync/01-overview.md +19 -26
- package/docs/02-sync/02-functions.md +8 -9
- package/docs/02-sync/03-slack.md +5 -5
- package/docs/02-sync/04-github.md +2 -2
- package/docs/02-sync/06-providers.md +91 -62
- package/docs/03-guides/01-coding-agents.md +1 -1
- package/docs/03-guides/03-ci.md +7 -4
- package/docs/04-reference/01-cli.md +109 -51
- package/docs/04-reference/02-board.md +76 -28
- package/docs/04-reference/03-http.md +76 -38
- package/docs/04-reference/04-log.md +8 -11
- package/docs/04-reference/05-conformance.md +47 -7
- package/docs/index.md +2 -2
- package/package.json +7 -10
- package/sql/postgres/board.sql +1 -1
- package/sql/postgres/schema.sql +1 -1
- package/dist/conformance-DlnfuzEB.mjs +0 -556
- package/dist/errors-CusN4x2l.mjs +0 -88
- package/dist/events-qf5XG5VQ.mjs +0 -75
- package/dist/exports-mhAYiU4i.d.mts +0 -5
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
- package/dist/store-memory-ZLiZbH3R.mjs +0 -2
- package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
- package/dist/sync-kapso.mjs +0 -89
- package/dist/sync.chat.d.mts +0 -2
- package/dist/sync.chat.mjs +0 -2
- package/dist/users-yryHKFMo.mjs +0 -42
- package/docs/02-sync/05-whatsapp.md +0 -152
package/dist/conformance.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { f as Board } from "./backend-Bob-5CM7.mjs";
|
|
2
2
|
//#region src/conformance/index.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Checks that a board speaks the protocol, through a handle on it (`conformance(board())`), as
|
|
@@ -7,7 +7,7 @@ import { d as Board } from "./backend-CPC_vyOo.mjs";
|
|
|
7
7
|
* (`conformance` by default), keeps every post on the board, and deletes what it posted.
|
|
8
8
|
*/
|
|
9
9
|
/** What a check is about, for the report's headings. */
|
|
10
|
-
export type CheckGroup = 'core' | 'schedules' | 'attachments' | 'watchers' | 'sync';
|
|
10
|
+
export type CheckGroup = 'core' | 'schedules' | 'grants' | 'tokens' | 'attachments' | 'watchers' | 'sync';
|
|
11
11
|
export type CheckResult = {
|
|
12
12
|
group: CheckGroup;
|
|
13
13
|
name: string;
|
|
@@ -24,6 +24,12 @@ export type ConformanceReport = {
|
|
|
24
24
|
export type ConformanceOptions = {
|
|
25
25
|
/** Where it posts. Default `conformance`. */
|
|
26
26
|
channel?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The same board, with a board token's secret as the credential (as `BOARD_TOKEN` sends it):
|
|
29
|
+
* the tokens check calls it so to see that a token does only what its `can` says. Without it,
|
|
30
|
+
* that check is skipped.
|
|
31
|
+
*/
|
|
32
|
+
withToken?: (secret: string) => Board;
|
|
27
33
|
};
|
|
28
34
|
/** Runs the checks against `board`, in order, and reports each. It never throws for what the board answers. */
|
|
29
35
|
export declare function conformance(board: Board, options?: ConformanceOptions): Promise<ConformanceReport>;
|
package/dist/conformance.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as conformance } from "./conformance-
|
|
1
|
+
import { t as conformance } from "./conformance-DK43HiHf.mjs";
|
|
2
2
|
export { conformance };
|
|
@@ -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-CJXhhzvT.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-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
|
|
@@ -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) {
|
|
@@ -178,11 +179,12 @@ function createBoardClient(options) {
|
|
|
178
179
|
discovery: () => call("GET", "/", { auth: false }),
|
|
179
180
|
me: () => call("GET", "/me"),
|
|
180
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 } }),
|
|
181
187
|
actors: async () => (await call("GET", "/actors")).actors,
|
|
182
|
-
/** Your linked platform accounts, and the sync entries you haven't linked (`unlinked`). */
|
|
183
|
-
identity: () => call("GET", "/identity"),
|
|
184
|
-
/** Unlinks one of your accounts, by actor ID or handle. History keeps its messages as they were. */
|
|
185
|
-
unlinkAccount: (account) => call("POST", "/identity/unlink", { body: { actor: account } }),
|
|
186
188
|
messages: (query = {}) => call("GET", `/messages${queryString(query)}`),
|
|
187
189
|
thread: (id) => call("GET", `/messages/${path(id)}`),
|
|
188
190
|
/** Your own post with the client ID you gave it, as posting it answered: 404 `post_not_found` when there's none. */
|
|
@@ -221,6 +223,26 @@ function createBoardClient(options) {
|
|
|
221
223
|
/** Stops a schedule: yours, or any with manage in its channel. `canceled` is false when it was already done or canceled. */
|
|
222
224
|
cancelSchedule: (id) => call("POST", `/schedules/${path(id)}/cancel`, { body: {} }),
|
|
223
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
|
+
/**
|
|
224
246
|
* Stores a file on the board, for a post or reply's `attachments`. Up to 4 MB goes in one
|
|
225
247
|
* request; bigger, up to 100 MB, goes straight to the board's blob store on a URL the board
|
|
226
248
|
* signs, where its store can (`no_direct_uploads` where it can't).
|
|
@@ -230,10 +252,13 @@ function createBoardClient(options) {
|
|
|
230
252
|
const size = input.data instanceof Blob ? input.data.size : input.data.byteLength;
|
|
231
253
|
if (size > 4194304) {
|
|
232
254
|
if (size > 104857600) throw new BoardError("too_large", `Attachments are at most ${DIRECT_UPLOAD_BYTES_MAX / 1024 / 1024} MB.`, 413);
|
|
255
|
+
const bytes = input.data instanceof Blob ? await input.data.arrayBuffer() : input.data;
|
|
256
|
+
const sha256 = btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))));
|
|
233
257
|
const ticket = await call("POST", "/attachments/uploads", { body: {
|
|
234
258
|
name: input.name,
|
|
235
259
|
type,
|
|
236
|
-
size
|
|
260
|
+
size,
|
|
261
|
+
sha256
|
|
237
262
|
} }).catch((error) => {
|
|
238
263
|
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
264
|
throw error;
|
|
@@ -260,18 +285,28 @@ function createBoardClient(options) {
|
|
|
260
285
|
},
|
|
261
286
|
/** An attachment's bytes, as the board serves them: `content-type` is the file's type. */
|
|
262
287
|
download: (id, opts = {}) => raw("GET", `/attachments/${encodeURIComponent(id)}`, opts.signal ? { signal: opts.signal } : {}),
|
|
263
|
-
|
|
264
|
-
|
|
288
|
+
/**
|
|
289
|
+
* `revision` is the message's as you read it: if it's been edited or deleted since, nothing
|
|
290
|
+
* changes, and it throws a 409 `revision_conflict`.
|
|
291
|
+
*/
|
|
292
|
+
edit: (id, text, opts = {}) => call("POST", `/messages/${path(id)}/edit`, { body: {
|
|
293
|
+
text,
|
|
294
|
+
...opts
|
|
295
|
+
} }),
|
|
296
|
+
delete: (id, opts = {}) => call("POST", `/messages/${path(id)}/delete`, { body: { ...opts } }),
|
|
265
297
|
/** `emoji` is a short name, `:name:`, or the character itself. */
|
|
266
298
|
react: (id, emoji, opts = {}) => call("POST", `/messages/${path(id)}/reactions`, { body: {
|
|
267
299
|
emoji: normalizeEmoji(emoji),
|
|
268
300
|
...opts
|
|
269
301
|
} }),
|
|
302
|
+
/**
|
|
303
|
+
* `if` is what the labels must be now, a value or null for one that mustn't be set: if one
|
|
304
|
+
* isn't, nothing changes, and it throws a 409 `label_conflict`.
|
|
305
|
+
*/
|
|
270
306
|
label: (id, change) => call("POST", `/messages/${path(id)}/labels`, { body: change }),
|
|
271
307
|
/**
|
|
272
|
-
* Shows typing in the platform threads the message's thread is synced to
|
|
273
|
-
*
|
|
274
|
-
* 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.
|
|
275
310
|
* `status` replaces "Typing…" on Slack ("running tests…").
|
|
276
311
|
*/
|
|
277
312
|
typing: async (id, status) => (await call("POST", `/messages/${path(id)}/typing`, { body: status ? { status } : {} })).targets,
|
|
@@ -315,31 +350,16 @@ function createBoardClient(options) {
|
|
|
315
350
|
syncReceipt: (body) => call("POST", "/sync/receipts", { body }),
|
|
316
351
|
/** A board thread's sync, by its key. */
|
|
317
352
|
syncThread: (thread) => call("GET", `/sync/threads/${path(thread)}`),
|
|
318
|
-
/** What platform IDs are on the board: a thread, a message
|
|
353
|
+
/** What platform IDs are on the board: a thread, or a message. */
|
|
319
354
|
syncFind: (query, opts = {}) => call("GET", `/sync/find${queryString(query)}`, opts.signal ? { signal: opts.signal } : {}),
|
|
320
355
|
/** Messages' copies on platforms, by message and target. */
|
|
321
356
|
syncCopies: async (messages) => (await call("GET", `/sync/copies?${messages.map((id) => `message=${encodeURIComponent(id)}`).join("&")}`)).copies,
|
|
322
|
-
/** A person's roots since `since`. */
|
|
323
|
-
syncRoots: async (by, since) => (await call("GET", `/sync/roots${queryString({
|
|
324
|
-
by,
|
|
325
|
-
since: since instanceof Date ? since.getTime() : since
|
|
326
|
-
})}`)).roots,
|
|
327
357
|
/** Pushes platform events into your inbox for sync key `key`; each platform ID lands once. */
|
|
328
358
|
inboxPush: (key, events) => call("POST", `/inbox/${path(key)}`, { body: { events } }),
|
|
329
359
|
/** Your next inbox events for `key`, leased to you until you ack them, one platform thread at a time. */
|
|
330
360
|
inboxNext: async (key, opts = {}) => (await call("GET", `/inbox/${path(key)}/next${queryString(opts)}`)).items,
|
|
331
361
|
/** How an inbox event went: done, or an error to try it again. */
|
|
332
362
|
inboxAck: (key, id, body) => call("POST", `/inbox/${path(key)}/${path(id)}/ack`, { body }),
|
|
333
|
-
/**
|
|
334
|
-
* A one-time code for linking your account on sync entry `sync`, to post from that account where
|
|
335
|
-
* the board syncs, and `signIn` when the entry signs people in: open it (only you) and approve.
|
|
336
|
-
* Only people get one, each for themselves. `identity()` shows the link once it's made.
|
|
337
|
-
*/
|
|
338
|
-
syncCode: (sync) => call("POST", "/sync/codes", { body: { sync } }),
|
|
339
|
-
/** Links a platform account to whoever a code was issued to: one posted from it, or carried through its sign-in. */
|
|
340
|
-
syncAccount: (body) => call("POST", "/sync/accounts", { body }),
|
|
341
|
-
/** Unlinks a platform account: your own, or anyone's with `sync`. */
|
|
342
|
-
syncUnlink: (account) => call("POST", "/sync/accounts/unlink", { body: { account } }),
|
|
343
363
|
/** Live events. Reconnects from the last seen index whenever the stream closes. */
|
|
344
364
|
async *events(query = {}, opts = {}) {
|
|
345
365
|
let after = query.after;
|
|
@@ -405,4 +425,89 @@ async function toError(response) {
|
|
|
405
425
|
return new BoardError("http_error", `The board answered ${`${response.status} ${response.statusText}`.trim()}.`, response.status);
|
|
406
426
|
}
|
|
407
427
|
//#endregion
|
|
408
|
-
|
|
428
|
+
//#region src/server/errors.ts
|
|
429
|
+
var HttpError = class extends Error {
|
|
430
|
+
status;
|
|
431
|
+
code;
|
|
432
|
+
/** What fixes it, when there's one (`BoardError`'s `hint`). */
|
|
433
|
+
hint;
|
|
434
|
+
/** `handle` for a handle's own error on its way through: everything else is the board's. */
|
|
435
|
+
origin;
|
|
436
|
+
constructor(status, code, message, options = {}) {
|
|
437
|
+
super(message);
|
|
438
|
+
this.status = status;
|
|
439
|
+
this.code = code;
|
|
440
|
+
this.hint = options.hint;
|
|
441
|
+
this.origin = options.origin ?? "board";
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
/** A request a host's own verifier rejected with `response`, which the board answers with as it is. */
|
|
445
|
+
var RejectedError = class extends HttpError {
|
|
446
|
+
response;
|
|
447
|
+
constructor(response) {
|
|
448
|
+
super(response.status, "rejected", `rejected with ${response.status}`);
|
|
449
|
+
this.response = response;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
const badRequest = (message, code = "invalid_request") => new HttpError(400, code, message);
|
|
453
|
+
const unauthorized = (message) => new HttpError(401, "unauthorized", message);
|
|
454
|
+
const forbidden = (message) => new HttpError(403, "forbidden", message);
|
|
455
|
+
const notFound = (message, code = "not_found") => new HttpError(404, code, message);
|
|
456
|
+
const duplicateId = () => new HttpError(409, "duplicate_id", "that id was already used for a different write");
|
|
457
|
+
const json = (body, status = 200, headers) => new Response(JSON.stringify(body), {
|
|
458
|
+
status,
|
|
459
|
+
headers: {
|
|
460
|
+
"content-type": "application/json",
|
|
461
|
+
...headers
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
/**
|
|
465
|
+
* The issue a request is told about. A union that matched no option says only "Invalid input", so
|
|
466
|
+
* it's the first issue of the option that came closest, the one with the fewest, at its own path.
|
|
467
|
+
*/
|
|
468
|
+
function reported(issue) {
|
|
469
|
+
if (issue.code !== "invalid_union" || issue.errors.length === 0) return issue;
|
|
470
|
+
const first = issue.errors.reduce((best, option) => option.length < best.length ? option : best)[0];
|
|
471
|
+
return first ? reported({
|
|
472
|
+
...first,
|
|
473
|
+
path: [...issue.path, ...first.path]
|
|
474
|
+
}) : issue;
|
|
475
|
+
}
|
|
476
|
+
/** Every failure becomes `{ error: { code, message, hint? } }` with a matching status. */
|
|
477
|
+
function toErrorResponse(error) {
|
|
478
|
+
if (error instanceof RejectedError) return error.response;
|
|
479
|
+
if (error instanceof HttpError) {
|
|
480
|
+
const response = json({ error: {
|
|
481
|
+
code: error.code,
|
|
482
|
+
message: error.message,
|
|
483
|
+
...error.hint !== void 0 ? { hint: error.hint } : {}
|
|
484
|
+
} }, error.status);
|
|
485
|
+
if (error.origin === "handle") handleErrors.add(response);
|
|
486
|
+
return response;
|
|
487
|
+
}
|
|
488
|
+
if (error instanceof ZodError) {
|
|
489
|
+
const issue = error.issues[0] && reported(error.issues[0]);
|
|
490
|
+
const path = issue?.path.length ? `${issue.path.join(".")}: ` : "";
|
|
491
|
+
return json({ error: {
|
|
492
|
+
code: "invalid_request",
|
|
493
|
+
message: `${path}${issue?.message ?? "invalid request"}`
|
|
494
|
+
} }, 400);
|
|
495
|
+
}
|
|
496
|
+
const code = error?.code;
|
|
497
|
+
if (code === "INVALID_PAYLOAD") return json({ error: {
|
|
498
|
+
code: "invalid_request",
|
|
499
|
+
message: error.message
|
|
500
|
+
} }, 400);
|
|
501
|
+
if (code === "PARTIAL_DUPLICATE_BATCH" || code === "DUPLICATE_EVENT_ID") return toErrorResponse(duplicateId());
|
|
502
|
+
if (code === "STORE_UNAVAILABLE" || code === "STORE_NOT_CONFIGURED") return json({ error: {
|
|
503
|
+
code: "unavailable",
|
|
504
|
+
message: error.message
|
|
505
|
+
} }, 503);
|
|
506
|
+
console.error("[board]", error);
|
|
507
|
+
return json({ error: {
|
|
508
|
+
code: "internal",
|
|
509
|
+
message: "internal error"
|
|
510
|
+
} }, 500);
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
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 };
|