@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,293 @@
|
|
|
1
|
+
import { b as actorKind } from "./schemas-Be08nyfW.mjs";
|
|
2
|
+
import { f as boardContract, p as identityContract } from "./contract-CuJmlrhw.mjs";
|
|
3
|
+
import { t as canonicalEmoji } from "./emoji-CTLpvH-A.mjs";
|
|
4
|
+
import { n as setOwn, t as own } from "./own-OwgPWdmh.mjs";
|
|
5
|
+
import { reducer } from "experimental-a2";
|
|
6
|
+
//#region src/contract/reducers.ts
|
|
7
|
+
const iso = (date) => typeof date === "string" ? date : date.toISOString();
|
|
8
|
+
function foldMessages(state, event) {
|
|
9
|
+
const changed = foldMessage(state, event);
|
|
10
|
+
if (changed) changed.version = event.index;
|
|
11
|
+
return state;
|
|
12
|
+
}
|
|
13
|
+
/** Applies `event`, and returns the message it changed: none for an event that changes nothing. */
|
|
14
|
+
function foldMessage(state, event) {
|
|
15
|
+
switch (event.type) {
|
|
16
|
+
case "message.posted": {
|
|
17
|
+
const { payload } = event;
|
|
18
|
+
const message = {
|
|
19
|
+
id: event.index,
|
|
20
|
+
channel: payload.channel,
|
|
21
|
+
text: payload.text,
|
|
22
|
+
re: payload.re,
|
|
23
|
+
replyTo: payload.replyTo,
|
|
24
|
+
thread: payload.thread,
|
|
25
|
+
labels: { ...payload.labels },
|
|
26
|
+
reactions: {},
|
|
27
|
+
mentions: payload.mentions,
|
|
28
|
+
by: payload.by,
|
|
29
|
+
noSync: payload.noSync,
|
|
30
|
+
origin: payload.origin,
|
|
31
|
+
from: payload.from,
|
|
32
|
+
createdAt: iso(event.createdAt),
|
|
33
|
+
editedAt: null,
|
|
34
|
+
deleted: false,
|
|
35
|
+
undelivered: {},
|
|
36
|
+
attachments: payload.attachments ?? [],
|
|
37
|
+
links: payload.links ?? [],
|
|
38
|
+
...payload.schedule ? { schedule: { ...payload.schedule } } : {},
|
|
39
|
+
...payload.blind ? { blind: true } : {}
|
|
40
|
+
};
|
|
41
|
+
state.byId[event.index] = message;
|
|
42
|
+
if (payload.re !== null) (state.replies[payload.re] ??= []).push(event.index);
|
|
43
|
+
return message;
|
|
44
|
+
}
|
|
45
|
+
case "message.edited": {
|
|
46
|
+
const message = state.byId[event.payload.id];
|
|
47
|
+
if (!message || message.deleted) return void 0;
|
|
48
|
+
message.text = event.payload.text;
|
|
49
|
+
message.mentions = event.payload.mentions;
|
|
50
|
+
message.editedAt = iso(event.createdAt);
|
|
51
|
+
return message;
|
|
52
|
+
}
|
|
53
|
+
case "message.deleted": {
|
|
54
|
+
const message = state.byId[event.payload.id];
|
|
55
|
+
if (!message || message.deleted) return void 0;
|
|
56
|
+
message.deleted = true;
|
|
57
|
+
message.text = "";
|
|
58
|
+
message.reactions = {};
|
|
59
|
+
message.mentions = [];
|
|
60
|
+
message.attachments = [];
|
|
61
|
+
message.links = [];
|
|
62
|
+
return message;
|
|
63
|
+
}
|
|
64
|
+
case "reaction.added": {
|
|
65
|
+
const message = state.byId[event.payload.id];
|
|
66
|
+
if (!message || message.deleted) return void 0;
|
|
67
|
+
const emoji = canonicalEmoji(event.payload.emoji);
|
|
68
|
+
const actors = own(message.reactions, emoji);
|
|
69
|
+
if (actors?.includes(event.payload.by.actor)) return void 0;
|
|
70
|
+
if (actors) actors.push(event.payload.by.actor);
|
|
71
|
+
else setOwn(message.reactions, emoji, [event.payload.by.actor]);
|
|
72
|
+
return message;
|
|
73
|
+
}
|
|
74
|
+
case "reaction.removed": {
|
|
75
|
+
const message = state.byId[event.payload.id];
|
|
76
|
+
if (!message || message.deleted) return void 0;
|
|
77
|
+
const { by } = event.payload;
|
|
78
|
+
const emoji = canonicalEmoji(event.payload.emoji);
|
|
79
|
+
const had = own(message.reactions, emoji);
|
|
80
|
+
if (!had?.includes(by.actor)) return void 0;
|
|
81
|
+
const actors = had.filter((a) => a !== by.actor);
|
|
82
|
+
if (actors.length > 0) setOwn(message.reactions, emoji, actors);
|
|
83
|
+
else delete message.reactions[emoji];
|
|
84
|
+
return message;
|
|
85
|
+
}
|
|
86
|
+
case "labels.changed": {
|
|
87
|
+
const message = state.byId[event.payload.id];
|
|
88
|
+
if (!message || message.deleted) return void 0;
|
|
89
|
+
for (const [key, value] of Object.entries(event.payload.set)) setOwn(message.labels, key, value);
|
|
90
|
+
for (const key of event.payload.unset) delete message.labels[key];
|
|
91
|
+
return message;
|
|
92
|
+
}
|
|
93
|
+
case "message.linked": {
|
|
94
|
+
const message = state.byId[event.payload.id];
|
|
95
|
+
if (!message || message.deleted) return void 0;
|
|
96
|
+
message.links = event.payload.links;
|
|
97
|
+
return message;
|
|
98
|
+
}
|
|
99
|
+
case "message.sync_failed": {
|
|
100
|
+
const message = state.byId[event.payload.id];
|
|
101
|
+
if (!message) return void 0;
|
|
102
|
+
setOwn(message.undelivered, event.payload.target, event.payload.error);
|
|
103
|
+
return message;
|
|
104
|
+
}
|
|
105
|
+
case "message.synced": {
|
|
106
|
+
const message = state.byId[event.payload.id];
|
|
107
|
+
if (!message || !Object.hasOwn(message.undelivered, event.payload.target)) return void 0;
|
|
108
|
+
delete message.undelivered[event.payload.target];
|
|
109
|
+
return message;
|
|
110
|
+
}
|
|
111
|
+
default: return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const messagesReducer = reducer({
|
|
115
|
+
contract: boardContract,
|
|
116
|
+
name: "board.messages.v5",
|
|
117
|
+
initialState: {
|
|
118
|
+
byId: {},
|
|
119
|
+
replies: {}
|
|
120
|
+
}
|
|
121
|
+
}).fold(foldMessages);
|
|
122
|
+
reducer({
|
|
123
|
+
contract: boardContract,
|
|
124
|
+
name: "board.posted.v1",
|
|
125
|
+
initialState: {}
|
|
126
|
+
}).fold((state, event) => {
|
|
127
|
+
if (event.type === "message.posted") setOwn(state, event.id, event.index);
|
|
128
|
+
return state;
|
|
129
|
+
});
|
|
130
|
+
const channelsReducer = reducer({
|
|
131
|
+
contract: boardContract,
|
|
132
|
+
name: "board.channels.v1",
|
|
133
|
+
initialState: {}
|
|
134
|
+
}).fold((state, event) => {
|
|
135
|
+
if (event.type !== "message.posted") return state;
|
|
136
|
+
const { channel, re } = event.payload;
|
|
137
|
+
const entry = own(state, channel) ?? setOwn(state, channel, {
|
|
138
|
+
path: channel,
|
|
139
|
+
lastIndex: 0,
|
|
140
|
+
messages: 0,
|
|
141
|
+
threads: 0
|
|
142
|
+
});
|
|
143
|
+
entry.lastIndex = event.index;
|
|
144
|
+
entry.messages += 1;
|
|
145
|
+
if (re === null) entry.threads += 1;
|
|
146
|
+
return state;
|
|
147
|
+
});
|
|
148
|
+
/** The principal an actor belongs to: its link, its own principal for a profile (`…+claude`), or itself. */
|
|
149
|
+
function personOf(actors, actor) {
|
|
150
|
+
const linked = own(actors.links, actor)?.to;
|
|
151
|
+
if (linked) return linked;
|
|
152
|
+
const plus = actor.indexOf("+");
|
|
153
|
+
return plus === -1 ? actor : actor.slice(0, plus);
|
|
154
|
+
}
|
|
155
|
+
/** The platform actors linked to `principal`. */
|
|
156
|
+
const linkedTo = (actors, principal) => Object.entries(actors.links).filter(([, link]) => link.to === principal).map(([actor]) => actor);
|
|
157
|
+
function foldActors(state, event) {
|
|
158
|
+
if (event.type === "actor.linked") {
|
|
159
|
+
setOwn(state.links, event.payload.actor, {
|
|
160
|
+
to: event.payload.to,
|
|
161
|
+
method: event.payload.method,
|
|
162
|
+
at: iso(event.createdAt)
|
|
163
|
+
});
|
|
164
|
+
return state;
|
|
165
|
+
}
|
|
166
|
+
if (event.type === "actor.unlinked") {
|
|
167
|
+
delete state.links[event.payload.actor];
|
|
168
|
+
return state;
|
|
169
|
+
}
|
|
170
|
+
if (event.type !== "actor.seen") return state;
|
|
171
|
+
const { actor, kind, handle, name, avatar, host } = event.payload;
|
|
172
|
+
const previous = own(state.byId, actor);
|
|
173
|
+
if (previous) {
|
|
174
|
+
const key = previous.handle.toLowerCase();
|
|
175
|
+
const remaining = (own(state.byHandle, key) ?? []).filter((id) => id !== actor);
|
|
176
|
+
if (remaining.length > 0) setOwn(state.byHandle, key, remaining);
|
|
177
|
+
else delete state.byHandle[key];
|
|
178
|
+
}
|
|
179
|
+
setOwn(state.byId, actor, {
|
|
180
|
+
id: actor,
|
|
181
|
+
kind: actorKind(kind),
|
|
182
|
+
handle,
|
|
183
|
+
name,
|
|
184
|
+
avatar,
|
|
185
|
+
...host || previous?.host ? { host: true } : {}
|
|
186
|
+
});
|
|
187
|
+
const key = handle.toLowerCase();
|
|
188
|
+
const ids = own(state.byHandle, key) ?? setOwn(state.byHandle, key, []);
|
|
189
|
+
if (!ids.includes(actor)) ids.push(actor);
|
|
190
|
+
return state;
|
|
191
|
+
}
|
|
192
|
+
const actorsReducer = reducer({
|
|
193
|
+
contract: boardContract,
|
|
194
|
+
name: "board.actors.v2",
|
|
195
|
+
initialState: {
|
|
196
|
+
byId: {},
|
|
197
|
+
byHandle: {},
|
|
198
|
+
links: {}
|
|
199
|
+
}
|
|
200
|
+
}).fold(foldActors);
|
|
201
|
+
/** `slack:C0123ABC:1727.1234` is a thread; `slack:C0123ABC` and `github:owner/repo` are channels. */
|
|
202
|
+
const isThreadTarget = (target) => target.slice(target.indexOf(":") + 1).includes(":");
|
|
203
|
+
const externalMessageKey = (thread, message) => `${thread}|${message}`;
|
|
204
|
+
const addTarget = (thread, target) => {
|
|
205
|
+
if (!thread.targets.includes(target)) thread.targets.push(target);
|
|
206
|
+
};
|
|
207
|
+
function foldSync(state, event) {
|
|
208
|
+
switch (event.type) {
|
|
209
|
+
case "message.posted": {
|
|
210
|
+
const { payload } = event;
|
|
211
|
+
if (payload.re === null) setOwn(state.threads, payload.thread, {
|
|
212
|
+
root: event.index,
|
|
213
|
+
channel: payload.channel,
|
|
214
|
+
targets: [],
|
|
215
|
+
external: {}
|
|
216
|
+
});
|
|
217
|
+
const origin = payload.origin;
|
|
218
|
+
if (!origin) return state;
|
|
219
|
+
const thread = own(state.threads, payload.thread);
|
|
220
|
+
if (thread && payload.re === null) {
|
|
221
|
+
addTarget(thread, origin.thread);
|
|
222
|
+
setOwn(thread.kinds ??= {}, origin.thread, "thread");
|
|
223
|
+
setOwn(thread.external, origin.thread, origin.thread);
|
|
224
|
+
setOwn(state.byExternalThread, origin.thread, payload.thread);
|
|
225
|
+
}
|
|
226
|
+
const precise = origin.subthread ?? origin.thread;
|
|
227
|
+
setOwn(state.copies[event.index] ??= {}, origin.thread, {
|
|
228
|
+
thread: precise,
|
|
229
|
+
message: origin.message,
|
|
230
|
+
...payload.importedBy ? { by: payload.importedBy } : {}
|
|
231
|
+
});
|
|
232
|
+
state.byExternalMessage[externalMessageKey(precise, origin.message)] = event.index;
|
|
233
|
+
return state;
|
|
234
|
+
}
|
|
235
|
+
case "thread.synced": {
|
|
236
|
+
const thread = own(state.threads, event.payload.thread);
|
|
237
|
+
if (!thread) return state;
|
|
238
|
+
addTarget(thread, event.payload.target);
|
|
239
|
+
if (event.payload.kind) setOwn(thread.kinds ??= {}, event.payload.target, event.payload.kind);
|
|
240
|
+
if ((event.payload.kind ?? (isThreadTarget(event.payload.target) ? "thread" : "channel")) === "thread") {
|
|
241
|
+
setOwn(thread.external, event.payload.target, event.payload.target);
|
|
242
|
+
setOwn(state.byExternalThread, event.payload.target, event.payload.thread);
|
|
243
|
+
}
|
|
244
|
+
return state;
|
|
245
|
+
}
|
|
246
|
+
case "message.files_synced": {
|
|
247
|
+
const copies = state.copies[event.payload.id];
|
|
248
|
+
const copy = copies && own(copies, event.payload.target);
|
|
249
|
+
if (copy && copy.by === event.payload.syncedBy) copy.files = event.payload.files;
|
|
250
|
+
return state;
|
|
251
|
+
}
|
|
252
|
+
case "message.synced": {
|
|
253
|
+
const { id, target, external, syncedBy } = event.payload;
|
|
254
|
+
setOwn(state.copies[id] ??= {}, target, {
|
|
255
|
+
...external,
|
|
256
|
+
...syncedBy ? { by: syncedBy } : {}
|
|
257
|
+
});
|
|
258
|
+
state.byExternalMessage[externalMessageKey(external.thread, external.message)] = id;
|
|
259
|
+
const thread = own(state.threads, event.payload.thread);
|
|
260
|
+
if (thread && thread.root === id && ((thread.kinds && own(thread.kinds, target)) ?? (isThreadTarget(target) ? "thread" : "channel")) === "channel") {
|
|
261
|
+
setOwn(thread.external, target, external.thread);
|
|
262
|
+
setOwn(state.byExternalThread, external.thread, event.payload.thread);
|
|
263
|
+
}
|
|
264
|
+
return state;
|
|
265
|
+
}
|
|
266
|
+
default: return state;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const identityCodesReducer = reducer({
|
|
270
|
+
contract: identityContract,
|
|
271
|
+
name: "board.identity-codes.v1",
|
|
272
|
+
initialState: {}
|
|
273
|
+
}).fold((state, event) => {
|
|
274
|
+
if (event.type === "identity.code_issued") {
|
|
275
|
+
const { hash, principal, sync, expiresAt } = event.payload;
|
|
276
|
+
state[hash] ??= {
|
|
277
|
+
principal,
|
|
278
|
+
sync,
|
|
279
|
+
expiresAt,
|
|
280
|
+
usedBy: null,
|
|
281
|
+
usedIn: null
|
|
282
|
+
};
|
|
283
|
+
} else if (event.type === "identity.code_used") {
|
|
284
|
+
const code = state[event.payload.hash];
|
|
285
|
+
if (code && code.usedBy === null) {
|
|
286
|
+
code.usedBy = event.payload.actor;
|
|
287
|
+
code.usedIn = event.payload.message ?? null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return state;
|
|
291
|
+
});
|
|
292
|
+
//#endregion
|
|
293
|
+
export { foldMessages as a, isThreadTarget as c, messagesReducer as d, personOf as f, foldActors as i, iso as l, channelsReducer as n, foldSync as o, externalMessageKey as r, identityCodesReducer as s, actorsReducer as t, linkedTo as u };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/contract/schemas.ts
|
|
3
|
+
const CHANNEL_PATH_MAX = 80;
|
|
4
|
+
const MESSAGE_TEXT_MAX = 1e4;
|
|
5
|
+
/** The longest `x-board-scope` header the board reads; a schedule keeps its creator's. */
|
|
6
|
+
const SCOPE_MAX = 4e3;
|
|
7
|
+
const ATTACHMENTS_PER_MESSAGE_MAX = 10;
|
|
8
|
+
const segment = "[a-z0-9][a-z0-9_-]*";
|
|
9
|
+
/** `errors-www`, `www/production`: lowercase segments joined by `/`. */
|
|
10
|
+
const ChannelPath = z.string().max(80).regex(new RegExp(`^${segment}(?:/${segment})*$`), "invalid channel path");
|
|
11
|
+
/**
|
|
12
|
+
* No board string holds a NUL, which Postgres can't store, or a lone surrogate,
|
|
13
|
+
* which no store keeps as it was: requests with one are refused, and sync
|
|
14
|
+
* cleans what platforms send.
|
|
15
|
+
*/
|
|
16
|
+
const storable = (value) => !value.includes("\0") && value.isWellFormed();
|
|
17
|
+
/** `value` as the board keeps it, for what comes in from a platform: without NUL, and a lone surrogate as U+FFFD. */
|
|
18
|
+
const toStorable = (value) => value.replaceAll("\0", "").toWellFormed();
|
|
19
|
+
const MessageText = z.string().max(MESSAGE_TEXT_MAX).refine((text) => text.trim().length > 0, "message text is empty").refine(storable, "message text has a NUL character or a lone surrogate");
|
|
20
|
+
/** A message is named by the log index of its `message.posted` event. */
|
|
21
|
+
const MessageId = z.number().int().positive();
|
|
22
|
+
/**
|
|
23
|
+
* Names every object has (`constructor`, `__proto__`), and `prototype`. Emoji and label keys are
|
|
24
|
+
* keys of plain objects, where these would read what an object inherits, so neither takes one.
|
|
25
|
+
*/
|
|
26
|
+
const INHERITED = /* @__PURE__ */ new Set([...Object.getOwnPropertyNames(Object.prototype), "prototype"]);
|
|
27
|
+
const notInherited = (name) => !INHERITED.has(name);
|
|
28
|
+
const LabelKey = z.string().max(64).regex(/^[a-z0-9][a-z0-9_.-]*$/, "invalid label key").refine(notInherited, "label key is a reserved name");
|
|
29
|
+
const LabelValue = z.string().max(256).refine(storable, "label value has a NUL character or a lone surrogate");
|
|
30
|
+
const Labels = z.record(LabelKey, LabelValue);
|
|
31
|
+
/** Normalized short names, as Chat SDK names them: `eyes`, `check`, `thumbs_up`. */
|
|
32
|
+
const Emoji = z.string().max(64).regex(/^[a-z0-9_+-]+$/, "invalid emoji name").refine(notInherited, "emoji name is a reserved name");
|
|
33
|
+
/** `vercel:user:u_abc123`, or `vercel:user:u_abc123+claude` for a profile. */
|
|
34
|
+
const ActorId = z.string().min(1).max(256);
|
|
35
|
+
/** What an actor is: a `person`, an `agent` acting for a principal (`--as claude`), a deployment, a workflow, or a platform user (`external`). */
|
|
36
|
+
const ActorKind = z.enum([
|
|
37
|
+
"person",
|
|
38
|
+
"agent",
|
|
39
|
+
"deployment",
|
|
40
|
+
"workflow",
|
|
41
|
+
"external"
|
|
42
|
+
]);
|
|
43
|
+
/** `actor.seen` events written before agents were called agents say `profile`. */
|
|
44
|
+
const SeenActorKind = z.union([ActorKind, z.literal("profile")]);
|
|
45
|
+
const actorKind = (kind) => kind === "profile" ? "agent" : kind;
|
|
46
|
+
const Handle = z.string().min(1).max(128).refine(storable, "handle has a NUL character or a lone surrogate");
|
|
47
|
+
/** A platform user's avatar as the board keeps it: an https URL, or none. Anything else shows nowhere. */
|
|
48
|
+
function platformAvatar(avatar) {
|
|
49
|
+
if (avatar === null) return null;
|
|
50
|
+
try {
|
|
51
|
+
return new URL(avatar).protocol === "https:" ? avatar : null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const Author = z.object({
|
|
57
|
+
actor: ActorId,
|
|
58
|
+
principal: z.string().min(1),
|
|
59
|
+
profile: z.string().nullable(),
|
|
60
|
+
session: z.string().nullable()
|
|
61
|
+
});
|
|
62
|
+
/** 128 random bits, base64url: an attachment is named by an ID nobody can guess. */
|
|
63
|
+
const AttachmentId = z.string().regex(/^[A-Za-z0-9_-]{22}$/, "invalid attachment id");
|
|
64
|
+
/** A file on a message. The board records `type` and `size` from what it stored, never from the request. */
|
|
65
|
+
const Attachment = z.object({
|
|
66
|
+
id: AttachmentId,
|
|
67
|
+
name: z.string().min(1).max(255),
|
|
68
|
+
type: z.string().min(1).max(255),
|
|
69
|
+
size: z.number().int().nonnegative()
|
|
70
|
+
});
|
|
71
|
+
/** A registered watcher's ID: a profile name, since its writes are by that profile. */
|
|
72
|
+
const RemoteWatcherId = z.string().regex(/^[A-Za-z][A-Za-z0-9-]{0,31}$/, "ids start with a letter and use up to 32 letters, numbers, and hyphens");
|
|
73
|
+
//#endregion
|
|
74
|
+
export { toStorable as C, storable as S, RemoteWatcherId as _, AttachmentId as a, actorKind as b, ChannelPath as c, LabelKey as d, LabelValue as f, MessageText as g, MessageId as h, Attachment as i, Emoji as l, MESSAGE_TEXT_MAX as m, ActorId as n, Author as o, Labels as p, ActorKind as r, CHANNEL_PATH_MAX as s, ATTACHMENTS_PER_MESSAGE_MAX as t, Handle as u, SCOPE_MAX as v, platformAvatar as x, SeenActorKind as y };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/contract/schemas.d.ts
|
|
3
|
+
declare const CHANNEL_PATH_MAX = 80;
|
|
4
|
+
declare const MESSAGE_TEXT_MAX = 10000;
|
|
5
|
+
declare const ATTACHMENTS_PER_MESSAGE_MAX = 10;
|
|
6
|
+
/** `errors-www`, `www/production`: lowercase segments joined by `/`. */
|
|
7
|
+
declare const ChannelPath: z.ZodString;
|
|
8
|
+
declare const MessageText: z.ZodString;
|
|
9
|
+
/** A message is named by the log index of its `message.posted` event. */
|
|
10
|
+
declare const MessageId: z.ZodNumber;
|
|
11
|
+
declare const LabelKey: z.ZodString;
|
|
12
|
+
declare const LabelValue: z.ZodString;
|
|
13
|
+
declare const Labels: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
14
|
+
/** Normalized short names, as Chat SDK names them: `eyes`, `check`, `thumbs_up`. */
|
|
15
|
+
declare const Emoji: z.ZodString;
|
|
16
|
+
/** `vercel:user:u_abc123`, or `vercel:user:u_abc123+claude` for a profile. */
|
|
17
|
+
declare const ActorId: z.ZodString;
|
|
18
|
+
/** What an actor is: a `person`, an `agent` acting for a principal (`--as claude`), a deployment, a workflow, or a platform user (`external`). */
|
|
19
|
+
declare const ActorKind: z.ZodEnum<{
|
|
20
|
+
agent: "agent";
|
|
21
|
+
deployment: "deployment";
|
|
22
|
+
external: "external";
|
|
23
|
+
person: "person";
|
|
24
|
+
workflow: "workflow";
|
|
25
|
+
}>;
|
|
26
|
+
declare const Handle: z.ZodString;
|
|
27
|
+
declare const Author: z.ZodObject<{
|
|
28
|
+
actor: z.ZodString;
|
|
29
|
+
principal: z.ZodString;
|
|
30
|
+
profile: z.ZodNullable<z.ZodString>;
|
|
31
|
+
session: z.ZodNullable<z.ZodString>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
/** 128 random bits, base64url: an attachment is named by an ID nobody can guess. */
|
|
34
|
+
declare const AttachmentId: z.ZodString;
|
|
35
|
+
/** A file on a message. The board records `type` and `size` from what it stored, never from the request. */
|
|
36
|
+
declare const Attachment: z.ZodObject<{
|
|
37
|
+
id: z.ZodString;
|
|
38
|
+
name: z.ZodString;
|
|
39
|
+
type: z.ZodString;
|
|
40
|
+
size: z.ZodNumber;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
type ChannelPath = z.infer<typeof ChannelPath>;
|
|
43
|
+
type MessageId = z.infer<typeof MessageId>;
|
|
44
|
+
type Labels = z.infer<typeof Labels>;
|
|
45
|
+
type ActorId = z.infer<typeof ActorId>;
|
|
46
|
+
type ActorKind = z.infer<typeof ActorKind>;
|
|
47
|
+
type Author = z.infer<typeof Author>;
|
|
48
|
+
type Attachment = z.infer<typeof Attachment>;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { AttachmentId as a, ChannelPath as c, LabelKey as d, LabelValue as f, MessageText as g, MessageId as h, Attachment as i, Emoji as l, MESSAGE_TEXT_MAX as m, ActorId as n, Author as o, Labels as p, ActorKind as r, CHANNEL_PATH_MAX as s, ATTACHMENTS_PER_MESSAGE_MAX as t, Handle as u };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { o as localTokenFile } from "./backend-http-BsDSgmeo.mjs";
|
|
2
|
+
import { t as board } from "./server-Cxey38B-.mjs";
|
|
3
|
+
import { t as oidcTokens } from "./tokens-DKLn5P9B.mjs";
|
|
4
|
+
import { n as fsBlob, t as a2 } from "./a2-DYGYankn.mjs";
|
|
5
|
+
import { t as localScheduler } from "./local-Cr_dSjvs.mjs";
|
|
6
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { userInfo } from "node:os";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { SignJWT, createLocalJWKSet, exportJWK, generateKeyPair } from "jose";
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
const ISSUER = "board:local";
|
|
12
|
+
const BODY_MAX = 8388608;
|
|
13
|
+
/** Your OS username as a handle: lowercase letters, digits, `.`, `_`, `-`. */
|
|
14
|
+
function localHandle(name = safeUsername()) {
|
|
15
|
+
return name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, "").slice(0, 64).replace(/[^a-z0-9]+$/, "") || "me";
|
|
16
|
+
}
|
|
17
|
+
function safeUsername() {
|
|
18
|
+
try {
|
|
19
|
+
return userInfo().username;
|
|
20
|
+
} catch {
|
|
21
|
+
return process.env.USER || process.env.USERNAME || "me";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A board on this machine, for agents and people sharing one computer: a log in
|
|
26
|
+
* SQLite under `dir`, attachments next to it, a scheduler in this process, no sync,
|
|
27
|
+
* on 127.0.0.1 only. Its
|
|
28
|
+
* keys live in memory for this run; it signs one token, for you, into a file
|
|
29
|
+
* only you can read (`localTokenFile`), which `vb` on this machine picks up.
|
|
30
|
+
*/
|
|
31
|
+
async function serveLocal(options) {
|
|
32
|
+
const dir = resolve(options.dir);
|
|
33
|
+
await mkdir(dir, {
|
|
34
|
+
recursive: true,
|
|
35
|
+
mode: 448
|
|
36
|
+
});
|
|
37
|
+
quietSqliteWarning();
|
|
38
|
+
const { sqlite } = await import("./backend-a2/store-sqlite.mjs");
|
|
39
|
+
const store = sqlite({ path: join(dir, "board.db") });
|
|
40
|
+
const { publicKey, privateKey } = await generateKeyPair("ES256");
|
|
41
|
+
const jwks = createLocalJWKSet({ keys: [{
|
|
42
|
+
...await exportJWK(publicKey),
|
|
43
|
+
kid: "local",
|
|
44
|
+
alg: "ES256"
|
|
45
|
+
}] });
|
|
46
|
+
let handler = null;
|
|
47
|
+
const server = createServer((request, response) => {
|
|
48
|
+
if (!handler) return void response.writeHead(503).end();
|
|
49
|
+
forward(handler, request, response);
|
|
50
|
+
});
|
|
51
|
+
let port;
|
|
52
|
+
try {
|
|
53
|
+
port = await listen(server, options.port ?? 4100);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
await store.close();
|
|
56
|
+
if (error.code === "EADDRINUSE") throw new Error(`port ${options.port ?? 4100} is taken: is a board already running? Pick another with --port`);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const url = `http://127.0.0.1:${port}/api/board`;
|
|
60
|
+
handler = board({
|
|
61
|
+
backend: a2({
|
|
62
|
+
store,
|
|
63
|
+
scheduler: localScheduler(),
|
|
64
|
+
blob: fsBlob(join(dir, "attachments"))
|
|
65
|
+
}),
|
|
66
|
+
authenticate: oidcTokens({ local: {
|
|
67
|
+
issuer: ISSUER,
|
|
68
|
+
jwks
|
|
69
|
+
} }),
|
|
70
|
+
url,
|
|
71
|
+
basePath: "/api/board"
|
|
72
|
+
}).fetch;
|
|
73
|
+
const handle = options.handle ?? localHandle();
|
|
74
|
+
const token = await new SignJWT({}).setProtectedHeader({
|
|
75
|
+
alg: "ES256",
|
|
76
|
+
kid: "local"
|
|
77
|
+
}).setIssuer(ISSUER).setAudience(url).setSubject(handle).setIssuedAt().setExpirationTime("30d").sign(privateKey);
|
|
78
|
+
const file = localTokenFile(url);
|
|
79
|
+
await mkdir(dirname(file), {
|
|
80
|
+
recursive: true,
|
|
81
|
+
mode: 448
|
|
82
|
+
});
|
|
83
|
+
await writeFile(file, JSON.stringify({
|
|
84
|
+
url,
|
|
85
|
+
token,
|
|
86
|
+
handle
|
|
87
|
+
}), { mode: 384 });
|
|
88
|
+
return {
|
|
89
|
+
url,
|
|
90
|
+
principal: `local:user:${handle}`,
|
|
91
|
+
handle,
|
|
92
|
+
token,
|
|
93
|
+
dir,
|
|
94
|
+
async close() {
|
|
95
|
+
await rm(file, { force: true });
|
|
96
|
+
server.closeAllConnections();
|
|
97
|
+
await new Promise((done) => server.close(() => done()));
|
|
98
|
+
await store.close();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Node flags `node:sqlite` as experimental on stderr, which would be the CLI's only stderr line. */
|
|
103
|
+
function quietSqliteWarning() {
|
|
104
|
+
const emit = process.emitWarning;
|
|
105
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
106
|
+
const text = typeof warning === "string" ? warning : warning.message;
|
|
107
|
+
if (/SQLite is an experimental feature/.test(text)) return;
|
|
108
|
+
return emit.call(process, warning, ...rest);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function listen(server, port) {
|
|
112
|
+
return new Promise((done, fail) => {
|
|
113
|
+
server.once("error", fail);
|
|
114
|
+
server.listen(port, "127.0.0.1", () => {
|
|
115
|
+
server.off("error", fail);
|
|
116
|
+
done(server.address().port);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function forward(handler, req, res) {
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
res.on("close", () => controller.abort());
|
|
123
|
+
try {
|
|
124
|
+
let body;
|
|
125
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
126
|
+
const chunks = [];
|
|
127
|
+
let size = 0;
|
|
128
|
+
for await (const chunk of req) {
|
|
129
|
+
size += chunk.length;
|
|
130
|
+
if (size > BODY_MAX) {
|
|
131
|
+
res.writeHead(413, { "content-type": "application/json" });
|
|
132
|
+
res.end(JSON.stringify({ error: {
|
|
133
|
+
code: "too_large",
|
|
134
|
+
message: "request too large"
|
|
135
|
+
} }));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
chunks.push(chunk);
|
|
139
|
+
}
|
|
140
|
+
body = new Uint8Array(Buffer.concat(chunks));
|
|
141
|
+
}
|
|
142
|
+
const headers = new Headers();
|
|
143
|
+
for (const [key, value] of Object.entries(req.headers)) if (typeof value === "string") headers.set(key, value);
|
|
144
|
+
else if (Array.isArray(value)) for (const item of value) headers.append(key, item);
|
|
145
|
+
const response = await handler(new Request(`http://${req.headers.host ?? "127.0.0.1"}${req.url ?? "/"}`, {
|
|
146
|
+
method: req.method ?? "GET",
|
|
147
|
+
headers,
|
|
148
|
+
...body && body.length > 0 ? { body } : {},
|
|
149
|
+
signal: controller.signal
|
|
150
|
+
}));
|
|
151
|
+
const out = {};
|
|
152
|
+
response.headers.forEach((value, key) => {
|
|
153
|
+
if (key === "set-cookie") return;
|
|
154
|
+
out[key] = value;
|
|
155
|
+
});
|
|
156
|
+
res.writeHead(response.status, out);
|
|
157
|
+
if (response.body) try {
|
|
158
|
+
for await (const chunk of response.body) res.write(chunk);
|
|
159
|
+
} catch {}
|
|
160
|
+
res.end();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" });
|
|
163
|
+
res.end(JSON.stringify({ error: {
|
|
164
|
+
code: "internal",
|
|
165
|
+
message: error.message
|
|
166
|
+
} }));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
export { localHandle, serveLocal };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { t as createBoard } from "./exports-CyY3_FjZ.mjs";
|
|
2
|
+
import { t as http } from "./backend-http-BsDSgmeo.mjs";
|
|
3
|
+
//#region src/server/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* The handle on a board. With a `backend`, the board lives there: `a2()` keeps it in this
|
|
6
|
+
* deployment, `http(url)` uses another. Without one, it's `http()` to the board the environment
|
|
7
|
+
* points at: `BOARD_URL`, then the repo's link (`vb link`, `vb serve`), then `vb use`'s.
|
|
8
|
+
*/
|
|
9
|
+
function board(options = {}) {
|
|
10
|
+
return createBoard(options, {
|
|
11
|
+
http: () => http(),
|
|
12
|
+
sync: () => import("./engine-3EvQ8idk.mjs"),
|
|
13
|
+
serving: () => import("./serving-BNbzc96L.mjs")
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { board as t };
|