@vercel/board 0.16.0 → 0.17.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 +27 -27
- package/dist/{a2-C41hD8SF.mjs → a2-Bck5x1Kh.mjs} +969 -25
- package/dist/access.d.mts +1 -1
- package/dist/access.mjs +3 -3
- package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
- package/dist/attempts-CLG840Oj.mjs +20 -0
- package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
- package/dist/backend-B-SJ7p6p.d.mts +680 -0
- package/dist/backend-a2/contract.d.mts +873 -2
- package/dist/backend-a2/contract.mjs +2 -2
- 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 +78 -1
- package/dist/backend-a2/scheduler-qstash.mjs +521 -1
- package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
- package/dist/backend-a2/scheduler-vercel.mjs +203 -1
- 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 +2850 -2
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +490 -2
- package/dist/backend-a2/store-redis.d.mts +10 -2
- package/dist/backend-a2/store-redis.mjs +51 -2
- 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 +2706 -3
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
- package/dist/{backend-http-0roEmjl0.mjs → backend-http-C5I7Wfdw.mjs} +2 -2
- package/dist/{backend-http-BVRno-ca.mjs → backend-http-CuUIACCs.mjs} +32 -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/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
- package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
- package/dist/cli.mjs +30 -10
- package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
- package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
- package/dist/events-CewD4kw7.mjs +75 -0
- package/dist/{exports-Dvc1o52I.mjs → exports-DOdA7KhF.mjs} +35 -32
- package/dist/exports-ZsgjzEnM.d.mts +4 -0
- package/dist/external-BV_cUncs.mjs +151 -0
- package/dist/folds-CRYY14kc.d.mts +534 -0
- package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
- package/dist/ids-C6AYo1bu.mjs +31 -0
- package/dist/index.d.mts +4 -4
- package/dist/{index.default-Bkg77Pxn.mjs → index.default-n4qKN7EL.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.node.d.mts +4 -4
- package/dist/index.node.mjs +3 -3
- package/dist/inspect/app.js +31 -31
- package/dist/inspect/engine.js +5 -5
- package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
- package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
- package/dist/mentions-CB6ShY4R.mjs +72 -0
- package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
- package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
- package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
- package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
- package/dist/provider-B3DpYSDq.mjs +220 -0
- package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
- package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
- package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
- package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
- package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
- package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
- package/dist/{serve-BsOOc-6_.mjs → serve-CNYvA4pT.mjs} +5 -5
- package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
- package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
- package/dist/{server-jwPY7_wi.mjs → server-snYFkITb.mjs} +4 -4
- package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
- package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
- package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
- package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
- package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
- package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
- package/dist/store-memory-Bas9Uq5C.mjs +872 -0
- package/dist/store-memory-ZLiZbH3R.mjs +2 -0
- package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
- package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
- package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
- package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
- package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
- package/dist/sync-github.d.mts +24 -17
- package/dist/sync-github.mjs +89 -22
- package/dist/sync-kapso.d.mts +15 -13
- package/dist/sync-kapso.mjs +17 -14
- package/dist/sync-slack.d.mts +12 -17
- package/dist/sync-slack.mjs +54 -42
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.chat.mjs +1 -1
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +5 -4
- package/dist/targets-C-wdR5wP.mjs +33 -0
- package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
- package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
- package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
- package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-DojIuoFg.mjs} +233 -28
- package/docs/01-board/01-mounting.md +1 -1
- package/docs/02-sync/01-overview.md +69 -47
- package/docs/02-sync/02-functions.md +395 -0
- package/docs/02-sync/03-slack.md +30 -25
- package/docs/02-sync/04-github.md +62 -26
- package/docs/02-sync/05-whatsapp.md +23 -19
- package/docs/02-sync/06-providers.md +58 -26
- package/docs/03-guides/04-triage.md +1 -1
- package/docs/04-reference/01-cli.md +26 -6
- package/docs/04-reference/02-board.md +18 -5
- package/docs/04-reference/03-http.md +5 -2
- package/docs/04-reference/05-conformance.md +1 -1
- package/docs/index.md +5 -10
- package/package.json +22 -6
- package/dist/a2-D24cvwpN.d.mts +0 -3359
- package/dist/access-CJU2-vaa.mjs +0 -226
- package/dist/attempts-Bxp9KgPf.mjs +0 -919
- package/dist/exports-DLiE7fLY.d.mts +0 -4
- package/dist/provider-CuewcDNZ.mjs +0 -205
- package/dist/redis-tables-B35vv4CZ.mjs +0 -842
- package/dist/reducers-B_vUGQRp.d.mts +0 -1172
- package/dist/watchers-hash-CLjsa6od.mjs +0 -225
- package/docs/02-sync/02-callbacks.md +0 -265
package/dist/sync-slack.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { m as MESSAGE_TEXT_MAX } from "./schemas-Be08nyfW.mjs";
|
|
2
|
-
import "./
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { r as
|
|
2
|
+
import "./external-BV_cUncs.mjs";
|
|
3
|
+
import { i as UnreadableError, s as channels } from "./provider-B3DpYSDq.mjs";
|
|
4
|
+
import { i as eventSchema, r as envelope } from "./events-CewD4kw7.mjs";
|
|
5
|
+
import { r as withUserTokens, t as connectUserTokens } from "./users-yryHKFMo.mjs";
|
|
6
|
+
import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-YHy-WbrB.mjs";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { stringifyMarkdown } from "chat";
|
|
8
9
|
import { createSlackAdapter } from "@chat-adapter/slack";
|
|
@@ -211,18 +212,35 @@ function slackFileName(filename, mimeType) {
|
|
|
211
212
|
}
|
|
212
213
|
const SlackEvent = eventSchema("slack", "message");
|
|
213
214
|
/**
|
|
214
|
-
* Syncs Slack
|
|
215
|
-
* (`'
|
|
216
|
-
*
|
|
217
|
-
*
|
|
215
|
+
* Syncs a Slack workspace with the board: `app` is a Vercel Connect connector
|
|
216
|
+
* (`'slack/acme'`) or the options, and `sync` is what each event does, both ways,
|
|
217
|
+
* like `channels({ '#prs': 'prs' })`. Invite the app to each channel it syncs:
|
|
218
|
+
* it never joins one on its own.
|
|
218
219
|
*/
|
|
219
|
-
function slack(
|
|
220
|
-
return new SlackProvider(
|
|
220
|
+
function slack(app, sync) {
|
|
221
|
+
return new SlackProvider(typeof app === "string" ? { adapter: app } : app, sync);
|
|
221
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Slack's client retries 10 times over about 30 minutes by default, which outlives any function:
|
|
225
|
+
* here a call gets 3 more tries. Each still waits out Slack's `Retry-After` (about a minute at
|
|
226
|
+
* most), and `timeout` caps only the request itself, at 30 seconds.
|
|
227
|
+
*/
|
|
228
|
+
const SLACK_CLIENT = {
|
|
229
|
+
retryConfig: {
|
|
230
|
+
retries: 3,
|
|
231
|
+
factor: 2,
|
|
232
|
+
minTimeout: 500,
|
|
233
|
+
randomize: true
|
|
234
|
+
},
|
|
235
|
+
timeout: 3e4
|
|
236
|
+
};
|
|
222
237
|
/** A Slack adapter, client, and people's tokens, all from one Vercel Connect connector. */
|
|
223
238
|
function fromConnector(connector) {
|
|
224
239
|
const credentials = connectSlackAdapter(connector);
|
|
225
|
-
const adapter = withUserTokens(createSlackAdapter(
|
|
240
|
+
const adapter = withUserTokens(createSlackAdapter({
|
|
241
|
+
...credentials,
|
|
242
|
+
webClientOptions: SLACK_CLIENT
|
|
243
|
+
}), connectUserTokens(connector));
|
|
226
244
|
const botToken = credentials.botToken;
|
|
227
245
|
const client = async () => adapter.withBotToken(typeof botToken === "function" ? await botToken() : botToken, () => adapter.webClient);
|
|
228
246
|
return {
|
|
@@ -233,7 +251,7 @@ function fromConnector(connector) {
|
|
|
233
251
|
var SlackProvider = class extends ChatSyncProvider {
|
|
234
252
|
web;
|
|
235
253
|
customize;
|
|
236
|
-
constructor(given) {
|
|
254
|
+
constructor(given, sync) {
|
|
237
255
|
const connected = typeof given.adapter === "string" ? fromConnector(given.adapter) : null;
|
|
238
256
|
const options = connected ? {
|
|
239
257
|
...given,
|
|
@@ -300,26 +318,6 @@ var SlackProvider = class extends ChatSyncProvider {
|
|
|
300
318
|
if (!found) throw new UnreadableError("channel_not_found", publicOnly ? `no public Slack channel named #${name} (the app lacks groups:read, so a private channel needs its ID)` : `no Slack channel named #${name} (a private channel shows up once the app is invited)`);
|
|
301
319
|
return found;
|
|
302
320
|
}
|
|
303
|
-
const joined = /* @__PURE__ */ new Map();
|
|
304
|
-
function join(client, id, known) {
|
|
305
|
-
let promise = joined.get(id);
|
|
306
|
-
if (!promise) {
|
|
307
|
-
promise = (async () => {
|
|
308
|
-
const channel = known ?? await client.conversations.info({ channel: id }).then((r) => r.channel, () => void 0);
|
|
309
|
-
if (!channel || channel.is_member) return;
|
|
310
|
-
const label = channel.name ? `#${channel.name}` : id;
|
|
311
|
-
if (channel.is_private) return console.warn(`[board] slack: invite the app to ${label}; it can't join private channels itself`);
|
|
312
|
-
try {
|
|
313
|
-
await client.conversations.join({ channel: id });
|
|
314
|
-
} catch (error) {
|
|
315
|
-
joined.delete(id);
|
|
316
|
-
console.warn(`[board] slack: couldn't join ${label} (${slackError(error)}); invite the app to it`);
|
|
317
|
-
}
|
|
318
|
-
})();
|
|
319
|
-
joined.set(id, promise);
|
|
320
|
-
}
|
|
321
|
-
return promise;
|
|
322
|
-
}
|
|
323
321
|
const avatars = /* @__PURE__ */ new Map();
|
|
324
322
|
function avatarOf(user) {
|
|
325
323
|
let promise = avatars.get(user);
|
|
@@ -365,7 +363,6 @@ var SlackProvider = class extends ChatSyncProvider {
|
|
|
365
363
|
fetchMetadata: rest
|
|
366
364
|
};
|
|
367
365
|
},
|
|
368
|
-
render: options.render,
|
|
369
366
|
platform: "slack",
|
|
370
367
|
adapter: options.adapter,
|
|
371
368
|
canStartThreads: true,
|
|
@@ -399,15 +396,11 @@ var SlackProvider = class extends ChatSyncProvider {
|
|
|
399
396
|
},
|
|
400
397
|
mention: (actor) => actor.id.startsWith("slack:user:") ? `<@${actor.id.slice(11)}>` : null,
|
|
401
398
|
async resolveChannel(key) {
|
|
402
|
-
if (!key.startsWith("#"))
|
|
403
|
-
const web = await client().catch(() => void 0);
|
|
404
|
-
if (web) await join(web, key);
|
|
405
|
-
return key;
|
|
406
|
-
}
|
|
399
|
+
if (!key.startsWith("#")) return key;
|
|
407
400
|
const web = await client();
|
|
408
401
|
if (!web) throw new Error(`can't look up Slack channel ${key}: use its ID instead`);
|
|
409
402
|
const channel = await find(web, key.slice(1));
|
|
410
|
-
|
|
403
|
+
if (!channel.is_member) console.warn(`[board] slack: invite the app to ${key}; its events don't arrive until it's in`);
|
|
411
404
|
return channel.id;
|
|
412
405
|
},
|
|
413
406
|
markdown(message) {
|
|
@@ -421,6 +414,26 @@ var SlackProvider = class extends ChatSyncProvider {
|
|
|
421
414
|
const raw = message.raw;
|
|
422
415
|
return slackLinks(key, typeof raw?.text === "string" ? raw.text : message.text, Array.isArray(raw?.attachments) ? raw.attachments : [], mrkdwn);
|
|
423
416
|
},
|
|
417
|
+
locate(url) {
|
|
418
|
+
const found = permalinkOf(url.trim());
|
|
419
|
+
return found ? {
|
|
420
|
+
thread: `${found.channel}:${found.thread}`,
|
|
421
|
+
message: found.ts
|
|
422
|
+
} : null;
|
|
423
|
+
},
|
|
424
|
+
async url(thread, message) {
|
|
425
|
+
const [channel] = thread.split(":");
|
|
426
|
+
const web = await client();
|
|
427
|
+
if (!web?.chat?.getPermalink || !channel) return null;
|
|
428
|
+
try {
|
|
429
|
+
return (await web.chat.getPermalink({
|
|
430
|
+
channel,
|
|
431
|
+
message_ts: message
|
|
432
|
+
})).permalink ?? null;
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
},
|
|
424
437
|
async fetchLink(link) {
|
|
425
438
|
const [, channel, thread] = link.thread.split(":");
|
|
426
439
|
const web = await client();
|
|
@@ -614,8 +627,7 @@ var SlackProvider = class extends ChatSyncProvider {
|
|
|
614
627
|
kind: at === -1 ? "channel" : "thread"
|
|
615
628
|
};
|
|
616
629
|
}
|
|
617
|
-
});
|
|
618
|
-
for (const [key, value] of Object.entries(options.channels)) this.map(key, value);
|
|
630
|
+
}, sync);
|
|
619
631
|
this.web = client;
|
|
620
632
|
}
|
|
621
633
|
/**
|
|
@@ -725,4 +737,4 @@ var SlackProvider = class extends ChatSyncProvider {
|
|
|
725
737
|
}
|
|
726
738
|
};
|
|
727
739
|
//#endregion
|
|
728
|
-
export { SlackEvent, SlackProvider, forwardText, slack, slackFileName, slackIcon, slackLinks, withUserTokens, withoutHtml, withoutLabel };
|
|
740
|
+
export { SlackEvent, SlackProvider, channels, forwardText, slack, slackFileName, slackIcon, slackLinks, withUserTokens, withoutHtml, withoutLabel };
|
package/dist/sync.chat.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as externalAuthor, n as ChatSyncProvider, r as ChatTransport, t as ChatInput } from "./chat-provider-
|
|
1
|
+
import { i as externalAuthor, n as ChatSyncProvider, r as ChatTransport, t as ChatInput } from "./chat-provider-DNJQyajS.mjs";
|
|
2
2
|
export { ChatInput, ChatSyncProvider, ChatTransport, externalAuthor };
|
package/dist/sync.chat.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as externalAuthor, t as ChatSyncProvider } from "./chat-provider-
|
|
1
|
+
import { n as externalAuthor, t as ChatSyncProvider } from "./chat-provider-YHy-WbrB.mjs";
|
|
2
2
|
export { ChatSyncProvider, externalAuthor };
|
package/dist/sync.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { C as
|
|
3
|
-
export { EventBase, type
|
|
1
|
+
import { d as ExternalFile, l as ExternalAuthor, u as ExternalEvent } from "./folds-CRYY14kc.mjs";
|
|
2
|
+
import { A as SyncOverrides, C as channels, D as EventBase, E as render, M as SyncRoot, N as envelope, O as ProviderEvent, P as eventSchema, S as SyncUnit, T as mirror, _ as withUserTokens, a as ProviderOptions, b as SyncIn, c as ReceivedEvent, d as ResolvedTarget, f as SyncContext, g as connectUserTokens, h as UserTokens, i as ProviderFile, j as SyncResult, k as RootOverride, l as RenderInput, m as UnreadableError, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, u as Renderer, v as Sync, w as into, x as SyncOut, y as SyncFn } from "./provider-B0b9Qxw4.mjs";
|
|
3
|
+
export { EventBase, type ExternalAuthor, ExternalEvent, type ExternalFile, type FileDelivery, type ProviderDelivery, type ProviderEvent, type ProviderFile, type ProviderOptions, type ProviderPost, type Receive, ReceivedEvent, type RenderInput, type Renderer, ResolvedTarget, type RootOverride, type Sync, type SyncContext, type SyncFn, type SyncIn, type SyncOut, SyncOverrides, SyncProvider, type SyncResult, type SyncRoot, type SyncUnit, UnreadableError, type UserTokens, channels, connectUserTokens, envelope, eventSchema, into, mirror, render, withUserTokens };
|
package/dist/sync.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { r as
|
|
4
|
-
|
|
1
|
+
import { n as ExternalEvent } from "./external-BV_cUncs.mjs";
|
|
2
|
+
import { c as into, i as UnreadableError, l as mirror, n as ResolvedTarget, r as SyncProvider, s as channels, t as ReceivedEvent, u as render } from "./provider-B3DpYSDq.mjs";
|
|
3
|
+
import { i as eventSchema, n as SyncOverrides, r as envelope, t as EventBase } from "./events-CewD4kw7.mjs";
|
|
4
|
+
import { r as withUserTokens, t as connectUserTokens } from "./users-yryHKFMo.mjs";
|
|
5
|
+
export { EventBase, ExternalEvent, ReceivedEvent, ResolvedTarget, SyncOverrides, SyncProvider, UnreadableError, channels, connectUserTokens, envelope, eventSchema, into, mirror, render, withUserTokens };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { l as isThreadTarget } from "./external-BV_cUncs.mjs";
|
|
2
|
+
import { n as replaceMentions, t as extractMentions } from "./mentions-CB6ShY4R.mjs";
|
|
3
|
+
//#region src/server/targets.ts
|
|
4
|
+
/** A target as a post or a receipt names it, resolved: a plain one's kind goes by its shape. */
|
|
5
|
+
const syncTarget = (target) => typeof target === "string" ? {
|
|
6
|
+
target,
|
|
7
|
+
kind: isThreadTarget(target) ? "thread" : "channel"
|
|
8
|
+
} : target;
|
|
9
|
+
/** Whether `text` mentions anyone by email, `@[ada@vercel.com]`, which sync looks up. */
|
|
10
|
+
const mentionsEmails = (text) => extractMentions(text).some((token) => token.kind === "actor" && token.actor.includes("@"));
|
|
11
|
+
/**
|
|
12
|
+
* `@[ada@vercel.com]`: each email `find` knows (a synced platform's user) becomes that user's
|
|
13
|
+
* exact mention, `@[slack:user:U0456]`, so it reads natively there; the rest stay plain text.
|
|
14
|
+
*/
|
|
15
|
+
async function withEmailMentions(text, find) {
|
|
16
|
+
const emails = extractMentions(text).flatMap((token) => token.kind === "actor" && token.actor.includes("@") ? [token.actor] : []);
|
|
17
|
+
if (emails.length === 0) return {
|
|
18
|
+
text,
|
|
19
|
+
found: []
|
|
20
|
+
};
|
|
21
|
+
const byEmail = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const email of emails) byEmail.set(email, await find?.(email).catch(() => null) ?? null);
|
|
23
|
+
return {
|
|
24
|
+
text: replaceMentions(text, (token) => {
|
|
25
|
+
if (token.kind !== "actor" || !byEmail.has(token.actor)) return null;
|
|
26
|
+
const actor = byEmail.get(token.actor);
|
|
27
|
+
return actor ? `@[${actor.id}]` : token.actor;
|
|
28
|
+
}),
|
|
29
|
+
found: [...byEmail.values()].filter((actor) => actor !== null)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { syncTarget as n, withEmailMentions as r, mentionsEmails as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { l as unauthorized } from "./errors-DX3hpwWt.mjs";
|
|
2
|
-
import { f as markVerified, m as trustedIssuers, n as TOKENS, r as VERCEL_ISSUER, u as issuerName } from "./auth-
|
|
2
|
+
import { f as markVerified, m as trustedIssuers, n as TOKENS, r as VERCEL_ISSUER, u as issuerName } from "./auth-Drg9FiC2.mjs";
|
|
3
3
|
import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
|
|
4
4
|
//#region src/server/identity.ts
|
|
5
5
|
let fromDisk;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as ExternalCopy, c as SyncedThread,
|
|
2
|
-
import { A2Store } from "
|
|
1
|
+
import { a as ExternalCopy, c as SyncedThread, g as BoardEvent, o as Message } from "./folds-CRYY14kc.mjs";
|
|
2
|
+
import { t as A2Store } from "./store-D0MhBvg6-4Z2L0Kqd.mjs";
|
|
3
3
|
//#region src/store/types.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* What a query matches, resolved by the board: handles are already actor and
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
const USER_TOKENS = Symbol.for("board.userTokens");
|
|
3
3
|
/**
|
|
4
4
|
* Gives an adapter people's tokens, for your own OAuth: `slack({ adapter:
|
|
5
|
-
* withUserTokens(createSlackAdapter(...), { authorize, token }) })`. A
|
|
6
|
-
* connector name (`
|
|
5
|
+
* withUserTokens(createSlackAdapter(...), { authorize, token }) }, sync)`. A
|
|
6
|
+
* Connect connector name (`slack('slack/acme', sync)`) does this for you.
|
|
7
7
|
*/
|
|
8
8
|
function withUserTokens(adapter, tokens) {
|
|
9
9
|
Object.defineProperty(adapter, USER_TOKENS, {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { n as DIRECT_UPLOAD_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
1
|
+
import { n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
2
|
import { S as storable, a as AttachmentId, c as ChannelPath, d as LabelKey, g as MessageText, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, p as Labels, x as platformAvatar } from "./schemas-Be08nyfW.mjs";
|
|
3
|
-
import { a as
|
|
3
|
+
import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin, t as ExternalAuthor } from "./external-BV_cUncs.mjs";
|
|
4
|
+
import { r as badRequest, t as HttpError } from "./errors-DX3hpwWt.mjs";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
//#region src/api/routes.ts
|
|
6
7
|
/**
|
|
@@ -150,6 +151,8 @@ const SourceReadQuery = z.object({
|
|
|
150
151
|
target: z.string().min(1).max(4e3),
|
|
151
152
|
limit: z.coerce.number().int().min(1).max(200).optional()
|
|
152
153
|
});
|
|
154
|
+
/** A platform's link to a message or thread, such as a Slack permalink or a GitHub comment's URL. */
|
|
155
|
+
const LinkedQuery = z.object({ url: z.string().url().max(4e3) });
|
|
153
156
|
const IdentityUnlinkBody = z.object({ actor: z.string().min(1).max(256) });
|
|
154
157
|
/** A sync entry's key: `slack`, `github`. */
|
|
155
158
|
const SyncKey = z.string().min(1).max(64);
|
|
@@ -414,6 +417,17 @@ const routes = {
|
|
|
414
417
|
path: "/messages/:id/sync",
|
|
415
418
|
body: SyncBody
|
|
416
419
|
},
|
|
420
|
+
/** Where the message's thread is synced, and its copy on each target: needs `read` where it is. */
|
|
421
|
+
synced: {
|
|
422
|
+
method: "GET",
|
|
423
|
+
path: "/messages/:id/sync"
|
|
424
|
+
},
|
|
425
|
+
/** The board message a platform link names, from what the board synced: 404 `message_not_found` for one it didn't, or that the caller can't read. */
|
|
426
|
+
linked: {
|
|
427
|
+
method: "GET",
|
|
428
|
+
path: "/links",
|
|
429
|
+
query: LinkedQuery
|
|
430
|
+
},
|
|
417
431
|
/** Ephemeral: shows typing in the thread's synced platform threads, and writes nothing to the log. Needs `post` where the thread is. */
|
|
418
432
|
typing: {
|
|
419
433
|
method: "POST",
|
|
@@ -612,35 +626,205 @@ const routes = {
|
|
|
612
626
|
}
|
|
613
627
|
};
|
|
614
628
|
//#endregion
|
|
615
|
-
//#region src/
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
629
|
+
//#region src/server/backend.ts
|
|
630
|
+
/** The subscription sync's outbound runs on, in `BoardBinding.subscriptions`. No watcher's ID starts with `@`. */
|
|
631
|
+
const SYNC = "@sync";
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/version.ts
|
|
634
|
+
const VERSION = "0.17.0";
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region src/server/routing.ts
|
|
637
|
+
/** board()'s own routes: the files it serves, and sync's, which run where board() does. */
|
|
638
|
+
const BOARD_ROUTES = [
|
|
639
|
+
"avatar",
|
|
640
|
+
"openapi",
|
|
641
|
+
"webhook",
|
|
642
|
+
"signIn",
|
|
643
|
+
"signedIn",
|
|
644
|
+
"identityUnlink",
|
|
645
|
+
"sync"
|
|
646
|
+
];
|
|
647
|
+
/** Every protocol method, by the route it's served at. */
|
|
648
|
+
const METHODS = [
|
|
649
|
+
"discovery",
|
|
650
|
+
"me",
|
|
651
|
+
"channels",
|
|
652
|
+
"actors",
|
|
653
|
+
"identity",
|
|
654
|
+
"messages",
|
|
655
|
+
"threads",
|
|
656
|
+
"message",
|
|
657
|
+
"post",
|
|
658
|
+
"posted",
|
|
659
|
+
"edit",
|
|
660
|
+
"delete",
|
|
661
|
+
"react",
|
|
662
|
+
"label",
|
|
663
|
+
"schedule",
|
|
664
|
+
"schedules",
|
|
665
|
+
"cancelSchedule",
|
|
666
|
+
"upload",
|
|
667
|
+
"attachment",
|
|
668
|
+
"uploadStart",
|
|
669
|
+
"uploadFinish",
|
|
670
|
+
"watchers",
|
|
671
|
+
"register",
|
|
672
|
+
"matches",
|
|
673
|
+
"ack",
|
|
674
|
+
"events",
|
|
675
|
+
"syncImport",
|
|
676
|
+
"syncReceipt",
|
|
677
|
+
"syncThread",
|
|
678
|
+
"syncFind",
|
|
679
|
+
"syncCopies",
|
|
680
|
+
"syncRoots",
|
|
681
|
+
"inboxPush",
|
|
682
|
+
"inboxNext",
|
|
683
|
+
"inboxAck",
|
|
684
|
+
"inbox",
|
|
685
|
+
"syncCode",
|
|
686
|
+
"syncAccount",
|
|
687
|
+
"syncUnlink",
|
|
688
|
+
"typing",
|
|
689
|
+
"readSource",
|
|
690
|
+
"synced",
|
|
691
|
+
"linked"
|
|
692
|
+
];
|
|
693
|
+
const escape = (literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
694
|
+
function compile(method, path, target, own) {
|
|
695
|
+
const parts = path.split("/").filter(Boolean);
|
|
696
|
+
const source = parts.map((part) => part.startsWith(":") ? "/([^/]+)" : `/${escape(part)}`).join("");
|
|
697
|
+
return {
|
|
698
|
+
method,
|
|
699
|
+
pattern: new RegExp(`${source}$`),
|
|
700
|
+
names: parts.filter((part) => part.startsWith(":")).map((part) => part.slice(1)),
|
|
701
|
+
target,
|
|
702
|
+
segments: parts.length,
|
|
703
|
+
literals: parts.filter((part) => !part.startsWith(":")).length,
|
|
704
|
+
own
|
|
705
|
+
};
|
|
620
706
|
}
|
|
621
707
|
/**
|
|
622
|
-
*
|
|
623
|
-
*
|
|
708
|
+
* The routes a board answers, most specific first: each is matched as a suffix of the request's
|
|
709
|
+
* path, so `/api/board/messages/12` is `GET /messages/:id` under `/api/board`. A backend's own
|
|
710
|
+
* route at a protocol path takes its place. Discovery is the mount itself.
|
|
624
711
|
*/
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
712
|
+
function createRouter(backendRoutes, basePath) {
|
|
713
|
+
const patterns = [
|
|
714
|
+
...BOARD_ROUTES.map((name) => compile(routes[name].method, routes[name].path, {
|
|
715
|
+
kind: "board",
|
|
716
|
+
name
|
|
717
|
+
}, false)),
|
|
718
|
+
...METHODS.filter((name) => name !== "discovery").map((name) => compile(routes[name].method, routes[name].path, {
|
|
719
|
+
kind: "protocol",
|
|
720
|
+
name
|
|
721
|
+
}, false)),
|
|
722
|
+
...backendRoutes.map((route) => compile(route.method, route.path, {
|
|
723
|
+
kind: "backend",
|
|
724
|
+
route
|
|
725
|
+
}, true))
|
|
726
|
+
].sort((a, b) => b.segments - a.segments || b.literals - a.literals || Number(b.own) - Number(a.own));
|
|
727
|
+
/** A segment that isn't URI-encoded names nothing: no route. */
|
|
728
|
+
const found = (candidate, base, values) => {
|
|
729
|
+
const params = {};
|
|
730
|
+
try {
|
|
731
|
+
candidate.names.forEach((name, i) => params[name] = decodeURIComponent(values[i]));
|
|
732
|
+
} catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const first = candidate.names[0];
|
|
736
|
+
return {
|
|
737
|
+
target: candidate.target,
|
|
738
|
+
base,
|
|
739
|
+
param: first === void 0 ? null : params[first],
|
|
740
|
+
params
|
|
741
|
+
};
|
|
742
|
+
};
|
|
743
|
+
const discovery = (base) => ({
|
|
744
|
+
target: {
|
|
745
|
+
kind: "protocol",
|
|
746
|
+
name: "discovery"
|
|
747
|
+
},
|
|
748
|
+
base,
|
|
749
|
+
param: null,
|
|
750
|
+
params: {},
|
|
751
|
+
root: true
|
|
752
|
+
});
|
|
753
|
+
return function route(method, pathname) {
|
|
754
|
+
const path = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
755
|
+
if (basePath !== void 0) {
|
|
756
|
+
const base = basePath.replace(/\/+$/, "");
|
|
757
|
+
if (path !== base && !path.startsWith(`${base}/`)) return null;
|
|
758
|
+
const rest = path.slice(base.length) || "/";
|
|
759
|
+
if (rest === "/") return method === "GET" ? discovery(base) : null;
|
|
760
|
+
for (const candidate of patterns) {
|
|
761
|
+
const hit = candidate.method === method ? rest.match(new RegExp(`^${candidate.pattern.source}`)) : null;
|
|
762
|
+
if (hit) return found(candidate, base, hit.slice(1));
|
|
763
|
+
}
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
for (const candidate of patterns) {
|
|
767
|
+
if (candidate.method !== method) continue;
|
|
768
|
+
const hit = path.match(candidate.pattern);
|
|
769
|
+
if (hit) return found(candidate, path.slice(0, hit.index), hit.slice(1));
|
|
770
|
+
}
|
|
771
|
+
return method === "GET" ? discovery(path === "/" ? "" : path) : null;
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function queryObject(url) {
|
|
775
|
+
const out = {};
|
|
776
|
+
for (const key of new Set(url.searchParams.keys())) {
|
|
777
|
+
const values = url.searchParams.getAll(key);
|
|
778
|
+
out[key] = values.length > 1 ? values : values[0];
|
|
779
|
+
}
|
|
780
|
+
return out;
|
|
781
|
+
}
|
|
782
|
+
async function readJson(request) {
|
|
783
|
+
const text = await request.text();
|
|
784
|
+
if (text.trim() === "") return void 0;
|
|
785
|
+
try {
|
|
786
|
+
return JSON.parse(text);
|
|
787
|
+
} catch {
|
|
788
|
+
throw badRequest("request body is not valid JSON");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const tooLarge = () => new HttpError(413, "too_large", `attachments are at most ${ATTACHMENT_BYTES_MAX / 1024 / 1024} MB`);
|
|
792
|
+
/** The body, refused as soon as it's past `max`: with or without a `content-length`, it never reads more. */
|
|
793
|
+
async function readCapped(request, max) {
|
|
794
|
+
if (Number(request.headers.get("content-length") ?? 0) > max) throw tooLarge();
|
|
795
|
+
const chunks = [];
|
|
796
|
+
let size = 0;
|
|
797
|
+
for await (const chunk of request.body ?? []) {
|
|
798
|
+
size += chunk.byteLength;
|
|
799
|
+
if (size > max) throw tooLarge();
|
|
800
|
+
chunks.push(chunk);
|
|
801
|
+
}
|
|
802
|
+
const body = new Uint8Array(size);
|
|
803
|
+
let at = 0;
|
|
804
|
+
for (const chunk of chunks) {
|
|
805
|
+
body.set(chunk, at);
|
|
806
|
+
at += chunk.byteLength;
|
|
807
|
+
}
|
|
808
|
+
return body;
|
|
809
|
+
}
|
|
810
|
+
function openapi() {
|
|
811
|
+
const paths = {};
|
|
812
|
+
for (const [name, definition] of Object.entries(routes)) {
|
|
813
|
+
const path = definition.path.replace(/:(\w+)/g, "{$1}");
|
|
814
|
+
const operation = { operationId: name };
|
|
815
|
+
if ("query" in definition) operation["x-query-schema"] = z.toJSONSchema(definition.query, { io: "input" });
|
|
816
|
+
if ("body" in definition) operation.requestBody = { content: { "application/json": { schema: z.toJSONSchema(definition.body, { io: "input" }) } } };
|
|
817
|
+
(paths[path] ??= {})[definition.method.toLowerCase()] = operation;
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
openapi: "3.1.0",
|
|
821
|
+
info: {
|
|
822
|
+
title: "board",
|
|
823
|
+
version: VERSION
|
|
824
|
+
},
|
|
825
|
+
paths
|
|
826
|
+
};
|
|
631
827
|
}
|
|
632
|
-
/** A derived ID's shape. */
|
|
633
|
-
const DERIVED_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
634
|
-
/**
|
|
635
|
-
* A post's ID, from one derived for it, under `principal`: a board takes a `key:<principal>:` ID
|
|
636
|
-
* only from that principal, so no one else can post with it first.
|
|
637
|
-
*/
|
|
638
|
-
const keyId = async (principal, derived) => `key:${principal}:${(await sha256(`board-key\u001f${derived}`)).slice(0, 32)}`;
|
|
639
|
-
/**
|
|
640
|
-
* Whether `id` is exactly the shape `keyId` makes for `principal`. A prefix check isn't enough:
|
|
641
|
-
* a principal's ID can be the start of another's (`oidc:gitlab.com:acme` and `oidc:gitlab.com:acme:ci`).
|
|
642
|
-
*/
|
|
643
|
-
const isKeyIdOf = (id, principal) => id.startsWith(`key:${principal}:`) && /^[0-9a-f]{32}$/.test(id.slice(`key:${principal}:`.length));
|
|
644
828
|
//#endregion
|
|
645
829
|
//#region src/api/tail.ts
|
|
646
830
|
/** The filter flags `read`, `tail`, and `wait` share, and that a watcher's `tail` accepts. */
|
|
@@ -784,4 +968,25 @@ function compileWatcher(id, spec) {
|
|
|
784
968
|
};
|
|
785
969
|
}
|
|
786
970
|
//#endregion
|
|
787
|
-
|
|
971
|
+
//#region src/server/watchers-hash.ts
|
|
972
|
+
/** By UTF-16 code unit, as `sort()` orders strings: the same in every locale and runtime. */
|
|
973
|
+
const byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
974
|
+
/**
|
|
975
|
+
* What `watchersHash` hashes, the same on both ends: a board on `http()` hashes its watchers, and
|
|
976
|
+
* whether its sync subscribes, and the board it uses hashes what it holds.
|
|
977
|
+
*/
|
|
978
|
+
function canonicalWatchers(watch, sync = []) {
|
|
979
|
+
const canonical = Object.keys(watch).sort(byCodeUnit).map((id) => [
|
|
980
|
+
id,
|
|
981
|
+
watch[id].tail,
|
|
982
|
+
watch[id].when
|
|
983
|
+
]);
|
|
984
|
+
if (sync.length > 0) canonical.push(["sync", [...sync].sort((a, b) => byCodeUnit(a.key, b.key)).map((entry) => [
|
|
985
|
+
entry.key,
|
|
986
|
+
entry.platform,
|
|
987
|
+
entry.signIn
|
|
988
|
+
])]);
|
|
989
|
+
return JSON.stringify(canonical);
|
|
990
|
+
}
|
|
991
|
+
//#endregion
|
|
992
|
+
export { ScheduleBody as A, SyncReceiptBody as B, LinkedQuery as C, ReactBody as D, PostBody as E, SyncBody as F, UploadStartBody as G, SyncUnlinkBody as H, SyncCodeBody as I, routes as K, SyncCopiesQuery as L, SignInQuery as M, SourceReadQuery as N, Ref as O, SyncAccountBody as P, SyncFindQuery as R, LabelBody as S, MessagesQuery as T, ThreadsQuery as U, SyncRootsQuery as V, TypingBody as W, IdentityUnlinkBody as _, withMentions as a, InboxPushBody as b, openapi as c, readJson as d, VERSION as f, EventsQuery as g, EditBody as h, FILTER_FLAGS as i, SchedulesQuery as j, RegisterBody as k, queryObject as l, AckBody as m, compileWatcher as n, METHODS as o, SYNC as p, hasCriteria as r, createRouter as s, canonicalWatchers as t, readCapped as u, InboxAckBody as v, MatchesQuery as w, InboxQuery as x, InboxNextQuery as y, SyncImportBody as z };
|
|
@@ -102,7 +102,7 @@ any backend:
|
|
|
102
102
|
| `middleware` | Runs on every post and edit before it reaches the backend, whatever it came from. See [Middleware](./08-middleware.md) |
|
|
103
103
|
| `project` | `{ id, teamId? }`: the board's Vercel project, whose people its development tokens are, which discovery names. Defaults to `VERCEL_PROJECT_ID`, then `.vercel/project.json`. The team scopes `vercelProject()` rules named without one |
|
|
104
104
|
| `watch` | Tails the board runs for you, keyed by id: `{ triage: { tail, run } }`. See [Watchers](./06-watchers.md) |
|
|
105
|
-
| `sync` | Two-way sync, keyed by a name you choose: `{ slack: slack({…}) }`. See [Sync](../02-sync/01-overview.md) |
|
|
105
|
+
| `sync` | Two-way sync, keyed by a name you choose: `{ slack: slack('slack/acme', channels({…})) }`. See [Sync](../02-sync/01-overview.md) |
|
|
106
106
|
| `url` | The board's public URL, which every token's audience must equal. Inferred from requests otherwise |
|
|
107
107
|
| `basePath` | The mount path, e.g. `/api/board`. Inferred from requests otherwise: a request can only make it look deeper than it is (`/api/board/x/messages/1`), so the board keeps the shallowest it's seen |
|
|
108
108
|
| `streamMaxMs` | How long an SSE stream stays open before the board closes it and the client reconnects. Default `280000` |
|