@autono/pinbox-core 0.1.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 (48) hide show
  1. package/README.md +17 -0
  2. package/dist/auth/verify.d.ts +15 -0
  3. package/dist/auth/verify.js +2 -0
  4. package/dist/connectors/github.d.ts +5 -0
  5. package/dist/connectors/github.js +49 -0
  6. package/dist/connectors/index.d.ts +37 -0
  7. package/dist/connectors/index.js +103 -0
  8. package/dist/context-BHcEpzVb.js +30 -0
  9. package/dist/delivery/openclaw.d.ts +10 -0
  10. package/dist/delivery/openclaw.js +50 -0
  11. package/dist/delivery/resume.d.ts +23 -0
  12. package/dist/delivery/resume.js +102 -0
  13. package/dist/delivery/router.d.ts +2 -0
  14. package/dist/delivery/router.js +262 -0
  15. package/dist/delivery/webhook.d.ts +9 -0
  16. package/dist/delivery/webhook.js +52 -0
  17. package/dist/do.d.ts +67 -0
  18. package/dist/do.js +713 -0
  19. package/dist/hub-QYz6OqYQ.js +328 -0
  20. package/dist/hub-server.d.ts +44 -0
  21. package/dist/hub-server.js +251 -0
  22. package/dist/hub.d.ts +125 -0
  23. package/dist/hub.js +2 -0
  24. package/dist/markdown.d.ts +14 -0
  25. package/dist/markdown.js +87 -0
  26. package/dist/payload-m1DbRWDD.d.ts +6 -0
  27. package/dist/poll-BrcAuaAz.js +252 -0
  28. package/dist/proc-BMp_pbPS.js +42 -0
  29. package/dist/router-D3dDjIaD.d.ts +62 -0
  30. package/dist/schema-BOTmn5SM.d.ts +297 -0
  31. package/dist/schema.d.ts +2 -0
  32. package/dist/schema.js +121 -0
  33. package/dist/schema.json +403 -0
  34. package/dist/sessions-CJdMBH3C.d.ts +47 -0
  35. package/dist/sessions-DrCVTMfI.js +129 -0
  36. package/dist/sessions.d.ts +2 -0
  37. package/dist/sessions.js +2 -0
  38. package/dist/store-DHbwWu93.d.ts +105 -0
  39. package/dist/store-DM8MjB8M.js +479 -0
  40. package/dist/store.d.ts +2 -0
  41. package/dist/store.js +3 -0
  42. package/dist/types-BLNQb7MH.d.ts +28 -0
  43. package/dist/verify-BDt9d9Np.js +55 -0
  44. package/dist/ws-protocol.d.ts +61 -0
  45. package/dist/ws-protocol.js +51 -0
  46. package/dist/ws.d.ts +7 -0
  47. package/dist/ws.js +1 -0
  48. package/package.json +104 -0
