@vercel/board 0.0.0-stage → 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.
Files changed (126) hide show
  1. package/README.md +179 -2
  2. package/dist/a2-5EG8FkZ8.d.mts +3359 -0
  3. package/dist/a2-DYGYankn.mjs +3045 -0
  4. package/dist/access-CJU2-vaa.mjs +226 -0
  5. package/dist/access-CV96SYgr.d.mts +32 -0
  6. package/dist/access.d.mts +29 -0
  7. package/dist/access.mjs +4 -0
  8. package/dist/actors-rdN4dDTU.mjs +223 -0
  9. package/dist/attempts-BQUp9MKO.mjs +919 -0
  10. package/dist/auth-COzc09sn.mjs +276 -0
  11. package/dist/auth-wHFcAte3.d.mts +203 -0
  12. package/dist/backend-a2/blob-vercel.d.mts +8 -0
  13. package/dist/backend-a2/blob-vercel.mjs +81 -0
  14. package/dist/backend-a2/contract.d.mts +18 -0
  15. package/dist/backend-a2/contract.mjs +5 -0
  16. package/dist/backend-a2/scheduler-local.d.mts +11 -0
  17. package/dist/backend-a2/scheduler-local.mjs +2 -0
  18. package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
  19. package/dist/backend-a2/scheduler-qstash.mjs +2 -0
  20. package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
  21. package/dist/backend-a2/scheduler-vercel.mjs +2 -0
  22. package/dist/backend-a2/store-memory.d.mts +11 -0
  23. package/dist/backend-a2/store-memory.mjs +19 -0
  24. package/dist/backend-a2/store-postgres.d.mts +38 -0
  25. package/dist/backend-a2/store-postgres.mjs +110 -0
  26. package/dist/backend-a2/store-redis-http.d.mts +17 -0
  27. package/dist/backend-a2/store-redis-http.mjs +32 -0
  28. package/dist/backend-a2/store-redis.d.mts +23 -0
  29. package/dist/backend-a2/store-redis.mjs +39 -0
  30. package/dist/backend-a2/store-sqlite.d.mts +12 -0
  31. package/dist/backend-a2/store-sqlite.mjs +31 -0
  32. package/dist/backend-a2.d.mts +4 -0
  33. package/dist/backend-a2.mjs +2 -0
  34. package/dist/backend-http-8ex5g7Wv.d.mts +630 -0
  35. package/dist/backend-http-BsDSgmeo.mjs +613 -0
  36. package/dist/backend-http-Cv6xbEy9.mjs +469 -0
  37. package/dist/backend-http.d.mts +10 -0
  38. package/dist/backend-http.mjs +2 -0
  39. package/dist/backend-http.node.d.mts +10 -0
  40. package/dist/backend-http.node.mjs +2 -0
  41. package/dist/blob-C0J3wbGv.d.mts +39 -0
  42. package/dist/bots-CGkO76qH.mjs +67 -0
  43. package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
  44. package/dist/chat-provider-jFSeyNmB.mjs +389 -0
  45. package/dist/cli.d.mts +1 -0
  46. package/dist/cli.mjs +2252 -0
  47. package/dist/client-DGDPaN7U.mjs +380 -0
  48. package/dist/conformance-DRR23YNy.mjs +512 -0
  49. package/dist/conformance.d.mts +30 -0
  50. package/dist/conformance.mjs +2 -0
  51. package/dist/contract-CuJmlrhw.mjs +566 -0
  52. package/dist/emoji-CTLpvH-A.mjs +445 -0
  53. package/dist/engine-3EvQ8idk.mjs +1449 -0
  54. package/dist/errors-DX3hpwWt.mjs +64 -0
  55. package/dist/exports-BpgyEdh6.d.mts +4 -0
  56. package/dist/exports-CyY3_FjZ.mjs +1285 -0
  57. package/dist/files-SniSmeEr.mjs +47 -0
  58. package/dist/index.d.mts +13 -0
  59. package/dist/index.default-BZ3lItjH.mjs +12 -0
  60. package/dist/index.mjs +19 -0
  61. package/dist/index.node.d.mts +14 -0
  62. package/dist/index.node.mjs +5 -0
  63. package/dist/inspect/app.js +416 -0
  64. package/dist/inspect/engine.js +5 -0
  65. package/dist/local-Cr_dSjvs.mjs +44 -0
  66. package/dist/own-OwgPWdmh.mjs +15 -0
  67. package/dist/project-CFiArraX.mjs +124 -0
  68. package/dist/provider-BGjz-mDa.d.mts +2473 -0
  69. package/dist/provider-CuewcDNZ.mjs +205 -0
  70. package/dist/redis-tables-B35vv4CZ.mjs +842 -0
  71. package/dist/reducers-B_vUGQRp.d.mts +1172 -0
  72. package/dist/reducers-mUPFuFJV.mjs +293 -0
  73. package/dist/schemas-Be08nyfW.mjs +74 -0
  74. package/dist/schemas-CWxs3ECJ.d.mts +50 -0
  75. package/dist/serve-Ccbb2LSb.mjs +170 -0
  76. package/dist/server-Cxey38B-.mjs +17 -0
  77. package/dist/serving-BNbzc96L.mjs +2 -0
  78. package/dist/sql-DbiEnqM0.mjs +571 -0
  79. package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
  80. package/dist/sync-github.d.mts +473 -0
  81. package/dist/sync-github.mjs +603 -0
  82. package/dist/sync-kapso.d.mts +175 -0
  83. package/dist/sync-kapso.mjs +86 -0
  84. package/dist/sync-slack.d.mts +364 -0
  85. package/dist/sync-slack.mjs +728 -0
  86. package/dist/sync.chat.d.mts +2 -0
  87. package/dist/sync.chat.mjs +2 -0
  88. package/dist/sync.d.mts +3 -0
  89. package/dist/sync.mjs +4 -0
  90. package/dist/targets-ojFEXEyN.mjs +94 -0
  91. package/dist/tokens-DKLn5P9B.mjs +256 -0
  92. package/dist/types-BHCjTZdB.d.mts +104 -0
  93. package/dist/users-Dls7_ANV.mjs +42 -0
  94. package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
  95. package/dist/watchers-hash-D9hbHLKB.mjs +225 -0
  96. package/docs/01-board/01-mounting.md +506 -0
  97. package/docs/01-board/02-database.md +258 -0
  98. package/docs/01-board/03-identity.md +262 -0
  99. package/docs/01-board/04-messages.md +192 -0
  100. package/docs/01-board/05-reading.md +177 -0
  101. package/docs/01-board/06-watchers.md +249 -0
  102. package/docs/01-board/07-schedules.md +133 -0
  103. package/docs/01-board/08-middleware.md +139 -0
  104. package/docs/01-board/09-access.md +499 -0
  105. package/docs/01-quickstart.md +196 -0
  106. package/docs/02-sync/01-overview.md +290 -0
  107. package/docs/02-sync/02-callbacks.md +265 -0
  108. package/docs/02-sync/03-slack.md +248 -0
  109. package/docs/02-sync/04-github.md +201 -0
  110. package/docs/02-sync/05-whatsapp.md +148 -0
  111. package/docs/02-sync/06-providers.md +228 -0
  112. package/docs/03-guides/01-coding-agents.md +226 -0
  113. package/docs/03-guides/02-inspector.md +104 -0
  114. package/docs/03-guides/03-ci.md +251 -0
  115. package/docs/03-guides/04-triage.md +233 -0
  116. package/docs/03-guides/05-agent-factory.md +142 -0
  117. package/docs/04-reference/01-cli.md +606 -0
  118. package/docs/04-reference/02-board.md +579 -0
  119. package/docs/04-reference/03-http.md +489 -0
  120. package/docs/04-reference/04-log.md +250 -0
  121. package/docs/04-reference/05-conformance.md +97 -0
  122. package/docs/index.md +186 -0
  123. package/package.json +165 -3
  124. package/sql/postgres/board.sql +145 -0
  125. package/sql/postgres/notifications.sql +24 -0
  126. 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 };