@vercel/board 0.0.0 → 0.15.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 +180 -0
- package/dist/a2-5EG8FkZ8.d.mts +3359 -0
- package/dist/a2-DYGYankn.mjs +3045 -0
- package/dist/access-CJU2-vaa.mjs +226 -0
- package/dist/access-CV96SYgr.d.mts +32 -0
- package/dist/access.d.mts +29 -0
- package/dist/access.mjs +4 -0
- package/dist/actors-rdN4dDTU.mjs +223 -0
- package/dist/attempts-BQUp9MKO.mjs +919 -0
- package/dist/auth-COzc09sn.mjs +276 -0
- package/dist/auth-wHFcAte3.d.mts +203 -0
- package/dist/backend-a2/blob-vercel.d.mts +8 -0
- package/dist/backend-a2/blob-vercel.mjs +81 -0
- package/dist/backend-a2/contract.d.mts +18 -0
- package/dist/backend-a2/contract.mjs +5 -0
- package/dist/backend-a2/scheduler-local.d.mts +11 -0
- package/dist/backend-a2/scheduler-local.mjs +2 -0
- package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
- package/dist/backend-a2/scheduler-qstash.mjs +2 -0
- package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
- package/dist/backend-a2/scheduler-vercel.mjs +2 -0
- package/dist/backend-a2/store-memory.d.mts +11 -0
- package/dist/backend-a2/store-memory.mjs +19 -0
- package/dist/backend-a2/store-postgres.d.mts +38 -0
- package/dist/backend-a2/store-postgres.mjs +110 -0
- package/dist/backend-a2/store-redis-http.d.mts +17 -0
- package/dist/backend-a2/store-redis-http.mjs +32 -0
- package/dist/backend-a2/store-redis.d.mts +23 -0
- package/dist/backend-a2/store-redis.mjs +39 -0
- package/dist/backend-a2/store-sqlite.d.mts +12 -0
- package/dist/backend-a2/store-sqlite.mjs +31 -0
- package/dist/backend-a2.d.mts +4 -0
- package/dist/backend-a2.mjs +2 -0
- package/dist/backend-http-8ex5g7Wv.d.mts +630 -0
- package/dist/backend-http-BsDSgmeo.mjs +613 -0
- package/dist/backend-http-Cv6xbEy9.mjs +469 -0
- package/dist/backend-http.d.mts +10 -0
- package/dist/backend-http.mjs +2 -0
- package/dist/backend-http.node.d.mts +10 -0
- package/dist/backend-http.node.mjs +2 -0
- package/dist/blob-C0J3wbGv.d.mts +39 -0
- package/dist/bots-CGkO76qH.mjs +67 -0
- package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
- package/dist/chat-provider-jFSeyNmB.mjs +389 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2252 -0
- package/dist/client-DGDPaN7U.mjs +380 -0
- package/dist/conformance-DRR23YNy.mjs +512 -0
- package/dist/conformance.d.mts +30 -0
- package/dist/conformance.mjs +2 -0
- package/dist/contract-CuJmlrhw.mjs +566 -0
- package/dist/emoji-CTLpvH-A.mjs +445 -0
- package/dist/engine-3EvQ8idk.mjs +1449 -0
- package/dist/errors-DX3hpwWt.mjs +64 -0
- package/dist/exports-BpgyEdh6.d.mts +4 -0
- package/dist/exports-CyY3_FjZ.mjs +1285 -0
- package/dist/files-SniSmeEr.mjs +47 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.default-BZ3lItjH.mjs +12 -0
- package/dist/index.mjs +19 -0
- package/dist/index.node.d.mts +14 -0
- package/dist/index.node.mjs +5 -0
- package/dist/inspect/app.js +416 -0
- package/dist/inspect/engine.js +5 -0
- package/dist/local-Cr_dSjvs.mjs +44 -0
- package/dist/own-OwgPWdmh.mjs +15 -0
- package/dist/project-CFiArraX.mjs +124 -0
- package/dist/provider-BGjz-mDa.d.mts +2473 -0
- package/dist/provider-CuewcDNZ.mjs +205 -0
- package/dist/redis-tables-B35vv4CZ.mjs +842 -0
- package/dist/reducers-B_vUGQRp.d.mts +1172 -0
- package/dist/reducers-mUPFuFJV.mjs +293 -0
- package/dist/schemas-Be08nyfW.mjs +74 -0
- package/dist/schemas-CWxs3ECJ.d.mts +50 -0
- package/dist/serve-Ccbb2LSb.mjs +170 -0
- package/dist/server-Cxey38B-.mjs +17 -0
- package/dist/serving-BNbzc96L.mjs +2 -0
- package/dist/sql-DbiEnqM0.mjs +571 -0
- package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
- package/dist/sync-github.d.mts +473 -0
- package/dist/sync-github.mjs +603 -0
- package/dist/sync-kapso.d.mts +175 -0
- package/dist/sync-kapso.mjs +86 -0
- package/dist/sync-slack.d.mts +364 -0
- package/dist/sync-slack.mjs +728 -0
- package/dist/sync.chat.d.mts +2 -0
- package/dist/sync.chat.mjs +2 -0
- package/dist/sync.d.mts +3 -0
- package/dist/sync.mjs +4 -0
- package/dist/targets-ojFEXEyN.mjs +94 -0
- package/dist/tokens-DKLn5P9B.mjs +256 -0
- package/dist/types-BHCjTZdB.d.mts +104 -0
- package/dist/users-Dls7_ANV.mjs +42 -0
- package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
- package/dist/watchers-hash-D9hbHLKB.mjs +225 -0
- package/docs/01-board/01-mounting.md +506 -0
- package/docs/01-board/02-database.md +258 -0
- package/docs/01-board/03-identity.md +262 -0
- package/docs/01-board/04-messages.md +192 -0
- package/docs/01-board/05-reading.md +177 -0
- package/docs/01-board/06-watchers.md +249 -0
- package/docs/01-board/07-schedules.md +133 -0
- package/docs/01-board/08-middleware.md +139 -0
- package/docs/01-board/09-access.md +499 -0
- package/docs/01-quickstart.md +196 -0
- package/docs/02-sync/01-overview.md +290 -0
- package/docs/02-sync/02-callbacks.md +265 -0
- package/docs/02-sync/03-slack.md +248 -0
- package/docs/02-sync/04-github.md +201 -0
- package/docs/02-sync/05-whatsapp.md +148 -0
- package/docs/02-sync/06-providers.md +228 -0
- package/docs/03-guides/01-coding-agents.md +226 -0
- package/docs/03-guides/02-inspector.md +104 -0
- package/docs/03-guides/03-ci.md +251 -0
- package/docs/03-guides/04-triage.md +233 -0
- package/docs/03-guides/05-agent-factory.md +142 -0
- package/docs/04-reference/01-cli.md +606 -0
- package/docs/04-reference/02-board.md +579 -0
- package/docs/04-reference/03-http.md +489 -0
- package/docs/04-reference/04-log.md +250 -0
- package/docs/04-reference/05-conformance.md +97 -0
- package/docs/index.md +186 -0
- package/package.json +168 -1
- package/sql/postgres/board.sql +145 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +451 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { n as DIRECT_UPLOAD_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
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 ExternalTarget, c as Link, l as Origin, n as ExternalAuthor, r as ExternalEvent } from "./contract-CuJmlrhw.mjs";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/api/routes.ts
|
|
6
|
+
/**
|
|
7
|
+
* A message's, schedule's, or cursor's ID: a string clients pass back as they got it, and never
|
|
8
|
+
* parse, compare, or count with. Each backend picks its own; a board on `a2()` writes decimals.
|
|
9
|
+
*/
|
|
10
|
+
const Ref = z.string().min(1).max(128);
|
|
11
|
+
const id = Ref;
|
|
12
|
+
const repeated = z.union([z.string(), z.array(z.string())]).transform((value) => Array.isArray(value) ? value : [value]);
|
|
13
|
+
/**
|
|
14
|
+
* `pr` → `['pr', null]` (has the label), `status=open` → `['status', ['open']]`,
|
|
15
|
+
* `state=open|draft` → either value.
|
|
16
|
+
*/
|
|
17
|
+
const LabelFilter = z.string().regex(/^[a-z0-9][a-z0-9_.-]*(=.*)?$/, "label filters look like key, key=value, or key=a|b").transform((value) => {
|
|
18
|
+
const at = value.indexOf("=");
|
|
19
|
+
return at === -1 ? [value, null] : [value.slice(0, at), value.slice(at + 1).split("|")];
|
|
20
|
+
});
|
|
21
|
+
/** A sync target as the board that syncs it resolved it: a platform channel to start a thread in, or a thread to join. */
|
|
22
|
+
const ResolvedSyncTarget = z.object({
|
|
23
|
+
target: ExternalTarget,
|
|
24
|
+
kind: z.enum(["channel", "thread"])
|
|
25
|
+
});
|
|
26
|
+
/** Shared by `/messages` and `/events`. */
|
|
27
|
+
const Filters = z.object({
|
|
28
|
+
channel: z.string().optional(),
|
|
29
|
+
re: id.optional(),
|
|
30
|
+
by: z.string().optional(),
|
|
31
|
+
mentions: z.string().optional(),
|
|
32
|
+
label: repeated.pipe(z.array(LabelFilter)).optional(),
|
|
33
|
+
/** Where messages came from: a sync key such as `slack`, or `board` for the board itself. */
|
|
34
|
+
from: z.string().min(1).max(64).optional(),
|
|
35
|
+
/** Case-insensitive words the text contains. */
|
|
36
|
+
text: z.string().min(1).max(200).optional()
|
|
37
|
+
});
|
|
38
|
+
const MessagesQuery = Filters.extend({
|
|
39
|
+
after: Ref.optional(),
|
|
40
|
+
before: id.optional(),
|
|
41
|
+
limit: z.coerce.number().int().min(1).max(500).optional()
|
|
42
|
+
});
|
|
43
|
+
/** `limit` counts threads: each thread with a matching message, by its latest reply. */
|
|
44
|
+
const ThreadsQuery = Filters.extend({
|
|
45
|
+
after: Ref.optional(),
|
|
46
|
+
limit: z.coerce.number().int().min(1).max(500).optional()
|
|
47
|
+
});
|
|
48
|
+
const EventsQuery = Filters.extend({
|
|
49
|
+
after: Ref.optional(),
|
|
50
|
+
wait: z.coerce.number().min(0).max(300).optional(),
|
|
51
|
+
all: z.coerce.boolean().optional()
|
|
52
|
+
});
|
|
53
|
+
const PostBody = z.object({
|
|
54
|
+
channel: ChannelPath.optional(),
|
|
55
|
+
re: Ref.optional(),
|
|
56
|
+
/** May be empty when the message has attachments. */
|
|
57
|
+
text: z.string().max(MESSAGE_TEXT_MAX).default(""),
|
|
58
|
+
labels: Labels.optional(),
|
|
59
|
+
noSync: z.boolean().optional(),
|
|
60
|
+
/**
|
|
61
|
+
* External targets a new thread starts with: `slack:#incidents`, `github:owner/repo:12`. The
|
|
62
|
+
* board that syncs a target resolves it to `{ target, kind }`, which a board elsewhere takes as is.
|
|
63
|
+
*/
|
|
64
|
+
sync: z.array(z.union([z.string().min(3).max(300), ResolvedSyncTarget])).max(10).optional(),
|
|
65
|
+
id: z.string().min(1).max(200).refine(storable, "id has a NUL character or a lone surrogate").optional(),
|
|
66
|
+
/** IDs from `POST /attachments`. */
|
|
67
|
+
attachments: z.array(AttachmentId).max(10).optional()
|
|
68
|
+
}).refine((body) => body.text.trim().length > 0 || (body.attachments?.length ?? 0) > 0, {
|
|
69
|
+
message: "message text is empty",
|
|
70
|
+
path: ["text"]
|
|
71
|
+
}).refine((body) => body.channel === void 0 !== (body.re === void 0), { message: "a post needs `channel`; a reply needs `re`" }).refine((body) => !(body.noSync && body.sync?.length), { message: "`noSync` and `sync` contradict each other" });
|
|
72
|
+
/** `30m`, `1d`: a number and a unit, `s`, `m`, `h`, or `d`. */
|
|
73
|
+
const Duration = z.string().regex(/^\d+(\.\d+)?(s|m|h|d)$/, "durations look like 30s, 30m, 12h, or 1d");
|
|
74
|
+
const Instant = z.string().datetime({ offset: true });
|
|
75
|
+
/**
|
|
76
|
+
* A message to post later, as the caller: once (`in` or `at`), or repeating
|
|
77
|
+
* (`every`, starting `at` or `in` from now, or one `every` from now), until
|
|
78
|
+
* `until`, `for` after the first send, or `times` sends. A repeat with no stop
|
|
79
|
+
* runs until it's canceled.
|
|
80
|
+
*/
|
|
81
|
+
const ScheduleBody = z.object({
|
|
82
|
+
channel: ChannelPath.optional(),
|
|
83
|
+
re: Ref.optional(),
|
|
84
|
+
text: z.string().max(MESSAGE_TEXT_MAX).default(""),
|
|
85
|
+
labels: Labels.optional(),
|
|
86
|
+
noSync: z.boolean().optional(),
|
|
87
|
+
attachments: z.array(AttachmentId).max(10).optional(),
|
|
88
|
+
in: Duration.optional(),
|
|
89
|
+
at: Instant.optional(),
|
|
90
|
+
every: Duration.optional(),
|
|
91
|
+
until: Instant.optional(),
|
|
92
|
+
for: Duration.optional(),
|
|
93
|
+
times: z.number().int().positive().optional()
|
|
94
|
+
}).refine((body) => body.text.trim().length > 0 || (body.attachments?.length ?? 0) > 0, {
|
|
95
|
+
message: "message text is empty",
|
|
96
|
+
path: ["text"]
|
|
97
|
+
}).refine((body) => body.channel === void 0 !== (body.re === void 0), { message: "a schedule needs `channel` for a new thread, or `re` for a reply" }).refine((body) => body.in !== void 0 || body.at !== void 0 || body.every !== void 0, { message: "say when: `in`, `at`, or `every`" }).refine((body) => !(body.in !== void 0 && body.at !== void 0), { message: "`in` and `at` contradict each other" }).refine((body) => [
|
|
98
|
+
body.until,
|
|
99
|
+
body.for,
|
|
100
|
+
body.times
|
|
101
|
+
].filter((v) => v !== void 0).length <= 1, { message: "pick one stop: `until`, `for`, or `times`" }).refine((body) => body.every !== void 0 || body.until === void 0 && body.for === void 0 && body.times === void 0, { message: "`until`, `for`, and `times` stop a repeat: add `every`" });
|
|
102
|
+
const SchedulesQuery = z.object({
|
|
103
|
+
/** Any message in a thread: the schedules that reply in it. */
|
|
104
|
+
re: id.optional(),
|
|
105
|
+
channel: z.string().optional(),
|
|
106
|
+
/** An actor ID or handle: the schedules it created. */
|
|
107
|
+
by: z.string().optional(),
|
|
108
|
+
/** Include done and canceled schedules. */
|
|
109
|
+
all: z.coerce.boolean().optional()
|
|
110
|
+
});
|
|
111
|
+
const SyncBody = z.object({ target: z.string().min(3).max(300) });
|
|
112
|
+
const InboxQuery = z.object({
|
|
113
|
+
channel: z.string().optional(),
|
|
114
|
+
limit: z.coerce.number().int().min(1).max(500).optional()
|
|
115
|
+
});
|
|
116
|
+
/** What a board on `http()` registers with the board it uses: its watchers' tails, and where to wake it. */
|
|
117
|
+
const RegisterBody = z.object({
|
|
118
|
+
/** Without one, the board pulls on its own instead of being woken. */
|
|
119
|
+
url: z.string().url().max(500).refine((url) => /^https:\/\//.test(url), "wake-ups go over https").transform((url) => url.replace(/\/+$/, "")).nullable().optional(),
|
|
120
|
+
watch: z.record(z.string().max(100), z.object({
|
|
121
|
+
tail: z.string().max(1e3),
|
|
122
|
+
when: z.enum(["matches", "enters"]).default("matches")
|
|
123
|
+
})).refine((watch) => Object.keys(watch).length <= 32, `a board registers at most 32 watchers`),
|
|
124
|
+
/**
|
|
125
|
+
* Its sync entries: its sync subscribes too, to every post, change, and thread sync in the channels
|
|
126
|
+
* where it may sync, and the board lists the entries and links accounts on them.
|
|
127
|
+
*/
|
|
128
|
+
sync: z.array(z.object({
|
|
129
|
+
key: z.string().min(1).max(64),
|
|
130
|
+
platform: z.string().min(1).max(64),
|
|
131
|
+
signIn: z.boolean().default(false)
|
|
132
|
+
})).max(32).optional()
|
|
133
|
+
});
|
|
134
|
+
/** A pull of the caller's matches: at most `limit`, waiting up to `wait` seconds for one. */
|
|
135
|
+
const MatchesQuery = z.object({
|
|
136
|
+
limit: z.coerce.number().int().min(1).max(50).optional(),
|
|
137
|
+
wait: z.coerce.number().min(0).max(25).optional()
|
|
138
|
+
});
|
|
139
|
+
/** How a pulled match went: its lease, and an error to try again, or `skip` to drop it. */
|
|
140
|
+
const AckBody = z.object({
|
|
141
|
+
lease: z.string().min(1).max(64),
|
|
142
|
+
error: z.string().min(1).max(500).optional(),
|
|
143
|
+
skip: z.boolean().optional()
|
|
144
|
+
});
|
|
145
|
+
/**
|
|
146
|
+
* What to read from a platform without syncing it: a channel (`#name` or an ID),
|
|
147
|
+
* a thread (`C0123:1727000000.123456`), or a message permalink (its thread).
|
|
148
|
+
*/
|
|
149
|
+
const SourceReadQuery = z.object({
|
|
150
|
+
target: z.string().min(1).max(4e3),
|
|
151
|
+
limit: z.coerce.number().int().min(1).max(200).optional()
|
|
152
|
+
});
|
|
153
|
+
const IdentityUnlinkBody = z.object({ actor: z.string().min(1).max(256) });
|
|
154
|
+
/** A sync entry's key: `slack`, `github`. */
|
|
155
|
+
const SyncKey = z.string().min(1).max(64);
|
|
156
|
+
/** A platform message an import changes, by its platform thread and ID; without a message, the root of the board thread the platform thread belongs to. */
|
|
157
|
+
const ImportOrigin = z.object({
|
|
158
|
+
thread: ExternalTarget,
|
|
159
|
+
message: z.string().min(1).max(300).optional()
|
|
160
|
+
});
|
|
161
|
+
/** A link as an import carries it: a board message it names is a ref. */
|
|
162
|
+
const ImportLink = Link.extend({ message: Ref.nullable() });
|
|
163
|
+
/**
|
|
164
|
+
* A platform user, as sync names them to the board. Their avatar shows wherever they do, so one
|
|
165
|
+
* that isn't an https URL is dropped, never refused: the write lands without it.
|
|
166
|
+
*/
|
|
167
|
+
const PlatformUser = ExternalAuthor.extend({ avatar: z.string().nullable().transform(platformAvatar) });
|
|
168
|
+
/**
|
|
169
|
+
* Who wrote it on the platform, and the sync key it came through: null for a write a sync callback
|
|
170
|
+
* makes as that user, which syncs out to every platform like one written on the board.
|
|
171
|
+
*/
|
|
172
|
+
const Imported = {
|
|
173
|
+
author: PlatformUser,
|
|
174
|
+
from: SyncKey.nullable()
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Where the platform event an import comes from is: its sync entry, platform channel, and thread.
|
|
178
|
+
* The middleware is told, and answers there when it drops the write.
|
|
179
|
+
*/
|
|
180
|
+
const ImportSource = z.object({
|
|
181
|
+
key: SyncKey,
|
|
182
|
+
channel: ExternalTarget,
|
|
183
|
+
thread: ExternalTarget
|
|
184
|
+
});
|
|
185
|
+
/** The message a change is about: by its platform IDs (`origin`), or by its board ID (`message`), one or the other. */
|
|
186
|
+
const Changed = {
|
|
187
|
+
origin: ImportOrigin.optional(),
|
|
188
|
+
message: Ref.optional()
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* A platform's write, as its platform author, with where it came from: only a caller with `sync` in
|
|
192
|
+
* the channel it lands in may make one. Posts are idempotent by `id`, derived from the platform
|
|
193
|
+
* event; every other op finds the message it changes from its `origin`.
|
|
194
|
+
*/
|
|
195
|
+
const SyncImportBody = z.discriminatedUnion("op", [
|
|
196
|
+
z.object({
|
|
197
|
+
op: z.literal("post"),
|
|
198
|
+
...Imported,
|
|
199
|
+
/** Where it is on the platform. Null for a write a sync callback makes itself. */
|
|
200
|
+
origin: Origin.nullable().default(null),
|
|
201
|
+
source: ImportSource.optional(),
|
|
202
|
+
channel: ChannelPath.optional(),
|
|
203
|
+
re: Ref.optional(),
|
|
204
|
+
text: z.string().max(MESSAGE_TEXT_MAX).default(""),
|
|
205
|
+
labels: Labels.optional(),
|
|
206
|
+
noSync: z.boolean().optional(),
|
|
207
|
+
attachments: z.array(AttachmentId).max(10).optional(),
|
|
208
|
+
links: z.array(ImportLink).max(10).optional(),
|
|
209
|
+
id: z.string().min(1).max(200).refine(storable, "id has a NUL character or a lone surrogate").optional()
|
|
210
|
+
}),
|
|
211
|
+
z.object({
|
|
212
|
+
op: z.literal("edit"),
|
|
213
|
+
...Imported,
|
|
214
|
+
...Changed,
|
|
215
|
+
text: MessageText,
|
|
216
|
+
source: ImportSource.optional()
|
|
217
|
+
}),
|
|
218
|
+
z.object({
|
|
219
|
+
op: z.literal("delete"),
|
|
220
|
+
...Imported,
|
|
221
|
+
...Changed
|
|
222
|
+
}),
|
|
223
|
+
z.object({
|
|
224
|
+
op: z.literal("react"),
|
|
225
|
+
...Imported,
|
|
226
|
+
...Changed,
|
|
227
|
+
emoji: Emoji,
|
|
228
|
+
remove: z.boolean().optional()
|
|
229
|
+
}),
|
|
230
|
+
z.object({
|
|
231
|
+
op: z.literal("labels"),
|
|
232
|
+
...Imported,
|
|
233
|
+
...Changed,
|
|
234
|
+
set: Labels.optional(),
|
|
235
|
+
unset: z.array(LabelKey).optional()
|
|
236
|
+
}),
|
|
237
|
+
z.object({
|
|
238
|
+
op: z.literal("links"),
|
|
239
|
+
...Imported,
|
|
240
|
+
...Changed,
|
|
241
|
+
links: z.array(ImportLink).max(10)
|
|
242
|
+
}),
|
|
243
|
+
z.object({
|
|
244
|
+
op: z.literal("seen"),
|
|
245
|
+
author: PlatformUser
|
|
246
|
+
})
|
|
247
|
+
]);
|
|
248
|
+
const PlatformFiles = z.array(z.string().min(1).max(300)).max(10).nullable();
|
|
249
|
+
/** What sync did on a platform, about a board message: it landed, its files did, it gave up, or its thread syncs with a target. */
|
|
250
|
+
const SyncReceiptBody = z.discriminatedUnion("kind", [
|
|
251
|
+
z.object({
|
|
252
|
+
kind: z.literal("message"),
|
|
253
|
+
message: Ref,
|
|
254
|
+
target: ExternalTarget,
|
|
255
|
+
external: z.object({
|
|
256
|
+
thread: ExternalTarget,
|
|
257
|
+
message: z.string().min(1).max(300),
|
|
258
|
+
asUser: z.literal(true).optional()
|
|
259
|
+
}),
|
|
260
|
+
/** Its files, when they went with it: the platform's IDs, or null when it refused them. */
|
|
261
|
+
files: PlatformFiles.optional()
|
|
262
|
+
}),
|
|
263
|
+
z.object({
|
|
264
|
+
kind: z.literal("files"),
|
|
265
|
+
message: Ref,
|
|
266
|
+
target: ExternalTarget,
|
|
267
|
+
files: PlatformFiles
|
|
268
|
+
}),
|
|
269
|
+
z.object({
|
|
270
|
+
kind: z.literal("failed"),
|
|
271
|
+
message: Ref,
|
|
272
|
+
target: ExternalTarget,
|
|
273
|
+
error: z.string().min(1).max(500)
|
|
274
|
+
}),
|
|
275
|
+
z.object({
|
|
276
|
+
kind: z.literal("thread"),
|
|
277
|
+
/** Any message in the thread. */
|
|
278
|
+
message: Ref,
|
|
279
|
+
target: ExternalTarget,
|
|
280
|
+
/** A channel to start a thread in, or a thread to join. */
|
|
281
|
+
targetKind: z.enum(["channel", "thread"]),
|
|
282
|
+
cause: z.enum(["config", "request"])
|
|
283
|
+
})
|
|
284
|
+
]);
|
|
285
|
+
/** What sync looks up by platform ID: a thread or message, who an account is, and a person's accounts. */
|
|
286
|
+
const SyncFindQuery = z.object({
|
|
287
|
+
/** A live one-time code: who it was issued to, for a sign-in that carries it. */
|
|
288
|
+
code: z.string().min(1).max(64).optional(),
|
|
289
|
+
/** The post the caller's sync imported with this client ID. */
|
|
290
|
+
post: z.string().min(1).max(200).optional(),
|
|
291
|
+
thread: ExternalTarget.optional(),
|
|
292
|
+
/** With `thread`: the platform message in it. */
|
|
293
|
+
message: z.string().min(1).max(300).optional(),
|
|
294
|
+
account: ActorId.optional(),
|
|
295
|
+
person: ActorId.optional()
|
|
296
|
+
});
|
|
297
|
+
const SyncCopiesQuery = z.object({ message: repeated.pipe(z.array(Ref).min(1).max(100)) });
|
|
298
|
+
/** A person's own roots since `since`, in epoch milliseconds. */
|
|
299
|
+
const SyncRootsQuery = z.object({
|
|
300
|
+
by: ActorId,
|
|
301
|
+
since: z.coerce.number().int().nonnegative()
|
|
302
|
+
});
|
|
303
|
+
/** Platform events for a sync key's inbox, each with the platform's own ID for it, which dedupes it. */
|
|
304
|
+
const InboxPushBody = z.object({ events: z.array(z.object({
|
|
305
|
+
id: z.string().min(1).max(300),
|
|
306
|
+
event: ExternalEvent,
|
|
307
|
+
raw: z.unknown()
|
|
308
|
+
})).min(1).max(50) });
|
|
309
|
+
const InboxNextQuery = z.object({ limit: z.coerce.number().int().min(1).max(50).optional() });
|
|
310
|
+
const InboxAckBody = z.object({
|
|
311
|
+
lease: z.string().min(1).max(64),
|
|
312
|
+
error: z.string().min(1).max(500).optional()
|
|
313
|
+
});
|
|
314
|
+
/** A one-time code for linking an account on sync entry `sync`: a person's own, for themselves. */
|
|
315
|
+
const SyncCodeBody = z.object({ sync: SyncKey });
|
|
316
|
+
/**
|
|
317
|
+
* A platform account, linked to whoever a code was issued to: one posted from the account, or
|
|
318
|
+
* carried through its sign-in. `message` is the platform message that posted it.
|
|
319
|
+
*/
|
|
320
|
+
const SyncAccountBody = z.object({
|
|
321
|
+
sync: SyncKey,
|
|
322
|
+
account: PlatformUser,
|
|
323
|
+
code: z.string().min(1).max(64),
|
|
324
|
+
message: z.string().max(1024).optional(),
|
|
325
|
+
/** `consent` when the code came through the account's own sign-in, so the board posts as them. */
|
|
326
|
+
method: z.enum(["code", "consent"]).optional()
|
|
327
|
+
});
|
|
328
|
+
/** A sign-in that carries a code, on the board whose sync runs the entry. */
|
|
329
|
+
const SignInQuery = z.object({
|
|
330
|
+
sync: SyncKey,
|
|
331
|
+
code: z.string().min(1).max(64)
|
|
332
|
+
});
|
|
333
|
+
const SyncUnlinkBody = z.object({ account: ActorId });
|
|
334
|
+
/** A direct upload, before its bytes: what the file will be called, its type, and its exact size. */
|
|
335
|
+
const UploadStartBody = z.object({
|
|
336
|
+
name: z.string().max(1024).nullable().optional(),
|
|
337
|
+
type: z.string().max(1024).nullable().optional(),
|
|
338
|
+
size: z.number().int().positive().max(DIRECT_UPLOAD_BYTES_MAX, `direct uploads are at most ${DIRECT_UPLOAD_BYTES_MAX / 1024 / 1024} MB`)
|
|
339
|
+
});
|
|
340
|
+
const EditBody = z.object({ text: MessageText });
|
|
341
|
+
const DeleteBody = z.object({}).optional();
|
|
342
|
+
/** `status`: what Slack shows in place of "Typing…" (WhatsApp and GitHub ignore it). */
|
|
343
|
+
const TypingBody = z.object({ status: z.string().trim().min(1).max(100).optional() }).optional();
|
|
344
|
+
const ReactBody = z.object({
|
|
345
|
+
emoji: Emoji,
|
|
346
|
+
remove: z.boolean().optional()
|
|
347
|
+
});
|
|
348
|
+
const LabelBody = z.object({
|
|
349
|
+
set: Labels.optional(),
|
|
350
|
+
unset: z.array(LabelKey).optional()
|
|
351
|
+
}).refine((body) => body.set !== void 0 || body.unset !== void 0, { message: "nothing to change: pass `set` or `unset`" });
|
|
352
|
+
/** The route table: method, path, and the schemas that validate input. */
|
|
353
|
+
const routes = {
|
|
354
|
+
/** Where the board is, its project, and the sync entries it links accounts on. No token. */
|
|
355
|
+
discovery: {
|
|
356
|
+
method: "GET",
|
|
357
|
+
path: "/"
|
|
358
|
+
},
|
|
359
|
+
me: {
|
|
360
|
+
method: "GET",
|
|
361
|
+
path: "/me"
|
|
362
|
+
},
|
|
363
|
+
channels: {
|
|
364
|
+
method: "GET",
|
|
365
|
+
path: "/channels"
|
|
366
|
+
},
|
|
367
|
+
actors: {
|
|
368
|
+
method: "GET",
|
|
369
|
+
path: "/actors"
|
|
370
|
+
},
|
|
371
|
+
messages: {
|
|
372
|
+
method: "GET",
|
|
373
|
+
path: "/messages",
|
|
374
|
+
query: MessagesQuery
|
|
375
|
+
},
|
|
376
|
+
message: {
|
|
377
|
+
method: "GET",
|
|
378
|
+
path: "/messages/:id"
|
|
379
|
+
},
|
|
380
|
+
/** Threads with a matching message, one summary each, by latest activity. */
|
|
381
|
+
threads: {
|
|
382
|
+
method: "GET",
|
|
383
|
+
path: "/threads",
|
|
384
|
+
query: ThreadsQuery
|
|
385
|
+
},
|
|
386
|
+
post: {
|
|
387
|
+
method: "POST",
|
|
388
|
+
path: "/messages",
|
|
389
|
+
body: PostBody
|
|
390
|
+
},
|
|
391
|
+
edit: {
|
|
392
|
+
method: "POST",
|
|
393
|
+
path: "/messages/:id/edit",
|
|
394
|
+
body: EditBody
|
|
395
|
+
},
|
|
396
|
+
delete: {
|
|
397
|
+
method: "POST",
|
|
398
|
+
path: "/messages/:id/delete",
|
|
399
|
+
body: DeleteBody
|
|
400
|
+
},
|
|
401
|
+
react: {
|
|
402
|
+
method: "POST",
|
|
403
|
+
path: "/messages/:id/reactions",
|
|
404
|
+
body: ReactBody
|
|
405
|
+
},
|
|
406
|
+
label: {
|
|
407
|
+
method: "POST",
|
|
408
|
+
path: "/messages/:id/labels",
|
|
409
|
+
body: LabelBody
|
|
410
|
+
},
|
|
411
|
+
/** Attaches the thread to a sync target, recording a `thread` receipt: needs `sync` where the thread is. */
|
|
412
|
+
sync: {
|
|
413
|
+
method: "POST",
|
|
414
|
+
path: "/messages/:id/sync",
|
|
415
|
+
body: SyncBody
|
|
416
|
+
},
|
|
417
|
+
/** Ephemeral: shows typing in the thread's synced platform threads, and writes nothing to the log. Needs `post` where the thread is. */
|
|
418
|
+
typing: {
|
|
419
|
+
method: "POST",
|
|
420
|
+
path: "/messages/:id/typing",
|
|
421
|
+
body: TypingBody
|
|
422
|
+
},
|
|
423
|
+
/** Posts a message later, as the caller: needs a scheduler. */
|
|
424
|
+
schedule: {
|
|
425
|
+
method: "POST",
|
|
426
|
+
path: "/schedules",
|
|
427
|
+
body: ScheduleBody
|
|
428
|
+
},
|
|
429
|
+
schedules: {
|
|
430
|
+
method: "GET",
|
|
431
|
+
path: "/schedules",
|
|
432
|
+
query: SchedulesQuery
|
|
433
|
+
},
|
|
434
|
+
/** Its creator, or manage in its channel, may cancel a schedule. Canceling one that's done is a no-op. */
|
|
435
|
+
cancelSchedule: {
|
|
436
|
+
method: "POST",
|
|
437
|
+
path: "/schedules/:id/cancel"
|
|
438
|
+
},
|
|
439
|
+
/** The caller's own post with the client ID it gave: what `POST /messages` answered, for telling a retry before posting. */
|
|
440
|
+
posted: {
|
|
441
|
+
method: "GET",
|
|
442
|
+
path: "/posts/:id"
|
|
443
|
+
},
|
|
444
|
+
/** Raw platform events in a sync key's inbox, newest first: the caller's own, and the board's with `sync` everywhere. */
|
|
445
|
+
inbox: {
|
|
446
|
+
method: "GET",
|
|
447
|
+
path: "/inbox/:key",
|
|
448
|
+
query: InboxQuery
|
|
449
|
+
},
|
|
450
|
+
/** Pushes platform events into the caller's inbox for a sync key, deduplicated by their platform IDs. */
|
|
451
|
+
inboxPush: {
|
|
452
|
+
method: "POST",
|
|
453
|
+
path: "/inbox/:key",
|
|
454
|
+
body: InboxPushBody
|
|
455
|
+
},
|
|
456
|
+
/** The caller's next inbox events, leased to it, one platform thread at a time and in order. */
|
|
457
|
+
inboxNext: {
|
|
458
|
+
method: "GET",
|
|
459
|
+
path: "/inbox/:key/next",
|
|
460
|
+
query: InboxNextQuery
|
|
461
|
+
},
|
|
462
|
+
/** How one went: done, or an error to try it again. */
|
|
463
|
+
inboxAck: {
|
|
464
|
+
method: "POST",
|
|
465
|
+
path: "/inbox/:key/:id/ack",
|
|
466
|
+
body: InboxAckBody
|
|
467
|
+
},
|
|
468
|
+
/** A platform's write, as its platform author: `sync` where it lands. */
|
|
469
|
+
syncImport: {
|
|
470
|
+
method: "POST",
|
|
471
|
+
path: "/sync/imports",
|
|
472
|
+
body: SyncImportBody
|
|
473
|
+
},
|
|
474
|
+
/** What sync did on a platform: a delivery, its files, a failure, or a thread's new target. */
|
|
475
|
+
syncReceipt: {
|
|
476
|
+
method: "POST",
|
|
477
|
+
path: "/sync/receipts",
|
|
478
|
+
body: SyncReceiptBody
|
|
479
|
+
},
|
|
480
|
+
/** A board thread's sync, by its key. */
|
|
481
|
+
syncThread: {
|
|
482
|
+
method: "GET",
|
|
483
|
+
path: "/sync/threads/:thread"
|
|
484
|
+
},
|
|
485
|
+
/** What platform IDs are on the board. */
|
|
486
|
+
syncFind: {
|
|
487
|
+
method: "GET",
|
|
488
|
+
path: "/sync/find",
|
|
489
|
+
query: SyncFindQuery
|
|
490
|
+
},
|
|
491
|
+
/** Messages' copies on platforms. */
|
|
492
|
+
syncCopies: {
|
|
493
|
+
method: "GET",
|
|
494
|
+
path: "/sync/copies",
|
|
495
|
+
query: SyncCopiesQuery
|
|
496
|
+
},
|
|
497
|
+
/** A person's recent roots, for telling their own platform copies from new messages. */
|
|
498
|
+
syncRoots: {
|
|
499
|
+
method: "GET",
|
|
500
|
+
path: "/sync/roots",
|
|
501
|
+
query: SyncRootsQuery
|
|
502
|
+
},
|
|
503
|
+
/** A one-time code for linking a platform account. */
|
|
504
|
+
syncCode: {
|
|
505
|
+
method: "POST",
|
|
506
|
+
path: "/sync/codes",
|
|
507
|
+
body: SyncCodeBody
|
|
508
|
+
},
|
|
509
|
+
/** Links a platform account, by a code or for a person. */
|
|
510
|
+
syncAccount: {
|
|
511
|
+
method: "POST",
|
|
512
|
+
path: "/sync/accounts",
|
|
513
|
+
body: SyncAccountBody
|
|
514
|
+
},
|
|
515
|
+
/** Unlinks a platform account. */
|
|
516
|
+
syncUnlink: {
|
|
517
|
+
method: "POST",
|
|
518
|
+
path: "/sync/accounts/unlink",
|
|
519
|
+
body: SyncUnlinkBody
|
|
520
|
+
},
|
|
521
|
+
/** Reads a platform channel or thread on demand, where that sync runs, without syncing or storing it. Full access only. */
|
|
522
|
+
readSource: {
|
|
523
|
+
method: "GET",
|
|
524
|
+
path: "/sources/:key/read",
|
|
525
|
+
query: SourceReadQuery
|
|
526
|
+
},
|
|
527
|
+
webhook: {
|
|
528
|
+
method: "POST",
|
|
529
|
+
path: "/webhooks/:key"
|
|
530
|
+
},
|
|
531
|
+
events: {
|
|
532
|
+
method: "GET",
|
|
533
|
+
path: "/events",
|
|
534
|
+
query: EventsQuery
|
|
535
|
+
},
|
|
536
|
+
avatar: {
|
|
537
|
+
method: "GET",
|
|
538
|
+
path: "/avatars/:name"
|
|
539
|
+
},
|
|
540
|
+
/** The file is the body; `content-type` is its type and `x-board-filename` its name, URI-encoded. */
|
|
541
|
+
upload: {
|
|
542
|
+
method: "POST",
|
|
543
|
+
path: "/attachments"
|
|
544
|
+
},
|
|
545
|
+
attachment: {
|
|
546
|
+
method: "GET",
|
|
547
|
+
path: "/attachments/:id"
|
|
548
|
+
},
|
|
549
|
+
/** A file too big for one request: the board answers where to `PUT` it in its blob store. Needs a store with `uploadUrl`. */
|
|
550
|
+
uploadStart: {
|
|
551
|
+
method: "POST",
|
|
552
|
+
path: "/attachments/uploads",
|
|
553
|
+
body: UploadStartBody
|
|
554
|
+
},
|
|
555
|
+
/** After the `PUT`: the board checks what landed in the store and records the attachment. */
|
|
556
|
+
uploadFinish: {
|
|
557
|
+
method: "POST",
|
|
558
|
+
path: "/attachments/uploads/:id"
|
|
559
|
+
},
|
|
560
|
+
openapi: {
|
|
561
|
+
method: "GET",
|
|
562
|
+
path: "/openapi.json"
|
|
563
|
+
},
|
|
564
|
+
/** Your linked platform accounts. */
|
|
565
|
+
identity: {
|
|
566
|
+
method: "GET",
|
|
567
|
+
path: "/identity"
|
|
568
|
+
},
|
|
569
|
+
/** Starts a sign-in that carries a code (no token), on the board that runs the entry's sync. */
|
|
570
|
+
signIn: {
|
|
571
|
+
method: "GET",
|
|
572
|
+
path: "/identity/sign-in",
|
|
573
|
+
query: SignInQuery
|
|
574
|
+
},
|
|
575
|
+
/** Where a sign-in comes back (no token): links the account it authorized to the code's person. */
|
|
576
|
+
signedIn: {
|
|
577
|
+
method: "GET",
|
|
578
|
+
path: "/identity/signed-in",
|
|
579
|
+
query: SignInQuery
|
|
580
|
+
},
|
|
581
|
+
identityUnlink: {
|
|
582
|
+
method: "POST",
|
|
583
|
+
path: "/identity/unlink",
|
|
584
|
+
body: IdentityUnlinkBody
|
|
585
|
+
},
|
|
586
|
+
/** Watchers registered by boards on `http()`: every one to `manage` everywhere, the caller's own to anyone else. */
|
|
587
|
+
watchers: {
|
|
588
|
+
method: "GET",
|
|
589
|
+
path: "/watchers"
|
|
590
|
+
},
|
|
591
|
+
/** Registers the caller's watchers, replacing its last registration. On a board on `http()`, registers its own, with no body or token. */
|
|
592
|
+
register: {
|
|
593
|
+
method: "POST",
|
|
594
|
+
path: "/watchers",
|
|
595
|
+
body: RegisterBody
|
|
596
|
+
},
|
|
597
|
+
/** The caller's matches it can run now, each leased to it until it acks. */
|
|
598
|
+
matches: {
|
|
599
|
+
method: "GET",
|
|
600
|
+
path: "/watchers/matches",
|
|
601
|
+
query: MatchesQuery
|
|
602
|
+
},
|
|
603
|
+
ack: {
|
|
604
|
+
method: "POST",
|
|
605
|
+
path: "/watchers/matches/:id/ack",
|
|
606
|
+
body: AckBody
|
|
607
|
+
},
|
|
608
|
+
/** On a board on `http()`: something matched. No body, no token: the board pulls what. */
|
|
609
|
+
wake: {
|
|
610
|
+
method: "POST",
|
|
611
|
+
path: "/wake"
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
//#endregion
|
|
615
|
+
//#region src/client/ids.ts
|
|
616
|
+
const hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
617
|
+
/** SHA-256 of `text`, in hex. */
|
|
618
|
+
async function sha256(text) {
|
|
619
|
+
return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text))));
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* A UUIDv8 from `parts`, exactly as A2's `idempotentId` makes one: the IDs a board derives for the
|
|
623
|
+
* writes its own code makes, which no request may post with.
|
|
624
|
+
*/
|
|
625
|
+
async function derivedId(...parts) {
|
|
626
|
+
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(parts)))).slice(0, 16);
|
|
627
|
+
bytes[6] = bytes[6] & 15 | 128;
|
|
628
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
629
|
+
const text = hex(bytes);
|
|
630
|
+
return `${text.slice(0, 8)}-${text.slice(8, 12)}-${text.slice(12, 16)}-${text.slice(16, 20)}-${text.slice(20, 32)}`;
|
|
631
|
+
}
|
|
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
|
+
//#endregion
|
|
645
|
+
//#region src/api/tail.ts
|
|
646
|
+
/** The filter flags `read`, `tail`, and `wait` share, and that a watcher's `tail` accepts. */
|
|
647
|
+
const FILTER_FLAGS = {
|
|
648
|
+
re: { type: "string" },
|
|
649
|
+
by: { type: "string" },
|
|
650
|
+
mentions: { type: "string" },
|
|
651
|
+
label: {
|
|
652
|
+
type: "string",
|
|
653
|
+
multiple: true
|
|
654
|
+
},
|
|
655
|
+
from: { type: "string" },
|
|
656
|
+
text: { type: "string" }
|
|
657
|
+
};
|
|
658
|
+
/** `--mentions` alone means mentions of me; `--mentions @ada`, mentions of ada. */
|
|
659
|
+
function withMentions(args) {
|
|
660
|
+
const out = [];
|
|
661
|
+
for (let i = 0; i < args.length; i++) {
|
|
662
|
+
const arg = args[i];
|
|
663
|
+
if (arg !== "--mentions") out.push(arg);
|
|
664
|
+
else if (args[i + 1]?.startsWith("@")) out.push(`--mentions=${args[++i]}`);
|
|
665
|
+
else out.push("--mentions=me");
|
|
666
|
+
}
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
/** Splits an expression the way a shell would: whitespace separates, quotes group, `\` escapes. */
|
|
670
|
+
function words(expression) {
|
|
671
|
+
const out = [];
|
|
672
|
+
let word = null;
|
|
673
|
+
let quote = null;
|
|
674
|
+
for (let i = 0; i < expression.length; i++) {
|
|
675
|
+
const char = expression[i];
|
|
676
|
+
if (quote) {
|
|
677
|
+
if (char === quote) quote = null;
|
|
678
|
+
else if (char === "\\" && quote === "\"" && i + 1 < expression.length) word += expression[++i];
|
|
679
|
+
else word += char;
|
|
680
|
+
} else if (/\s/.test(char)) {
|
|
681
|
+
if (word !== null) out.push(word);
|
|
682
|
+
word = null;
|
|
683
|
+
} else if (char === "\"" || char === "'") {
|
|
684
|
+
quote = char;
|
|
685
|
+
word ??= "";
|
|
686
|
+
} else if (char === "\\" && i + 1 < expression.length) word = (word ?? "") + expression[++i];
|
|
687
|
+
else word = (word ?? "") + char;
|
|
688
|
+
}
|
|
689
|
+
if (quote) throw new Error(`unterminated ${quote}`);
|
|
690
|
+
if (word !== null) out.push(word);
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
/** A watcher's tail takes the filters and `--all`, which shows receipts as it does in `vb tail`. */
|
|
694
|
+
const TAIL_FLAGS = {
|
|
695
|
+
...FILTER_FLAGS,
|
|
696
|
+
all: { type: "boolean" }
|
|
697
|
+
};
|
|
698
|
+
/**
|
|
699
|
+
* Parses filter flags as the CLI's parser does (Node's `parseArgs`, strict), written out so a tail
|
|
700
|
+
* parses wherever board() runs, a browser included.
|
|
701
|
+
*/
|
|
702
|
+
function parseFilterFlags(args) {
|
|
703
|
+
const values = {};
|
|
704
|
+
const positionals = [];
|
|
705
|
+
for (let i = 0; i < args.length; i++) {
|
|
706
|
+
const arg = args[i];
|
|
707
|
+
if (arg === "--") {
|
|
708
|
+
positionals.push(...args.slice(i + 1));
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
if (!arg.startsWith("-") || arg === "-") {
|
|
712
|
+
positionals.push(arg);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const eq = arg.indexOf("=");
|
|
716
|
+
const name = arg.startsWith("--") ? arg.slice(2, eq === -1 ? void 0 : eq) : null;
|
|
717
|
+
const spec = name !== null && Object.hasOwn(TAIL_FLAGS, name) ? TAIL_FLAGS[name] : null;
|
|
718
|
+
if (!spec) throw new Error(`${name !== null ? `--${name}` : arg.slice(0, 2)} isn't a filter`);
|
|
719
|
+
if (spec.type === "boolean") {
|
|
720
|
+
if (eq !== -1) throw new Error(`--${name} takes no value`);
|
|
721
|
+
values[name] = true;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
let value;
|
|
725
|
+
if (eq !== -1) value = arg.slice(eq + 1);
|
|
726
|
+
else {
|
|
727
|
+
const next = args[i + 1];
|
|
728
|
+
if (next === void 0 || next.length > 1 && next.startsWith("-")) throw new Error(`--${name} needs a value`);
|
|
729
|
+
value = next;
|
|
730
|
+
i++;
|
|
731
|
+
}
|
|
732
|
+
if ("multiple" in spec) values[name] = [...values[name] ?? [], value];
|
|
733
|
+
else values[name] = value;
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
values,
|
|
737
|
+
positionals
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
/** A watcher's `tail`: its filters, and whether `--all` shows it receipts too. */
|
|
741
|
+
function parseWatchTail(expression) {
|
|
742
|
+
const { values, positionals } = parseFilterFlags(withMentions(words(expression)));
|
|
743
|
+
if (positionals.length > 1) throw new Error(`expected one channel, got: ${positionals.join(" ")}`);
|
|
744
|
+
const channel = positionals[0];
|
|
745
|
+
const parsed = Filters.safeParse({
|
|
746
|
+
...channel ? { channel } : {},
|
|
747
|
+
...values.re ? { re: values.re.replace(/^#/, "") } : {},
|
|
748
|
+
...values.by ? { by: values.by.replace(/^@/, "") } : {},
|
|
749
|
+
...values.mentions ? { mentions: values.mentions.replace(/^@/, "") } : {},
|
|
750
|
+
...values.label?.length ? { label: values.label } : {},
|
|
751
|
+
...values.from ? { from: values.from } : {},
|
|
752
|
+
...values.text ? { text: values.text } : {}
|
|
753
|
+
});
|
|
754
|
+
if (!parsed.success) {
|
|
755
|
+
const issue = parsed.error.issues[0];
|
|
756
|
+
throw new Error(`${issue?.path.length ? `--${String(issue.path[0])}: ` : ""}${issue?.message ?? "invalid filters"}`);
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
filters: parsed.data,
|
|
760
|
+
all: values.all === true
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/server/watch-spec.ts
|
|
765
|
+
/** Whether filters narrow anything: a view a message can enter. */
|
|
766
|
+
const hasCriteria = (filters) => filters.channel !== void 0 || filters.re !== void 0 || filters.by !== void 0 || filters.mentions !== void 0 || filters.from !== void 0 || filters.text !== void 0 || (filters.label?.length ?? 0) > 0;
|
|
767
|
+
/** A watcher's tail, checked the way a boot checks it: errors name the watcher. */
|
|
768
|
+
function compileWatcher(id, spec) {
|
|
769
|
+
let filters;
|
|
770
|
+
let all;
|
|
771
|
+
try {
|
|
772
|
+
({filters, all} = parseWatchTail(spec.tail));
|
|
773
|
+
} catch (error) {
|
|
774
|
+
throw new Error(`watch ${id}: ${error.message}`);
|
|
775
|
+
}
|
|
776
|
+
if (filters.mentions === "me") throw new Error(`watch ${id}: a watcher has no "me": use --mentions @handle`);
|
|
777
|
+
const criteria = hasCriteria(filters);
|
|
778
|
+
if (spec.when === "enters" && !criteria) throw new Error(`watch ${id}: \`when: enters\` needs a filter to enter`);
|
|
779
|
+
return {
|
|
780
|
+
filters,
|
|
781
|
+
criteria,
|
|
782
|
+
enters: spec.when === "enters",
|
|
783
|
+
all
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
//#endregion
|
|
787
|
+
export { SyncBody as A, UploadStartBody as B, Ref as C, SignInQuery as D, SchedulesQuery as E, SyncReceiptBody as F, SyncRootsQuery as I, SyncUnlinkBody as L, SyncCopiesQuery as M, SyncFindQuery as N, SourceReadQuery as O, SyncImportBody as P, ThreadsQuery as R, ReactBody as S, ScheduleBody as T, routes as V, InboxQuery as _, DERIVED_ID as a, MessagesQuery as b, keyId as c, EditBody as d, EventsQuery as f, InboxPushBody as g, InboxNextQuery as h, withMentions as i, SyncCodeBody as j, SyncAccountBody as k, sha256 as l, InboxAckBody as m, hasCriteria as n, derivedId as o, IdentityUnlinkBody as p, FILTER_FLAGS as r, isKeyIdOf as s, compileWatcher as t, AckBody as u, LabelBody as v, RegisterBody as w, PostBody as x, MatchesQuery as y, TypingBody as z };
|