@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,566 @@
|
|
|
1
|
+
import { S as storable, _ as RemoteWatcherId, c as ChannelPath, d as LabelKey, g as MessageText, h as MessageId, i as Attachment, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, o as Author, p as Labels, u as Handle, v as SCOPE_MAX, y as SeenActorKind } from "./schemas-Be08nyfW.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { contract } from "experimental-a2";
|
|
4
|
+
//#region src/contract/contract.ts
|
|
5
|
+
/**
|
|
6
|
+
* An external thread or channel, prefixed with its sync key:
|
|
7
|
+
* `slack:C0123ABC` (a channel), `slack:C0123ABC:1727.1234` (a thread),
|
|
8
|
+
* `github:basehub-ai/www:12` (a PR thread).
|
|
9
|
+
*/
|
|
10
|
+
const ExternalTarget = z.string().min(3).max(300);
|
|
11
|
+
/** Where a message came from on a platform. `thread` is grouped (GitHub review threads fold into their PR). */
|
|
12
|
+
const Origin = z.object({
|
|
13
|
+
channel: ExternalTarget.optional(),
|
|
14
|
+
thread: ExternalTarget,
|
|
15
|
+
/** The exact platform thread, when it differs from `thread`. */
|
|
16
|
+
subthread: ExternalTarget.nullable(),
|
|
17
|
+
message: z.string().min(1).max(300),
|
|
18
|
+
context: z.string().max(300).nullable()
|
|
19
|
+
});
|
|
20
|
+
/** What a linked message said, as the board saw it when the link arrived. */
|
|
21
|
+
const LinkSnapshot = z.object({
|
|
22
|
+
author: z.string().min(1).max(256),
|
|
23
|
+
text: z.string().max(MESSAGE_TEXT_MAX),
|
|
24
|
+
/** When it was written, if the platform said. */
|
|
25
|
+
createdAt: z.string().max(64).nullable(),
|
|
26
|
+
/** Its files, linked rather than copied. */
|
|
27
|
+
files: z.array(z.object({
|
|
28
|
+
name: z.string().min(1).max(1e3),
|
|
29
|
+
url: z.string().max(4e3).nullable()
|
|
30
|
+
})).max(20)
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* A platform message linked from a message (a Slack permalink): the board message
|
|
34
|
+
* it was synced to, if any, and a snapshot of it, or why the board couldn't read it.
|
|
35
|
+
*/
|
|
36
|
+
const Link = z.object({
|
|
37
|
+
url: z.string().min(1).max(4e3),
|
|
38
|
+
message: MessageId.nullable(),
|
|
39
|
+
snapshot: LinkSnapshot.nullable(),
|
|
40
|
+
/** Why there's no snapshot: `not_in_channel`, `channel_not_found`. */
|
|
41
|
+
unavailable: z.string().max(300).nullable()
|
|
42
|
+
});
|
|
43
|
+
/** The sync key a write came from, or null when it was written on the board. */
|
|
44
|
+
const From = z.string().nullable();
|
|
45
|
+
/** The thread key: the event ID of the thread's root. Lanes and sync state are keyed by it. */
|
|
46
|
+
const ThreadKey = z.string().min(1).max(200).refine(storable, "thread key has a NUL character or a lone surrogate");
|
|
47
|
+
/** The board's one log. Every board fact is an event in session `board`. */
|
|
48
|
+
const boardContract = contract({
|
|
49
|
+
name: "board",
|
|
50
|
+
events: {
|
|
51
|
+
"message.posted": z.object({
|
|
52
|
+
channel: ChannelPath,
|
|
53
|
+
/** Empty only when the message has attachments. */
|
|
54
|
+
text: z.union([MessageText, z.literal("")]),
|
|
55
|
+
/** The thread root, or null for a root. */
|
|
56
|
+
re: MessageId.nullable(),
|
|
57
|
+
/** The exact message replied to, when it isn't the root. */
|
|
58
|
+
replyTo: MessageId.nullable(),
|
|
59
|
+
thread: ThreadKey,
|
|
60
|
+
labels: Labels,
|
|
61
|
+
noSync: z.boolean(),
|
|
62
|
+
mentions: z.array(ActorId),
|
|
63
|
+
by: Author,
|
|
64
|
+
origin: Origin.nullable(),
|
|
65
|
+
from: From,
|
|
66
|
+
/** Files on the message. Events written before attachments have none. */
|
|
67
|
+
attachments: z.array(Attachment).max(10).optional(),
|
|
68
|
+
/** Platform messages it links to. Events written before links have none. */
|
|
69
|
+
links: z.array(Link).max(10).optional(),
|
|
70
|
+
/** (server) Posted by a schedule: its ID and which send this is, from 0. Other messages have none. */
|
|
71
|
+
schedule: z.object({
|
|
72
|
+
id: z.number().int().positive(),
|
|
73
|
+
n: z.number().int().nonnegative()
|
|
74
|
+
}).optional(),
|
|
75
|
+
/**
|
|
76
|
+
* (server) Its author couldn't read the channel it posted in. Where a
|
|
77
|
+
* caller can't read, it edits and deletes only these of its own. Events
|
|
78
|
+
* from before channel access have none: everyone read everything then.
|
|
79
|
+
*/
|
|
80
|
+
blind: z.literal(true).optional(),
|
|
81
|
+
/**
|
|
82
|
+
* (server) The principal of the sync elsewhere that imported it, whose copy `origin` names:
|
|
83
|
+
* that sync's to act on. None for the board's own sync, or a write on the board.
|
|
84
|
+
*/
|
|
85
|
+
importedBy: ActorId.optional()
|
|
86
|
+
}),
|
|
87
|
+
/** A message's links changed: an edit linked something new, or the platform sent what a link says. */
|
|
88
|
+
"message.linked": z.object({
|
|
89
|
+
id: MessageId,
|
|
90
|
+
thread: ThreadKey,
|
|
91
|
+
links: z.array(Link).max(10),
|
|
92
|
+
by: Author,
|
|
93
|
+
from: From
|
|
94
|
+
}),
|
|
95
|
+
"message.edited": z.object({
|
|
96
|
+
id: MessageId,
|
|
97
|
+
thread: ThreadKey,
|
|
98
|
+
text: MessageText,
|
|
99
|
+
mentions: z.array(ActorId),
|
|
100
|
+
by: Author,
|
|
101
|
+
from: From
|
|
102
|
+
}),
|
|
103
|
+
"message.deleted": z.object({
|
|
104
|
+
id: MessageId,
|
|
105
|
+
thread: ThreadKey,
|
|
106
|
+
by: Author,
|
|
107
|
+
from: From
|
|
108
|
+
}),
|
|
109
|
+
"reaction.added": z.object({
|
|
110
|
+
id: MessageId,
|
|
111
|
+
thread: ThreadKey,
|
|
112
|
+
emoji: Emoji,
|
|
113
|
+
by: Author,
|
|
114
|
+
from: From
|
|
115
|
+
}),
|
|
116
|
+
"reaction.removed": z.object({
|
|
117
|
+
id: MessageId,
|
|
118
|
+
thread: ThreadKey,
|
|
119
|
+
emoji: Emoji,
|
|
120
|
+
by: Author,
|
|
121
|
+
from: From
|
|
122
|
+
}),
|
|
123
|
+
"labels.changed": z.object({
|
|
124
|
+
id: MessageId,
|
|
125
|
+
thread: ThreadKey,
|
|
126
|
+
set: Labels,
|
|
127
|
+
unset: z.array(LabelKey),
|
|
128
|
+
by: Author
|
|
129
|
+
}),
|
|
130
|
+
/** A thread is synced with an external channel (start a thread there) or thread (join it). */
|
|
131
|
+
"thread.synced": z.object({
|
|
132
|
+
thread: ThreadKey,
|
|
133
|
+
target: ExternalTarget,
|
|
134
|
+
cause: z.enum(["config", "request"]),
|
|
135
|
+
kind: z.enum(["channel", "thread"]).optional(),
|
|
136
|
+
by: Author
|
|
137
|
+
}),
|
|
138
|
+
/** An outbound delivery receipt: board message `id` exists on the platform as `external`. */
|
|
139
|
+
"message.synced": z.object({
|
|
140
|
+
id: MessageId,
|
|
141
|
+
thread: ThreadKey,
|
|
142
|
+
target: ExternalTarget,
|
|
143
|
+
external: z.object({
|
|
144
|
+
thread: ExternalTarget,
|
|
145
|
+
message: z.string().min(1).max(300),
|
|
146
|
+
/** Posted with its author's own platform token, so it shows as them. Older receipts have none. */
|
|
147
|
+
asUser: z.literal(true).optional()
|
|
148
|
+
}),
|
|
149
|
+
/**
|
|
150
|
+
* (server) The principal of the sync elsewhere that recorded it: its copy is that sync's to
|
|
151
|
+
* change. None for the board's own sync.
|
|
152
|
+
*/
|
|
153
|
+
syncedBy: ActorId.optional()
|
|
154
|
+
}),
|
|
155
|
+
/**
|
|
156
|
+
* Board message `id`'s files reached `target` after its text: `files` are the
|
|
157
|
+
* platform's IDs for them, or null when the platform refused files and the
|
|
158
|
+
* text got a line for each instead.
|
|
159
|
+
*/
|
|
160
|
+
"message.files_synced": z.object({
|
|
161
|
+
id: MessageId,
|
|
162
|
+
thread: ThreadKey,
|
|
163
|
+
target: ExternalTarget,
|
|
164
|
+
files: z.array(z.string().min(1).max(300)).max(10).nullable(),
|
|
165
|
+
/** (server) As on `message.synced`: only the sync that recorded the copy sets its files. */
|
|
166
|
+
syncedBy: ActorId.optional()
|
|
167
|
+
}),
|
|
168
|
+
/** A delivery that gave up: board message `id` didn't reach `target`, and `error` says why. */
|
|
169
|
+
"message.sync_failed": z.object({
|
|
170
|
+
id: MessageId,
|
|
171
|
+
thread: ThreadKey,
|
|
172
|
+
target: ExternalTarget,
|
|
173
|
+
error: z.string().min(1).max(500)
|
|
174
|
+
}),
|
|
175
|
+
"actor.seen": z.object({
|
|
176
|
+
actor: ActorId,
|
|
177
|
+
kind: SeenActorKind,
|
|
178
|
+
handle: Handle,
|
|
179
|
+
name: z.string().min(1).max(128).refine(storable, "name has a NUL character or a lone surrogate"),
|
|
180
|
+
avatar: z.string().url().nullable(),
|
|
181
|
+
/**
|
|
182
|
+
* (server) A principal a host's `authenticate` made, or one of its profiles: someone the board
|
|
183
|
+
* signs in, whatever its ID or kind, so sync never writes as it. Others have none.
|
|
184
|
+
*/
|
|
185
|
+
host: z.literal(true).optional()
|
|
186
|
+
}),
|
|
187
|
+
/**
|
|
188
|
+
* A platform actor (`slack:user:U0456`) is the same person as a principal
|
|
189
|
+
* (`vercel:user:…`): for display, `--by`, and mentions, never for permissions.
|
|
190
|
+
* `method` says how it was proved: a one-time code posted from that account,
|
|
191
|
+
* or the account's own sign-in (`consent`).
|
|
192
|
+
*/
|
|
193
|
+
"actor.linked": z.object({
|
|
194
|
+
actor: ActorId,
|
|
195
|
+
to: ActorId,
|
|
196
|
+
method: z.enum(["code", "consent"]),
|
|
197
|
+
by: Author
|
|
198
|
+
}),
|
|
199
|
+
"actor.unlinked": z.object({
|
|
200
|
+
actor: ActorId,
|
|
201
|
+
by: Author
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
const BOARD_SESSION = "board";
|
|
206
|
+
const ExternalAuthor = z.object({
|
|
207
|
+
/** The platform principal, e.g. `slack:user:U0456`. */
|
|
208
|
+
id: z.string().min(1),
|
|
209
|
+
handle: z.string().min(1),
|
|
210
|
+
name: z.string().min(1),
|
|
211
|
+
avatar: z.string().nullable(),
|
|
212
|
+
isBot: z.boolean()
|
|
213
|
+
});
|
|
214
|
+
/**
|
|
215
|
+
* A file on a platform message, as Chat SDK describes it: enough for the
|
|
216
|
+
* platform's adapter to fetch it later (`rehydrateAttachment`), never the bytes.
|
|
217
|
+
*/
|
|
218
|
+
const ExternalFile = z.object({
|
|
219
|
+
type: z.enum([
|
|
220
|
+
"image",
|
|
221
|
+
"file",
|
|
222
|
+
"video",
|
|
223
|
+
"audio"
|
|
224
|
+
]),
|
|
225
|
+
name: z.string().max(1e3).optional(),
|
|
226
|
+
mimeType: z.string().max(255).optional(),
|
|
227
|
+
size: z.number().int().nonnegative().optional(),
|
|
228
|
+
url: z.string().max(4e3).optional(),
|
|
229
|
+
fetchMetadata: z.record(z.string(), z.string().max(4e3)).optional()
|
|
230
|
+
});
|
|
231
|
+
/**
|
|
232
|
+
* A platform message a platform message links to, at intake: where it lives
|
|
233
|
+
* (`thread`, with the sync key prefix, and `message`), and what it said when the
|
|
234
|
+
* platform sent that along (Slack's unfurl).
|
|
235
|
+
*/
|
|
236
|
+
const ExternalLink = z.object({
|
|
237
|
+
url: z.string().min(1).max(4e3),
|
|
238
|
+
thread: ExternalTarget,
|
|
239
|
+
message: z.string().min(1).max(300),
|
|
240
|
+
snapshot: LinkSnapshot.nullable()
|
|
241
|
+
});
|
|
242
|
+
const ExternalLocation = z.object({
|
|
243
|
+
channel: ExternalTarget,
|
|
244
|
+
thread: ExternalTarget,
|
|
245
|
+
subthread: ExternalTarget.nullable()
|
|
246
|
+
});
|
|
247
|
+
/** A platform event, normalized at intake. Thread IDs are external IDs with the sync key prefix. */
|
|
248
|
+
const ExternalEvent = z.discriminatedUnion("kind", [
|
|
249
|
+
z.object({
|
|
250
|
+
kind: z.literal("message"),
|
|
251
|
+
...ExternalLocation.shape,
|
|
252
|
+
message: z.string(),
|
|
253
|
+
isRoot: z.boolean(),
|
|
254
|
+
text: z.string(),
|
|
255
|
+
context: z.string().nullable(),
|
|
256
|
+
author: ExternalAuthor,
|
|
257
|
+
/** Files on the message, fetched when it lands on the board. Older events have none. */
|
|
258
|
+
files: z.array(ExternalFile).max(20).optional(),
|
|
259
|
+
/** Platform messages it links to. Older events have none. */
|
|
260
|
+
links: z.array(ExternalLink).max(10).optional()
|
|
261
|
+
}),
|
|
262
|
+
z.object({
|
|
263
|
+
kind: z.literal("edited"),
|
|
264
|
+
...ExternalLocation.shape,
|
|
265
|
+
message: z.string(),
|
|
266
|
+
text: z.string(),
|
|
267
|
+
author: ExternalAuthor,
|
|
268
|
+
links: z.array(ExternalLink).max(10).optional(),
|
|
269
|
+
/** Only the links changed (the platform sent what they say), so `text` isn't news. */
|
|
270
|
+
linksOnly: z.boolean().optional()
|
|
271
|
+
}),
|
|
272
|
+
z.object({
|
|
273
|
+
kind: z.literal("deleted"),
|
|
274
|
+
...ExternalLocation.shape,
|
|
275
|
+
message: z.string()
|
|
276
|
+
}),
|
|
277
|
+
z.object({
|
|
278
|
+
kind: z.literal("reaction"),
|
|
279
|
+
...ExternalLocation.shape,
|
|
280
|
+
message: z.string(),
|
|
281
|
+
emoji: z.string(),
|
|
282
|
+
added: z.boolean(),
|
|
283
|
+
author: ExternalAuthor
|
|
284
|
+
}),
|
|
285
|
+
z.object({
|
|
286
|
+
/** A platform lifecycle event: a PR opening, closing, being reviewed. */
|
|
287
|
+
kind: z.literal("lifecycle"),
|
|
288
|
+
...ExternalLocation.omit({ subthread: true }).shape,
|
|
289
|
+
type: z.string(),
|
|
290
|
+
title: z.string(),
|
|
291
|
+
url: z.string().nullable(),
|
|
292
|
+
labels: z.record(z.string(), z.string()),
|
|
293
|
+
unset: z.array(z.string()),
|
|
294
|
+
/** Key prefixes the platform owns on the root, e.g. `tag.`: keys under them that aren't in `labels` are unset. */
|
|
295
|
+
owns: z.array(z.string()).optional(),
|
|
296
|
+
/** A reply to add to the thread, e.g. "merged by julian". */
|
|
297
|
+
reply: z.string().nullable(),
|
|
298
|
+
/** Create the root if the board hasn't seen this thread. */
|
|
299
|
+
createRoot: z.boolean(),
|
|
300
|
+
author: ExternalAuthor
|
|
301
|
+
})
|
|
302
|
+
]);
|
|
303
|
+
/** Raw platform events live outside the board log, one session per external channel. */
|
|
304
|
+
const inboxContract = contract({
|
|
305
|
+
name: "board-inbox",
|
|
306
|
+
events: {
|
|
307
|
+
"external.received": z.object({
|
|
308
|
+
sync: z.string().min(1),
|
|
309
|
+
event: ExternalEvent,
|
|
310
|
+
raw: z.unknown()
|
|
311
|
+
}),
|
|
312
|
+
/** An inbox got its first event: session `inboxes` lists them, so a sync key's inbox lists every channel's. */
|
|
313
|
+
"inbox.opened": z.object({ channel: ExternalTarget })
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
/** `inbox:slack:C0123ABC`: the inbox session for one external channel. */
|
|
317
|
+
const inboxSession = (channel) => `inbox:${channel}`;
|
|
318
|
+
/** The session that lists the inboxes: one `inbox.opened` each. */
|
|
319
|
+
const INBOXES_SESSION = "inboxes";
|
|
320
|
+
/** What watchers matched, outside the board log: one session per watcher, so each fails on its own. */
|
|
321
|
+
const MatchId = z.string().min(1).max(200);
|
|
322
|
+
const LeaseId = z.string().min(1).max(64);
|
|
323
|
+
/** What a queue keeps in order: a board thread's key, or a platform thread for an inbox. */
|
|
324
|
+
const QueueLane = z.string().min(1).max(300).refine(storable, "lane has a NUL character or a lone surrogate");
|
|
325
|
+
const watchContract = contract({
|
|
326
|
+
name: "board-watch",
|
|
327
|
+
events: {
|
|
328
|
+
"watch.matched": z.object({
|
|
329
|
+
/** The board thread, or for an inbox the platform thread: each one's matches go one at a time. */
|
|
330
|
+
thread: QueueLane,
|
|
331
|
+
/** The board event, as `vb tail --json` prints it, as the watcher may see it, or a platform event for an inbox. */
|
|
332
|
+
event: z.unknown(),
|
|
333
|
+
/** A registered watcher's ID: its registrant's session holds all of its watchers' matches. */
|
|
334
|
+
watcher: RemoteWatcherId.optional(),
|
|
335
|
+
/** The registrant's sync, which gets every event it delivers to platforms. */
|
|
336
|
+
sync: z.literal(true).optional(),
|
|
337
|
+
/** A platform event the registrant pushed to its inbox for this sync key, to pull back and import. */
|
|
338
|
+
inbox: z.string().min(1).max(64).optional()
|
|
339
|
+
}),
|
|
340
|
+
/** The watcher gave up on one match: `run` kept failing, and the thread's later events go on without it. */
|
|
341
|
+
"watch.failed": z.object({
|
|
342
|
+
/** The `watch.matched` event's ID. */
|
|
343
|
+
matched: MatchId,
|
|
344
|
+
thread: QueueLane,
|
|
345
|
+
error: z.string().min(1).max(500)
|
|
346
|
+
}),
|
|
347
|
+
/** A registrant pulled a match: it's theirs to run until `until`, or until they ack it. */
|
|
348
|
+
"watch.leased": z.object({
|
|
349
|
+
matched: MatchId,
|
|
350
|
+
lease: LeaseId,
|
|
351
|
+
until: z.number().int().nonnegative()
|
|
352
|
+
}),
|
|
353
|
+
/** The registrant ran it: done, or an error to try again after `retryAt`. A stale lease's ack changes nothing. */
|
|
354
|
+
"watch.acked": z.object({
|
|
355
|
+
matched: MatchId,
|
|
356
|
+
lease: LeaseId,
|
|
357
|
+
error: z.string().min(1).max(500).optional(),
|
|
358
|
+
retryAt: z.number().int().nonnegative().optional()
|
|
359
|
+
}),
|
|
360
|
+
/** A lease ran out with no ack: a try that didn't finish. */
|
|
361
|
+
"watch.expired": z.object({
|
|
362
|
+
matched: MatchId,
|
|
363
|
+
lease: LeaseId
|
|
364
|
+
}),
|
|
365
|
+
/** A match's wait after an error is over: it can be pulled again. */
|
|
366
|
+
"watch.retry": z.object({ matched: MatchId }),
|
|
367
|
+
/** A match its registrant can't have anymore: it may not read it, or the watcher is gone. */
|
|
368
|
+
"watch.skipped": z.object({
|
|
369
|
+
matched: MatchId,
|
|
370
|
+
reason: z.string().min(1).max(200)
|
|
371
|
+
}),
|
|
372
|
+
/** A sweep is set for `at`: one timer for every lease that runs out before it, or a wake-up to try again. */
|
|
373
|
+
"watch.armed": z.object({ at: z.number().int().nonnegative() }),
|
|
374
|
+
/** A timer: an error's wait is over (`matched`), or a sweep (`sweep`). Its consumer decides what it still means. */
|
|
375
|
+
"watch.due": z.object({
|
|
376
|
+
matched: MatchId.optional(),
|
|
377
|
+
retry: z.boolean().optional(),
|
|
378
|
+
sweep: z.number().int().nonnegative().optional()
|
|
379
|
+
}),
|
|
380
|
+
/** A wake-up didn't get there, `failures` in a row: nothing wakes the registrant again before `until`. */
|
|
381
|
+
"watch.unreachable": z.object({
|
|
382
|
+
failures: z.number().int().positive(),
|
|
383
|
+
until: z.number().int().nonnegative(),
|
|
384
|
+
/** Its answer's status, or the error. */
|
|
385
|
+
why: z.string().min(1).max(200)
|
|
386
|
+
}),
|
|
387
|
+
/** The registrant's board answered a wake-up, pulled, or registered: its next match wakes it. */
|
|
388
|
+
"watch.reachable": z.object({ by: z.enum([
|
|
389
|
+
"wake",
|
|
390
|
+
"pull",
|
|
391
|
+
"registration"
|
|
392
|
+
]) })
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
/** `watch:triage`: the session for one watcher, or `watch:@<principal>` for everything a registrant registered. */
|
|
396
|
+
const watchSession = (id) => `watch:${id}`;
|
|
397
|
+
/**
|
|
398
|
+
* A principal as the board keeps it, to work out its access again later under
|
|
399
|
+
* the rules as they are then: who it is, and the token facts rules match on.
|
|
400
|
+
*/
|
|
401
|
+
const StoredPrincipal = z.object({
|
|
402
|
+
id: z.string().min(1),
|
|
403
|
+
kind: z.enum([
|
|
404
|
+
"person",
|
|
405
|
+
"deployment",
|
|
406
|
+
"workflow",
|
|
407
|
+
"external"
|
|
408
|
+
]),
|
|
409
|
+
handle: Handle,
|
|
410
|
+
facts: z.object({
|
|
411
|
+
issuer: z.enum([
|
|
412
|
+
"vercel",
|
|
413
|
+
"github",
|
|
414
|
+
"oidc",
|
|
415
|
+
"host"
|
|
416
|
+
]),
|
|
417
|
+
iss: z.string().max(1e3),
|
|
418
|
+
claims: z.record(z.string(), z.union([
|
|
419
|
+
z.string().max(1e3),
|
|
420
|
+
z.number(),
|
|
421
|
+
z.boolean()
|
|
422
|
+
])),
|
|
423
|
+
/** The board's own Vercel team, as the board knew it when it verified the token. */
|
|
424
|
+
team: z.string().max(200).optional()
|
|
425
|
+
}).optional(),
|
|
426
|
+
scope: z.string().max(SCOPE_MAX).optional()
|
|
427
|
+
});
|
|
428
|
+
/** A watcher as a board on `http()` registers it: its tail, and when it runs. */
|
|
429
|
+
const WatcherSpec = z.object({
|
|
430
|
+
tail: z.string().max(1e3),
|
|
431
|
+
when: z.enum(["matches", "enters"])
|
|
432
|
+
});
|
|
433
|
+
/**
|
|
434
|
+
* Watchers that boards on `http()` registered with this one, outside the board
|
|
435
|
+
* log: one session, `registry`. Each principal has one registration, and its
|
|
436
|
+
* newest replaces the last.
|
|
437
|
+
*/
|
|
438
|
+
const registryContract = contract({
|
|
439
|
+
name: "board-registry",
|
|
440
|
+
events: { "watchers.registered": z.object({
|
|
441
|
+
/** The principal that registered, which its watchers' writes are by, with their IDs as profiles. */
|
|
442
|
+
owner: z.string().min(1).max(200),
|
|
443
|
+
/** It, to work out what it may read at each match: a watcher sees only what its registrant can. */
|
|
444
|
+
principal: StoredPrincipal,
|
|
445
|
+
/** Where to wake it: `<url>/wake`. Without one, it pulls on its own. */
|
|
446
|
+
url: z.string().url().max(500).nullable(),
|
|
447
|
+
watch: z.record(RemoteWatcherId, WatcherSpec),
|
|
448
|
+
/** Its sync entries: with any, its sync subscribes too, to every post, change, and thread sync where it may sync. Older registrations have none. */
|
|
449
|
+
sync: z.array(z.object({
|
|
450
|
+
key: z.string().min(1).max(64),
|
|
451
|
+
platform: z.string().min(1).max(64),
|
|
452
|
+
signIn: z.boolean()
|
|
453
|
+
})).max(32).optional(),
|
|
454
|
+
/** `watchersHash(watch)`, which every wake-up carries so the board can tell it's stale. */
|
|
455
|
+
hash: z.string().min(1).max(100)
|
|
456
|
+
}) }
|
|
457
|
+
});
|
|
458
|
+
const REGISTRY_SESSION = "registry";
|
|
459
|
+
/** Epoch milliseconds. */
|
|
460
|
+
const Millis = z.number().int().nonnegative();
|
|
461
|
+
/**
|
|
462
|
+
* Who a schedule posts as: the identity that created it, as the board resolved
|
|
463
|
+
* it then. A token's verified claims (`facts`) and the scope it was narrowed by
|
|
464
|
+
* let each send work out its access again, under the rules as they are then.
|
|
465
|
+
* Schedules from before they were kept have neither.
|
|
466
|
+
*/
|
|
467
|
+
const ScheduleIdentity = z.object({
|
|
468
|
+
author: Author,
|
|
469
|
+
principal: StoredPrincipal,
|
|
470
|
+
actor: z.object({
|
|
471
|
+
id: ActorId,
|
|
472
|
+
kind: SeenActorKind,
|
|
473
|
+
handle: Handle,
|
|
474
|
+
name: z.string().min(1).max(128),
|
|
475
|
+
avatar: z.string().nullable()
|
|
476
|
+
})
|
|
477
|
+
});
|
|
478
|
+
/**
|
|
479
|
+
* Scheduled messages, outside the board log: one session, `schedules`. A schedule's ID is
|
|
480
|
+
* its `schedule.created` index. Each send is an A2 timer carrying `schedule.due`; timers
|
|
481
|
+
* can't be revoked, so a canceled or superseded one fires into a no-op.
|
|
482
|
+
*/
|
|
483
|
+
const schedulesContract = contract({
|
|
484
|
+
name: "board-schedules",
|
|
485
|
+
events: {
|
|
486
|
+
"schedule.created": z.object({
|
|
487
|
+
/** A new root in `channel`, or a reply in the thread rooted at `re`. */
|
|
488
|
+
channel: ChannelPath,
|
|
489
|
+
re: MessageId.nullable(),
|
|
490
|
+
text: z.union([MessageText, z.literal("")]),
|
|
491
|
+
labels: Labels,
|
|
492
|
+
noSync: z.boolean(),
|
|
493
|
+
attachments: z.array(Attachment).max(10).optional(),
|
|
494
|
+
/** The first send. */
|
|
495
|
+
first: Millis,
|
|
496
|
+
/** Repeats this often after the first send, or null for once. */
|
|
497
|
+
every: Millis.nullable(),
|
|
498
|
+
/** Stops repeating after this time, or after this many sends. Null: never. */
|
|
499
|
+
until: Millis.nullable(),
|
|
500
|
+
times: z.number().int().positive().nullable(),
|
|
501
|
+
as: ScheduleIdentity,
|
|
502
|
+
/**
|
|
503
|
+
* (server) Its creator couldn't read its channel. Where a caller can't
|
|
504
|
+
* read, it sees and cancels only these of its own. Schedules from before
|
|
505
|
+
* channel access have none.
|
|
506
|
+
*/
|
|
507
|
+
blind: z.literal(true).optional()
|
|
508
|
+
}),
|
|
509
|
+
"schedule.canceled": z.object({
|
|
510
|
+
id: z.number().int().positive(),
|
|
511
|
+
by: Author,
|
|
512
|
+
/**
|
|
513
|
+
* `canceled`: someone canceled it; `thread_deleted`: the thread it replies in is gone;
|
|
514
|
+
* `dropped`: the board's middleware dropped a send, so it would drop the rest;
|
|
515
|
+
* `revoked`: its creator can no longer post there.
|
|
516
|
+
*/
|
|
517
|
+
reason: z.enum([
|
|
518
|
+
"canceled",
|
|
519
|
+
"thread_deleted",
|
|
520
|
+
"dropped",
|
|
521
|
+
"revoked"
|
|
522
|
+
])
|
|
523
|
+
}),
|
|
524
|
+
/** (server) A timer: send `n` is due at `at`. */
|
|
525
|
+
"schedule.due": z.object({
|
|
526
|
+
id: z.number().int().positive(),
|
|
527
|
+
n: z.number().int().nonnegative(),
|
|
528
|
+
at: Millis
|
|
529
|
+
}),
|
|
530
|
+
/** (server) Send `n` was posted as `message`; the next is due at `next`, or never. */
|
|
531
|
+
"schedule.fired": z.object({
|
|
532
|
+
id: z.number().int().positive(),
|
|
533
|
+
n: z.number().int().nonnegative(),
|
|
534
|
+
message: MessageId,
|
|
535
|
+
next: Millis.nullable()
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
const SCHEDULES_SESSION = "schedules";
|
|
540
|
+
/**
|
|
541
|
+
* One-time codes for linking accounts, outside the board log: a code is a secret
|
|
542
|
+
* until it's used, and only its hash is kept. Session `identity`.
|
|
543
|
+
*/
|
|
544
|
+
const identityContract = contract({
|
|
545
|
+
name: "board-identity",
|
|
546
|
+
events: {
|
|
547
|
+
"identity.code_issued": z.object({
|
|
548
|
+
hash: z.string().min(1).max(128),
|
|
549
|
+
/** Who the account it's posted from links to. */
|
|
550
|
+
principal: ActorId,
|
|
551
|
+
/** The sync entry it must be posted through. */
|
|
552
|
+
sync: z.string().min(1).max(64),
|
|
553
|
+
expiresAt: z.string().max(64)
|
|
554
|
+
}),
|
|
555
|
+
/** The first account to post it wins; any later post is just a message. */
|
|
556
|
+
"identity.code_used": z.object({
|
|
557
|
+
hash: z.string().min(1).max(128),
|
|
558
|
+
actor: ActorId,
|
|
559
|
+
/** The message that used it (`externalMessageKey`): only a retry of that one claims it again. */
|
|
560
|
+
message: z.string().max(1024).optional()
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
const IDENTITY_SESSION = "identity";
|
|
565
|
+
//#endregion
|
|
566
|
+
export { schedulesContract as _, ExternalTarget as a, Link as c, SCHEDULES_SESSION as d, boardContract as f, registryContract as g, inboxSession as h, ExternalLocation as i, Origin as l, inboxContract as m, ExternalAuthor as n, IDENTITY_SESSION as o, identityContract as p, ExternalEvent as r, INBOXES_SESSION as s, BOARD_SESSION as t, REGISTRY_SESSION as u, watchContract as v, watchSession as y };
|