@@ -0,0 +1,328 @@
1
+ import { AttachmentSchema, PinInputSchema, SessionRefSchema } from "./schema.js";
2
+ import { a as NotFoundError, i as ConflictError, o as newId } from "./sessions-DrCVTMfI.js";
3
+ import { t as POLL_OPEN_MS } from "./poll-BrcAuaAz.js";
4
+ import { t as buildInjectionContext } from "./context-BHcEpzVb.js";
5
+ import { z } from "zod";
6
+ //#region src/routes-links.ts
7
+ const LinkPostSchema = z.object({ connector: z.string() });
8
+ const routeLinks = async (req, url, opts) => {
9
+ const pinId = match(url.pathname, /^\/pins\/([a-z0-9_]+)\/links$/);
10
+ if (pinId === null) return null;
11
+ const { store } = opts;
12
+ if (req.method === "GET") {
13
+ mustGetPin(store, pinId);
14
+ return ok(200, store.links.forPin(pinId));
15
+ }
16
+ if (req.method !== "POST") return null;
17
+ const pin = mustGetPin(store, pinId);
18
+ const body = LinkPostSchema.parse(await readJson(req));
19
+ const connector = opts.connectors?.find((c) => c.name === body.connector);
20
+ if (connector === void 0) return err(502, "E_CONNECTOR", `no connector available: ${body.connector}`, { hint: "run `pinbox doctor` to see which connectors this hub can reach" });
21
+ let link;
22
+ try {
23
+ link = await connector.createItem(pin, store.getThread(pinId));
24
+ } catch (cause) {
25
+ const message = cause instanceof Error ? cause.message : "connector request failed";
26
+ const hint = cause instanceof Error && "hint" in cause && typeof cause.hint === "string" ? cause.hint : void 0;
27
+ return err(502, "E_CONNECTOR", message, hint === void 0 ? void 0 : { hint });
28
+ }
29
+ const updated = store.addLink(pinId, link);
30
+ store.setDueAt(pinId, new Date(Date.now() + POLL_OPEN_MS).toISOString());
31
+ return ok(201, updated);
32
+ };
33
+ //#endregion
34
+ //#region src/routes-sessions.ts
35
+ async function routeSessions(req, url, opts) {
36
+ const { store } = opts;
37
+ const { sessions } = store;
38
+ if (url.pathname === "/sessions") {
39
+ if (req.method === "POST") {
40
+ const ref = SessionRefSchema.parse(await readJson(req));
41
+ return ok(sessions.findByRef(ref) === null ? 201 : 200, sessions.register(ref));
42
+ }
43
+ if (req.method === "GET") return ok(200, sessions.list());
44
+ return null;
45
+ }
46
+ const injectId = match(url.pathname, /^\/sessions\/([a-z0-9_]+)\/inject$/);
47
+ if (injectId !== null && req.method === "POST") return inject(store, injectId);
48
+ const pendingId = match(url.pathname, /^\/sessions\/([a-z0-9_]+)\/pending$/);
49
+ if (pendingId !== null && req.method === "GET") return pending(store, pendingId);
50
+ const sessionId = match(url.pathname, /^\/sessions\/([a-z0-9_]+)$/);
51
+ if (sessionId !== null && req.method === "DELETE") {
52
+ sessions.end(sessionId);
53
+ return ok(200, mustGetSession(sessions, sessionId));
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * The hook pull: touch the session, flip its pending delivery rows to delivered, and
59
+ * ALSO claim unassigned pending rows (assign → deliver — rule 1's "the next session to
60
+ * register receives all unassigned pins", satisfied at first pull). The context always
61
+ * carries ALL open pins: re-injection every turn is the delivery model that made
62
+ * attachments path-only.
63
+ */
64
+ function inject(store, id) {
65
+ const session = mustGetSession(store.sessions, id);
66
+ const { deliveries } = store;
67
+ store.sessions.touch(session.id);
68
+ let delivered = 0;
69
+ for (const row of deliveries.pendingForSession(session.id)) {
70
+ deliveries.markDelivered(row.id);
71
+ delivered += 1;
72
+ }
73
+ for (const row of deliveries.unassigned()) {
74
+ deliveries.assign(row.id, session.id);
75
+ deliveries.markDelivered(row.id);
76
+ delivered += 1;
77
+ }
78
+ const pins = store.listPins({ status: "open" });
79
+ return ok(200, {
80
+ context: buildInjectionContext(pins),
81
+ pins,
82
+ delivered
83
+ });
84
+ }
85
+ /**
86
+ * Read-only Stop-hook gate: the OPEN pins referenced by this session's pending
87
+ * delivery rows. Rows whose pin has since been resolved do not hold the agent —
88
+ * `count` is the count of actionable pins, not of ledger rows.
89
+ */
90
+ function pending(store, id) {
91
+ const session = mustGetSession(store.sessions, id);
92
+ const pins = openPinsFor(store, store.deliveries.pendingForSession(session.id));
93
+ return ok(200, {
94
+ count: pins.length,
95
+ pins
96
+ });
97
+ }
98
+ function openPinsFor(store, rows) {
99
+ const seen = /* @__PURE__ */ new Set();
100
+ const pins = [];
101
+ for (const row of rows) {
102
+ const event = store.eventsAfter(row.eventSeq - 1)[0];
103
+ if (event === void 0 || event.seq !== row.eventSeq) continue;
104
+ const pinId = pinIdOf(event.payload);
105
+ if (pinId === null || seen.has(pinId)) continue;
106
+ seen.add(pinId);
107
+ const pin = store.getPin(pinId);
108
+ if (pin?.status === "open") pins.push(pin);
109
+ }
110
+ return pins;
111
+ }
112
+ /** pin.created payloads are the Pin (`id`); thread.message payloads carry `pinId`. */
113
+ function pinIdOf(payload) {
114
+ if (typeof payload !== "object" || payload === null) return null;
115
+ const record = payload;
116
+ const id = "pinId" in record ? record["pinId"] : record["id"];
117
+ return typeof id === "string" && id.startsWith("pin_") ? id : null;
118
+ }
119
+ function mustGetSession(sessions, id) {
120
+ const session = sessions.get(id);
121
+ if (!session) throw new NotFoundError(`session not found: ${id}`);
122
+ return session;
123
+ }
124
+ //#endregion
125
+ //#region src/attachments.ts
126
+ const MAX_ATTACHMENT_BYTES = 5242880;
127
+ const sinks = /* @__PURE__ */ new WeakMap();
128
+ function registerAttachmentSink(store, sink) {
129
+ sinks.set(store, sink);
130
+ }
131
+ function attachmentSinkFor(store) {
132
+ return sinks.get(store);
133
+ }
134
+ //#endregion
135
+ //#region src/routes-toolbar.ts
136
+ const VerifyPostSchema = z.object({ outcome: z.enum(["accepted", "reopened"]) });
137
+ const AttachmentKindSchema = z.enum(["screenshot", "file"]);
138
+ const routeToolbar = async (req, url, opts) => {
139
+ const verifyPinId = match(url.pathname, /^\/pins\/([a-z0-9_]+)\/verify$/);
140
+ if (verifyPinId !== null && req.method === "POST") {
141
+ const body = VerifyPostSchema.parse(await readJson(req));
142
+ return ok(200, opts.store.verifyPin(verifyPinId, body.outcome));
143
+ }
144
+ if (url.pathname === "/attachments" && req.method === "POST") return postAttachment(req, url, opts);
145
+ return null;
146
+ };
147
+ async function postAttachment(req, url, opts) {
148
+ const kind = AttachmentKindSchema.parse(url.searchParams.get("kind"));
149
+ const contentType = req.headers.get("content-type");
150
+ if (contentType === null || contentType === "") return err(400, "E_INVALID_INPUT", "content-type header is required", { hint: "send the attachment's MIME type, e.g. content-type: image/webp" });
151
+ const tooLarge = () => err(413, "E_ATTACHMENT", `attachment exceeds ${MAX_ATTACHMENT_BYTES} bytes`, { hint: "attachments are capped at 5 MB; downscale or compress before uploading" });
152
+ const declared = Number(req.headers.get("content-length"));
153
+ if (Number.isFinite(declared) && declared > 5242880) return tooLarge();
154
+ const bytes = new Uint8Array(await req.arrayBuffer());
155
+ if (bytes.byteLength > 5242880) return tooLarge();
156
+ const sink = attachmentSinkFor(opts.store);
157
+ if (sink === void 0) return err(500, "E_INTERNAL", "no attachment sink registered for this store", { hint: "the host must call registerAttachmentSink(store, sink) before serving /attachments" });
158
+ const { attachment, uploadUrl } = await sink.write({
159
+ id: newId("att"),
160
+ kind,
161
+ contentType
162
+ }, bytes);
163
+ return ok(201, uploadUrl === void 0 ? { attachment } : {
164
+ attachment,
165
+ uploadUrl
166
+ });
167
+ }
168
+ //#endregion
169
+ //#region src/hub.ts
170
+ const HUB_VERSION = "0.1.0";
171
+ const StatusFilterSchema = z.enum(["open", "resolved"]);
172
+ const AfterSchema = z.coerce.number().int().nonnegative();
173
+ const ThreadPostSchema = z.object({
174
+ role: z.enum([
175
+ "human",
176
+ "agent",
177
+ "mirror"
178
+ ]),
179
+ text: z.string().min(1),
180
+ attachments: z.array(AttachmentSchema).optional(),
181
+ origin: z.string().optional()
182
+ });
183
+ const ResolvePostSchema = z.object({
184
+ by: z.enum(["human", "agent"]),
185
+ note: z.string().optional(),
186
+ commit: z.string().optional()
187
+ });
188
+ const ROUTES = [
189
+ routeCollections,
190
+ routePinItem,
191
+ routeSessions,
192
+ routeToolbar,
193
+ routeLinks
194
+ ];
195
+ function createHubHandler(opts) {
196
+ return async (req) => {
197
+ const url = new URL(req.url);
198
+ if (req.method === "GET" && url.pathname === "/health") return ok(200, {
199
+ version: HUB_VERSION,
200
+ schemaVersion: 1,
201
+ wsProtocol: 1
202
+ });
203
+ if (opts.verify) {
204
+ if (await opts.verify(req) === null) return err(401, "E_AUTH", "request rejected by the configured verifier", { hint: "send a credential the hub's verify strategy accepts" });
205
+ } else if (req.headers.get("authorization") !== `Bearer ${opts.token}`) return err(401, "E_INVALID_INPUT", "missing or invalid bearer token", { hint: "send Authorization: Bearer <token>; the token lives in the XDG state file" });
206
+ try {
207
+ return await route(req, url, opts);
208
+ } catch (cause) {
209
+ return mapError(cause);
210
+ }
211
+ };
212
+ }
213
+ async function route(req, url, opts) {
214
+ for (const routeModule of ROUTES) {
215
+ const response = await routeModule(req, url, opts);
216
+ if (response) return response;
217
+ }
218
+ throw new NotFoundError(`no route: ${req.method} ${url.pathname}`);
219
+ }
220
+ async function routeCollections(req, url, opts) {
221
+ const { store } = opts;
222
+ switch (`${req.method} ${url.pathname}`) {
223
+ case "GET /summary": {
224
+ const sessions = store.sessions.list().filter((s) => s.endedAt === void 0).length;
225
+ return ok(200, {
226
+ ...store.summary(),
227
+ sessions
228
+ });
229
+ }
230
+ case "GET /events": {
231
+ const after = url.searchParams.get("after");
232
+ return ok(200, store.eventsAfter(after === null ? 0 : AfterSchema.parse(after)));
233
+ }
234
+ case "POST /pins": {
235
+ const input = PinInputSchema.parse(await readJson(req));
236
+ return ok(201, store.createPin(input, opts.enrichEnv?.() ?? {}));
237
+ }
238
+ case "GET /pins": {
239
+ const search = url.searchParams.get("search");
240
+ if (search !== null) return ok(200, store.searchPins(search));
241
+ const status = url.searchParams.get("status");
242
+ const filter = status === null ? void 0 : { status: StatusFilterSchema.parse(status) };
243
+ return ok(200, store.listPins(filter));
244
+ }
245
+ default: return null;
246
+ }
247
+ }
248
+ async function routePinItem(req, url, opts) {
249
+ const { store } = opts;
250
+ const { method } = req;
251
+ const path = url.pathname;
252
+ const pinId = match(path, /^\/pins\/([a-z0-9_]+)$/);
253
+ if (pinId !== null && method === "GET") return ok(200, mustGetPin(store, pinId));
254
+ const threadPinId = match(path, /^\/pins\/([a-z0-9_]+)\/thread$/);
255
+ if (threadPinId !== null && method === "GET") {
256
+ mustGetPin(store, threadPinId);
257
+ return ok(200, store.getThread(threadPinId));
258
+ }
259
+ if (threadPinId !== null && method === "POST") {
260
+ const body = ThreadPostSchema.parse(await readJson(req));
261
+ const messageOpts = {
262
+ ...body.attachments === void 0 ? {} : { attachments: body.attachments },
263
+ ...body.origin === void 0 ? {} : { origin: body.origin }
264
+ };
265
+ const hasOpts = Object.keys(messageOpts).length > 0;
266
+ return ok(201, store.addThreadMessage(threadPinId, body.role, body.text, hasOpts ? messageOpts : void 0));
267
+ }
268
+ const resolvePinId = match(path, /^\/pins\/([a-z0-9_]+)\/resolve$/);
269
+ if (resolvePinId !== null && method === "POST") {
270
+ const body = ResolvePostSchema.parse(await readJson(req));
271
+ return ok(200, store.resolvePin(resolvePinId, body.by, body.note, body.commit));
272
+ }
273
+ return null;
274
+ }
275
+ function match(path, pattern) {
276
+ return pattern.exec(path)?.[1] ?? null;
277
+ }
278
+ function mustGetPin(store, id) {
279
+ const pin = store.getPin(id);
280
+ if (!pin) throw new NotFoundError(`pin not found: ${id}`);
281
+ return pin;
282
+ }
283
+ async function readJson(req) {
284
+ try {
285
+ return await req.json();
286
+ } catch {
287
+ throw new BodyNotJsonError();
288
+ }
289
+ }
290
+ var BodyNotJsonError = class extends Error {};
291
+ function json(status, body) {
292
+ return new Response(JSON.stringify(body), {
293
+ status,
294
+ headers: { "content-type": "application/json" }
295
+ });
296
+ }
297
+ function ok(status, data) {
298
+ return json(status, {
299
+ ok: true,
300
+ data
301
+ });
302
+ }
303
+ function err(status, code, message, extra) {
304
+ const hint = extra?.hint;
305
+ return json(status, {
306
+ ok: false,
307
+ error: hint === void 0 ? {
308
+ code,
309
+ message
310
+ } : {
311
+ code,
312
+ message,
313
+ hint
314
+ }
315
+ });
316
+ }
317
+ function mapError(cause) {
318
+ if (cause instanceof NotFoundError) return err(404, "E_NOT_FOUND", cause.message);
319
+ if (cause instanceof ConflictError) return err(409, "E_CONFLICT", cause.message);
320
+ if (cause instanceof z.ZodError) {
321
+ const issue = cause.issues[0];
322
+ return err(400, "E_INVALID_INPUT", issue ? `${issue.path.length > 0 ? `${issue.path.join(".")}: ` : ""}${issue.message}` : "invalid input");
323
+ }
324
+ if (cause instanceof BodyNotJsonError) return err(400, "E_INVALID_INPUT", "request body is not valid JSON", { hint: "send a JSON body with content-type: application/json" });
325
+ return err(500, "E_INTERNAL", cause instanceof Error ? cause.message : "unexpected error");
326
+ }
327
+ //#endregion
328
+ export { mustGetPin as a, registerAttachmentSink as c, match as i, createHubHandler as n, ok as o, err as r, readJson as s, BodyNotJsonError as t };
@@ -0,0 +1,44 @@
1
+ import { t as Attachment } from "./schema-BOTmn5SM.js";
2
+ import { c as PinStore } from "./store-DHbwWu93.js";
3
+ import { HubOptions } from "./hub.js";
4
+ //#region src/attachments.d.ts
5
+ interface AttachmentSink {
6
+ /** Persist bytes; return the finished Attachment (local: path set; cloud R2: url + uploadUrl). */
7
+ write(meta: {
8
+ id: string;
9
+ kind: "screenshot" | "file";
10
+ contentType: string;
11
+ }, bytes: Uint8Array): Promise<{
12
+ attachment: Attachment;
13
+ uploadUrl?: string;
14
+ }>;
15
+ }
16
+ declare function registerAttachmentSink(store: PinStore, sink: AttachmentSink): void;
17
+ //#endregion
18
+ //#region src/attachments-local.d.ts
19
+ /** Bytes to `${dir}/${id}.${ext}`; Bun.write creates the directory tree on demand. */
20
+ declare function localDirSink(dir: string): AttachmentSink;
21
+ //#endregion
22
+ //#region src/git-env.d.ts
23
+ declare function gitEnv(cwd: string): {
24
+ branch?: string;
25
+ commit?: string;
26
+ };
27
+ //#endregion
28
+ //#region src/hub-server.d.ts
29
+ type RealtimeOptions = {
30
+ projectId: string;
31
+ /** Test override for the 5s hello deadline. */
32
+ helloTimeoutMs?: number;
33
+ };
34
+ declare function startHubServer(opts: HubOptions & {
35
+ port?: number;
36
+ idleMs?: number;
37
+ realtime?: RealtimeOptions;
38
+ }): Promise<{
39
+ port: number;
40
+ url: string;
41
+ close: () => Promise<void>;
42
+ }>;
43
+ //#endregion
44
+ export { RealtimeOptions, gitEnv, localDirSink, registerAttachmentSink, startHubServer };
@@ -0,0 +1,251 @@
1
+ import { c as registerAttachmentSink, n as createHubHandler } from "./hub-QYz6OqYQ.js";
2
+ import { ClientHelloSchema, WS_CLOSE_PROTOCOL, WS_CLOSE_UNAUTHORIZED, WS_TOKEN_SUBPROTOCOL_PREFIX, encodeWsEvent } from "./ws-protocol.js";
3
+ //#region src/attachments-local.ts
4
+ const EXTENSIONS = {
5
+ "image/png": "png",
6
+ "image/webp": "webp",
7
+ "image/jpeg": "jpg",
8
+ "image/gif": "gif",
9
+ "text/plain": "txt"
10
+ };
11
+ function extensionFor(contentType) {
12
+ const essence = (contentType.split(";")[0] ?? "").trim().toLowerCase();
13
+ return EXTENSIONS[essence] ?? "bin";
14
+ }
15
+ /** Bytes to `${dir}/${id}.${ext}`; Bun.write creates the directory tree on demand. */
16
+ function localDirSink(dir) {
17
+ return { async write(meta, bytes) {
18
+ const path = `${dir}/${meta.id}.${extensionFor(meta.contentType)}`;
19
+ await Bun.write(path, bytes);
20
+ return { attachment: {
21
+ id: meta.id,
22
+ kind: meta.kind,
23
+ path,
24
+ contentType: meta.contentType
25
+ } };
26
+ } };
27
+ }
28
+ //#endregion
29
+ //#region src/git-env.ts
30
+ function gitEnv(cwd) {
31
+ const branch = revParse(cwd, "--abbrev-ref", "HEAD");
32
+ const commit = revParse(cwd, "HEAD");
33
+ if (branch === null || commit === null) return {};
34
+ return {
35
+ branch,
36
+ commit
37
+ };
38
+ }
39
+ function revParse(cwd, ...args) {
40
+ try {
41
+ const result = Bun.spawnSync([
42
+ "git",
43
+ "rev-parse",
44
+ ...args
45
+ ], { cwd });
46
+ if (!result.success) return null;
47
+ const out = result.stdout.toString().trim();
48
+ return out === "" ? null : out;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ //#endregion
54
+ //#region src/hub-server.ts
55
+ const DEFAULT_IDLE_MS = 18e5;
56
+ const DEFAULT_HELLO_TIMEOUT_MS = 5e3;
57
+ const BACKPRESSURE_LIMIT_BYTES = 1048576;
58
+ function startHubServer(opts) {
59
+ const handler = createHubHandler(opts);
60
+ const idleMs = opts.idleMs ?? DEFAULT_IDLE_MS;
61
+ const { realtime } = opts;
62
+ const topic = `project:${realtime?.projectId ?? ""}`;
63
+ const sockets = { count: 0 };
64
+ let idleTimer;
65
+ const touchIdleTimer = () => {
66
+ clearTimeout(idleTimer);
67
+ idleTimer = setTimeout(() => {
68
+ if (sockets.count > 0) {
69
+ touchIdleTimer();
70
+ return;
71
+ }
72
+ close();
73
+ }, idleMs);
74
+ idleTimer.unref();
75
+ };
76
+ const server = Bun.serve({
77
+ hostname: "127.0.0.1",
78
+ port: opts.port ?? 0,
79
+ fetch: (req, srv) => {
80
+ touchIdleTimer();
81
+ if (!realtime) return handler(req);
82
+ return realtimeFetch(req, srv, handler, opts.token, topic);
83
+ },
84
+ websocket: wsHandlers(opts.store, topic, realtime?.helloTimeoutMs ?? DEFAULT_HELLO_TIMEOUT_MS, sockets)
85
+ });
86
+ const broadcaster = {
87
+ publish: (t, d) => void server.publish(t, d),
88
+ subscriberCount: (t) => server.subscriberCount(t)
89
+ };
90
+ const unsubscribe = realtime ? opts.store.subscribe((event) => broadcaster.publish(topic, encodeWsEvent(event))) : void 0;
91
+ const close = async () => {
92
+ clearTimeout(idleTimer);
93
+ unsubscribe?.();
94
+ await Promise.race([server.stop(true), Bun.sleep(50)]);
95
+ };
96
+ const { port } = server;
97
+ if (port === void 0) throw new Error("hub server bound without a TCP port");
98
+ touchIdleTimer();
99
+ return Promise.resolve({
100
+ port,
101
+ url: server.url.href,
102
+ close
103
+ });
104
+ }
105
+ async function realtimeFetch(req, server, handler, token, topic) {
106
+ const url = new URL(req.url);
107
+ if (url.pathname === "/ws") return upgradeWs(req, url, server, token);
108
+ const origin = req.headers.get("origin");
109
+ const gated = corsGate(req, origin);
110
+ if (gated !== null) return gated;
111
+ let res = await handler(req);
112
+ if (req.method === "GET" && url.pathname === "/summary" && res.status === 200) res = await withConnectedToolbars(res, server.subscriberCount(topic));
113
+ return origin === null ? res : withAllowOrigin(res, origin);
114
+ }
115
+ function corsGate(req, origin) {
116
+ if (origin === null) return null;
117
+ if (!isLoopbackOrigin(origin)) return new Response(null, { status: 403 });
118
+ if (req.method === "OPTIONS") return preflight(origin);
119
+ return null;
120
+ }
121
+ function upgradeWs(req, url, server, token) {
122
+ const entry = (req.headers.get("sec-websocket-protocol") ?? "").split(",").map((part) => part.trim()).find((part) => part.startsWith(WS_TOKEN_SUBPROTOCOL_PREFIX));
123
+ const data = {
124
+ authorized: (entry?.slice(13) ?? url.searchParams.get("token")) === token,
125
+ helloDone: false,
126
+ helloTimer: void 0
127
+ };
128
+ server.timeout(req, 0);
129
+ return server.upgrade(req, {
130
+ data,
131
+ ...entry === void 0 ? {} : { headers: { "sec-websocket-protocol": entry } }
132
+ }) ? void 0 : new Response("websocket upgrade failed", { status: 400 });
133
+ }
134
+ function wsHandlers(store, topic, helloTimeoutMs, sockets) {
135
+ return {
136
+ sendPings: true,
137
+ backpressureLimit: BACKPRESSURE_LIMIT_BYTES,
138
+ closeOnBackpressureLimit: true,
139
+ open(ws) {
140
+ sockets.count += 1;
141
+ if (!ws.data.authorized) {
142
+ ws.close(WS_CLOSE_UNAUTHORIZED, "missing or invalid token");
143
+ return;
144
+ }
145
+ ws.subscribe(topic);
146
+ ws.data.helloTimer = setTimeout(() => {
147
+ ws.close(WS_CLOSE_PROTOCOL, "no hello within deadline");
148
+ }, helloTimeoutMs);
149
+ },
150
+ message(ws, raw) {
151
+ onWsMessage(store, ws, raw);
152
+ },
153
+ close(ws) {
154
+ sockets.count -= 1;
155
+ clearTimeout(ws.data.helloTimer);
156
+ }
157
+ };
158
+ }
159
+ function onWsMessage(store, ws, raw) {
160
+ if (ws.data.helloDone) {
161
+ ws.send(errorFrame("unexpected client frame: no client messages exist after hello"));
162
+ return;
163
+ }
164
+ const hello = parseHello(raw);
165
+ if (hello === null) {
166
+ ws.send(errorFrame("malformed hello"));
167
+ ws.close(WS_CLOSE_PROTOCOL, "malformed hello");
168
+ return;
169
+ }
170
+ if (hello.protocol < 1) {
171
+ ws.send(errorFrame(`protocol ${hello.protocol} below minimum 1`));
172
+ ws.close(WS_CLOSE_PROTOCOL, "protocol below minimum");
173
+ return;
174
+ }
175
+ clearTimeout(ws.data.helloTimer);
176
+ ws.data.helloDone = true;
177
+ const events = store.eventsAfter(hello.lastSeq).map((event) => ({
178
+ type: "event",
179
+ seq: event.seq,
180
+ eventType: event.type,
181
+ at: event.at,
182
+ payload: event.payload
183
+ }));
184
+ ws.send(JSON.stringify({
185
+ type: "catch-up",
186
+ protocol: 1,
187
+ minProtocol: 1,
188
+ lastSeq: store.summary().lastEventSeq,
189
+ events
190
+ }));
191
+ store.cursors.set(hello.consumerId, hello.lastSeq);
192
+ }
193
+ function parseHello(raw) {
194
+ try {
195
+ const parsed = ClientHelloSchema.safeParse(JSON.parse(String(raw)));
196
+ return parsed.success ? parsed.data : null;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+ function errorFrame(message) {
202
+ return JSON.stringify({
203
+ type: "error",
204
+ code: "E_WS_PROTOCOL",
205
+ message
206
+ });
207
+ }
208
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
209
+ "localhost",
210
+ "127.0.0.1",
211
+ "[::1]",
212
+ "::1"
213
+ ]);
214
+ function isLoopbackOrigin(origin) {
215
+ try {
216
+ return LOOPBACK_HOSTS.has(new URL(origin).hostname);
217
+ } catch {
218
+ return false;
219
+ }
220
+ }
221
+ function preflight(origin) {
222
+ return new Response(null, {
223
+ status: 204,
224
+ headers: {
225
+ "access-control-allow-origin": origin,
226
+ "access-control-allow-methods": "GET, POST, OPTIONS",
227
+ "access-control-allow-headers": "authorization, content-type",
228
+ "access-control-max-age": "600",
229
+ vary: "origin"
230
+ }
231
+ });
232
+ }
233
+ function withAllowOrigin(res, origin) {
234
+ const headers = new Headers(res.headers);
235
+ headers.set("access-control-allow-origin", origin);
236
+ headers.set("vary", "origin");
237
+ return new Response(res.body, {
238
+ status: res.status,
239
+ headers
240
+ });
241
+ }
242
+ async function withConnectedToolbars(res, connectedToolbars) {
243
+ const body = await res.json();
244
+ body.data["connectedToolbars"] = connectedToolbars;
245
+ return new Response(JSON.stringify(body), {
246
+ status: res.status,
247
+ headers: { "content-type": "application/json" }
248
+ });
249
+ }
250
+ //#endregion
251
+ export { gitEnv, localDirSink, registerAttachmentSink, startHubServer };