@sandbank.dev/relay 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.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @sandbank.dev/relay
2
+
3
+ > WebSocket relay server for multi-agent communication in [Sandbank](../../README.md).
4
+
5
+ Provides HTTP (long-polling) and WebSocket transport with JSON-RPC 2.0 protocol for real-time messaging and shared context between sandboxes.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @sandbank.dev/relay
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Usually used internally by `createSession()` from `@sandbank.dev/core`, but can be started standalone:
16
+
17
+ ```typescript
18
+ import { startRelay } from '@sandbank.dev/relay'
19
+
20
+ const relay = await startRelay({ port: 4000 })
21
+ console.log(relay.wsUrl) // ws://127.0.0.1:4000
22
+
23
+ // Later...
24
+ await relay.close()
25
+ ```
26
+
27
+ ## Protocol
28
+
29
+ - **Transport:** HTTP `POST /rpc` + WebSocket, dual-channel
30
+ - **Format:** JSON-RPC 2.0
31
+ - **Auth:** `X-Session-Id` + `X-Auth-Token` headers
32
+
33
+ ### RPC Methods
34
+
35
+ | Method | Description |
36
+ |--------|-------------|
37
+ | `session.auth` | WebSocket authentication |
38
+ | `message.send` | Point-to-point messaging |
39
+ | `message.broadcast` | Broadcast to all agents |
40
+ | `message.recv` | Pull messages (supports long-polling) |
41
+ | `context.get/set/delete/keys` | Shared context CRUD |
42
+ | `sandbox.complete` | Mark agent as completed |
43
+
44
+ ## License
45
+
46
+ MIT
@@ -0,0 +1,13 @@
1
+ import type { ConnectedClient } from './types.js';
2
+ export declare class ContextStoreServer {
3
+ private data;
4
+ private watchers;
5
+ get<T = unknown>(key: string): T | undefined;
6
+ set(key: string, value: unknown): void;
7
+ delete(key: string): boolean;
8
+ keys(): string[];
9
+ watch(fn: (key: string, value: unknown) => void): () => void;
10
+ /** 通知所有 WebSocket 客户端上下文变更(排除变更来源) */
11
+ notifyClients(clients: Set<ConnectedClient>, key: string, value: unknown, changedBy: string): void;
12
+ }
13
+ //# sourceMappingURL=context-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-store.d.ts","sourceRoot":"","sources":["../src/context-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,IAAI,CAA6B;IACzC,OAAO,CAAC,QAAQ,CAAmD;IAEnE,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAI5C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAOtC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAU5B,IAAI,IAAI,MAAM,EAAE;IAIhB,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI;IAK5D,sCAAsC;IACtC,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,eAAe,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;CAcnG"}
@@ -0,0 +1,45 @@
1
+ export class ContextStoreServer {
2
+ data = new Map();
3
+ watchers = new Set();
4
+ get(key) {
5
+ return this.data.get(key);
6
+ }
7
+ set(key, value) {
8
+ this.data.set(key, value);
9
+ for (const fn of this.watchers) {
10
+ fn(key, value);
11
+ }
12
+ }
13
+ delete(key) {
14
+ const existed = this.data.delete(key);
15
+ if (existed) {
16
+ for (const fn of this.watchers) {
17
+ fn(key, null);
18
+ }
19
+ }
20
+ return existed;
21
+ }
22
+ keys() {
23
+ return [...this.data.keys()];
24
+ }
25
+ watch(fn) {
26
+ this.watchers.add(fn);
27
+ return () => { this.watchers.delete(fn); };
28
+ }
29
+ /** 通知所有 WebSocket 客户端上下文变更(排除变更来源) */
30
+ notifyClients(clients, key, value, changedBy) {
31
+ const notification = JSON.stringify({
32
+ jsonrpc: '2.0',
33
+ method: 'context.changed',
34
+ params: { key, value, changedBy },
35
+ });
36
+ for (const client of clients) {
37
+ const clientName = client.sandboxName ?? (client.role === 'orchestrator' ? 'orchestrator' : '');
38
+ if (clientName === changedBy)
39
+ continue;
40
+ if (client.ws.readyState === client.ws.OPEN) {
41
+ client.ws.send(notification);
42
+ }
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,3 @@
1
+ export { startRelay } from './server.js';
2
+ export type { RelayServer, RelayServerOptions } from './server.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { startRelay } from './server.js';
@@ -0,0 +1,12 @@
1
+ import type { JsonRpcRequest, JsonRpcResponse } from '@sandbank.dev/core';
2
+ import type { ConnectedClient } from './types.js';
3
+ import type { SessionStore } from './session-store.js';
4
+ /** 处理 JSON-RPC 请求,返回响应 */
5
+ export declare function handleRpc(store: SessionStore, request: JsonRpcRequest, client: {
6
+ sessionId: string;
7
+ sandboxName: string | null;
8
+ role: 'orchestrator' | 'agent';
9
+ }): JsonRpcResponse | Promise<JsonRpcResponse>;
10
+ /** WebSocket 认证:首条消息 */
11
+ export declare function handleAuth(store: SessionStore, request: JsonRpcRequest, wsClient: ConnectedClient): JsonRpcResponse;
12
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,YAAY,CAAA;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,0BAA0B;AAC1B,wBAAgB,SAAS,CACvB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAA;CAAE,GACxF,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAmC5C;AAED,wBAAwB;AACxB,wBAAgB,UAAU,CACxB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,eAAe,GACxB,eAAe,CA6CjB"}
@@ -0,0 +1,201 @@
1
+ /** 处理 JSON-RPC 请求,返回响应 */
2
+ export function handleRpc(store, request, client) {
3
+ const { method, params, id } = request;
4
+ const p = (params ?? {});
5
+ switch (method) {
6
+ case 'session.register':
7
+ return handleRegister(store, id, client.sessionId, p);
8
+ case 'message.send':
9
+ return handleSend(store, id, client.sessionId, client.sandboxName, p);
10
+ case 'message.broadcast':
11
+ return handleBroadcast(store, id, client.sessionId, client.sandboxName, p);
12
+ case 'message.recv':
13
+ return handleRecv(store, id, client.sessionId, client.sandboxName, p);
14
+ case 'context.get':
15
+ return handleContextGet(store, id, client.sessionId, p);
16
+ case 'context.set':
17
+ return handleContextSet(store, id, client.sessionId, client.sandboxName ?? 'orchestrator', p);
18
+ case 'context.delete':
19
+ return handleContextDelete(store, id, client.sessionId, client.sandboxName ?? 'orchestrator', p);
20
+ case 'context.keys':
21
+ return handleContextKeys(store, id, client.sessionId);
22
+ case 'sandbox.complete':
23
+ return handleComplete(store, id, client.sessionId, client.sandboxName, p);
24
+ default:
25
+ return rpcError(id, -32601, `Method not found: ${method}`);
26
+ }
27
+ }
28
+ /** WebSocket 认证:首条消息 */
29
+ export function handleAuth(store, request, wsClient) {
30
+ const { id, params } = request;
31
+ const p = (params ?? {});
32
+ const sessionId = p['sessionId'];
33
+ const sandboxName = p['sandboxName'];
34
+ const token = p['token'];
35
+ const role = p['role'];
36
+ if (!sessionId) {
37
+ return rpcError(id, -32602, 'Missing sessionId');
38
+ }
39
+ const existing = store.getSession(sessionId);
40
+ let session;
41
+ if (existing) {
42
+ // Session 已存在 → 必须提供有效 token
43
+ if (!token) {
44
+ return rpcError(id, -32600, 'Missing token for existing session');
45
+ }
46
+ if (!store.validateToken(sessionId, token)) {
47
+ return rpcError(id, -32600, 'Invalid token');
48
+ }
49
+ session = existing;
50
+ }
51
+ else {
52
+ // Session 不存在 → 创建。This is the primary session creation path for
53
+ // orchestrators connecting via WebSocket. The relay should be bound to
54
+ // trusted networks (default: 127.0.0.1). maxSessions limits DoS risk.
55
+ session = store.getOrCreateSession(sessionId, token);
56
+ }
57
+ // Validate sandboxName: agents must claim a registered sandbox name
58
+ if (sandboxName && !session.sandboxes.has(sandboxName)) {
59
+ return rpcError(id, -32600, `Sandbox not registered: ${sandboxName}`);
60
+ }
61
+ wsClient.sessionId = sessionId;
62
+ wsClient.sandboxName = sandboxName ?? null;
63
+ wsClient.role = role === 'orchestrator' ? 'orchestrator' : 'agent';
64
+ // 加入 session
65
+ session.clients.add(wsClient);
66
+ return rpcResult(id, { ok: true, sessionId, sandboxName: sandboxName ?? null, token: session.token });
67
+ }
68
+ // --- Handlers ---
69
+ function handleRegister(store, id, sessionId, params) {
70
+ const name = params['name'];
71
+ const sandboxId = params['sandboxId'];
72
+ if (!name || !sandboxId) {
73
+ return rpcError(id, -32602, 'Missing name or sandboxId');
74
+ }
75
+ try {
76
+ store.registerSandbox(sessionId, name, sandboxId);
77
+ return rpcResult(id, { ok: true });
78
+ }
79
+ catch (e) {
80
+ return rpcError(id, -32000, e.message);
81
+ }
82
+ }
83
+ function handleSend(store, id, sessionId, fromName, params) {
84
+ const session = store.getSession(sessionId);
85
+ if (!session)
86
+ return rpcError(id, -32000, 'Session not found');
87
+ const to = params['to'];
88
+ const type = params['type'];
89
+ const payload = params['payload'];
90
+ const priority = params['priority'] === 'steer' ? 'steer' : 'normal';
91
+ if (!to || !type) {
92
+ return rpcError(id, -32602, 'Missing to or type');
93
+ }
94
+ const msg = {
95
+ from: fromName ?? 'orchestrator',
96
+ to,
97
+ type,
98
+ payload: payload ?? null,
99
+ priority,
100
+ timestamp: new Date().toISOString(),
101
+ };
102
+ store.enqueueMessage(session, to, msg);
103
+ return rpcResult(id, { ok: true });
104
+ }
105
+ function handleBroadcast(store, id, sessionId, fromName, params) {
106
+ const session = store.getSession(sessionId);
107
+ if (!session)
108
+ return rpcError(id, -32000, 'Session not found');
109
+ const type = params['type'];
110
+ const payload = params['payload'];
111
+ const priority = params['priority'] === 'steer' ? 'steer' : 'normal';
112
+ if (!type) {
113
+ return rpcError(id, -32602, 'Missing type');
114
+ }
115
+ const msg = {
116
+ from: fromName ?? 'orchestrator',
117
+ to: null,
118
+ type,
119
+ payload: payload ?? null,
120
+ priority,
121
+ timestamp: new Date().toISOString(),
122
+ };
123
+ store.broadcastMessage(session, msg, fromName ?? '');
124
+ return rpcResult(id, { ok: true });
125
+ }
126
+ function handleRecv(store, id, sessionId, sandboxName, params) {
127
+ const session = store.getSession(sessionId);
128
+ if (!session)
129
+ return rpcError(id, -32000, 'Session not found');
130
+ if (!sandboxName)
131
+ return rpcError(id, -32602, 'No sandbox name — cannot recv');
132
+ const limit = params['limit'] ?? 100;
133
+ const wait = params['wait'] ?? 0;
134
+ if (wait <= 0) {
135
+ const msgs = store.drainQueue(session, sandboxName, limit);
136
+ return rpcResult(id, { messages: msgs });
137
+ }
138
+ // Long polling
139
+ return store.waitForMessages(session, sandboxName, wait, limit).then((msgs) => {
140
+ return rpcResult(id, { messages: msgs });
141
+ });
142
+ }
143
+ function handleContextGet(store, id, sessionId, params) {
144
+ const ctx = store.getContext(sessionId);
145
+ if (!ctx)
146
+ return rpcError(id, -32000, 'Session not found');
147
+ const key = params['key'];
148
+ if (!key)
149
+ return rpcError(id, -32602, 'Missing key');
150
+ const value = ctx.get(key);
151
+ return rpcResult(id, { value: value ?? null });
152
+ }
153
+ function handleContextSet(store, id, sessionId, changedBy, params) {
154
+ const session = store.getSession(sessionId);
155
+ const ctx = store.getContext(sessionId);
156
+ if (!session || !ctx)
157
+ return rpcError(id, -32000, 'Session not found');
158
+ const key = params['key'];
159
+ const value = params['value'];
160
+ if (!key)
161
+ return rpcError(id, -32602, 'Missing key');
162
+ ctx.set(key, value);
163
+ ctx.notifyClients(session.clients, key, value, changedBy);
164
+ return rpcResult(id, { ok: true });
165
+ }
166
+ function handleContextDelete(store, id, sessionId, changedBy, params) {
167
+ const session = store.getSession(sessionId);
168
+ const ctx = store.getContext(sessionId);
169
+ if (!ctx || !session)
170
+ return rpcError(id, -32000, 'Session not found');
171
+ const key = params['key'];
172
+ if (!key)
173
+ return rpcError(id, -32602, 'Missing key');
174
+ ctx.delete(key);
175
+ ctx.notifyClients(session.clients, key, null, changedBy);
176
+ return rpcResult(id, { ok: true });
177
+ }
178
+ function handleContextKeys(store, id, sessionId) {
179
+ const ctx = store.getContext(sessionId);
180
+ if (!ctx)
181
+ return rpcError(id, -32000, 'Session not found');
182
+ return rpcResult(id, { keys: ctx.keys() });
183
+ }
184
+ function handleComplete(store, id, sessionId, sandboxName, params) {
185
+ const session = store.getSession(sessionId);
186
+ if (!session)
187
+ return rpcError(id, -32000, 'Session not found');
188
+ if (!sandboxName)
189
+ return rpcError(id, -32602, 'No sandbox name — cannot complete');
190
+ const status = params['status'] ?? 'success';
191
+ const summary = params['summary'] ?? '';
192
+ store.completeSandbox(session, sandboxName, status, summary);
193
+ return rpcResult(id, { ok: true });
194
+ }
195
+ // --- Helpers ---
196
+ function rpcResult(id, result) {
197
+ return { jsonrpc: '2.0', id, result };
198
+ }
199
+ function rpcError(id, code, message) {
200
+ return { jsonrpc: '2.0', id, error: { code, message } };
201
+ }
@@ -0,0 +1,13 @@
1
+ import type { SessionStoreOptions } from './types.js';
2
+ export interface RelayServerOptions extends SessionStoreOptions {
3
+ port?: number;
4
+ host?: string;
5
+ }
6
+ export interface RelayServer {
7
+ port: number;
8
+ url: string;
9
+ wsUrl: string;
10
+ close(): Promise<void>;
11
+ }
12
+ export declare function startRelay(options?: RelayServerOptions): Promise<RelayServer>;
13
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAmB,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAItE,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED,wBAAsB,UAAU,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAqCvF"}
package/dist/server.js ADDED
@@ -0,0 +1,203 @@
1
+ import { createServer } from 'node:http';
2
+ import { WebSocketServer } from 'ws';
3
+ import { SessionStore } from './session-store.js';
4
+ import { handleRpc, handleAuth } from './protocol.js';
5
+ export async function startRelay(options = {}) {
6
+ const port = options.port ?? 0;
7
+ const host = options.host ?? '127.0.0.1';
8
+ const store = new SessionStore(options);
9
+ const httpServer = createServer((req, res) => handleHttp(store, req, res));
10
+ const wss = new WebSocketServer({ server: httpServer });
11
+ wss.on('connection', (ws) => {
12
+ handleWsConnection(store, ws);
13
+ });
14
+ return new Promise((resolve) => {
15
+ httpServer.listen(port, host, () => {
16
+ const addr = httpServer.address();
17
+ const assignedPort = typeof addr === 'string' ? port : addr.port;
18
+ const url = `http://${host}:${assignedPort}`;
19
+ const wsUrl = `ws://${host}:${assignedPort}`;
20
+ resolve({
21
+ port: assignedPort,
22
+ url,
23
+ wsUrl,
24
+ async close() {
25
+ store.dispose();
26
+ // 关闭所有 WebSocket 连接
27
+ for (const ws of wss.clients) {
28
+ ws.close(1000, 'relay shutting down');
29
+ }
30
+ wss.close();
31
+ return new Promise((res, rej) => {
32
+ httpServer.close((err) => err ? rej(err) : res());
33
+ });
34
+ },
35
+ });
36
+ });
37
+ });
38
+ }
39
+ // --- HTTP Handler ---
40
+ function handleHttp(store, req, res) {
41
+ if (req.method !== 'POST' || req.url !== '/rpc') {
42
+ res.writeHead(404, { 'Content-Type': 'application/json' });
43
+ res.end(JSON.stringify({ error: 'Not found' }));
44
+ return;
45
+ }
46
+ // 读取请求头
47
+ const sessionId = req.headers['x-session-id'];
48
+ const sandboxName = req.headers['x-sandbox-name'];
49
+ const authToken = req.headers['x-auth-token'];
50
+ if (!sessionId) {
51
+ res.writeHead(400, { 'Content-Type': 'application/json' });
52
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Missing X-Session-Id header' } }));
53
+ return;
54
+ }
55
+ // Token 验证:所有请求都必须提供 token
56
+ if (!authToken) {
57
+ res.writeHead(403, { 'Content-Type': 'application/json' });
58
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Missing X-Auth-Token header' } }));
59
+ return;
60
+ }
61
+ const responseHeaders = {
62
+ 'Content-Type': 'application/json',
63
+ };
64
+ const MAX_BODY_SIZE = 1 * 1024 * 1024; // 1MB
65
+ let body = '';
66
+ let bodySize = 0;
67
+ let aborted = false;
68
+ req.on('data', (chunk) => {
69
+ bodySize += chunk.length;
70
+ if (bodySize > MAX_BODY_SIZE) {
71
+ aborted = true;
72
+ res.writeHead(413, responseHeaders);
73
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Payload too large' } }));
74
+ req.destroy();
75
+ return;
76
+ }
77
+ body += chunk.toString();
78
+ });
79
+ req.on('end', () => {
80
+ if (aborted)
81
+ return;
82
+ let request;
83
+ try {
84
+ request = JSON.parse(body);
85
+ }
86
+ catch {
87
+ res.writeHead(400, responseHeaders);
88
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }));
89
+ return;
90
+ }
91
+ // Session resolution: existing sessions require valid token;
92
+ // new sessions can only be created via session.register
93
+ let session = store.getSession(sessionId);
94
+ if (session) {
95
+ if (!store.validateToken(sessionId, authToken)) {
96
+ res.writeHead(403, responseHeaders);
97
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32600, message: 'Invalid token' } }));
98
+ return;
99
+ }
100
+ }
101
+ else {
102
+ if (request.method !== 'session.register') {
103
+ res.writeHead(403, responseHeaders);
104
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32600, message: 'Session not found' } }));
105
+ return;
106
+ }
107
+ session = store.createSession(sessionId, authToken);
108
+ }
109
+ store.touch(session);
110
+ // Validate sandboxName against registered sandboxes (skip for session.register itself)
111
+ if (sandboxName && request.method !== 'session.register' && !session.sandboxes.has(sandboxName)) {
112
+ res.writeHead(403, responseHeaders);
113
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32600, message: `Sandbox not registered: ${sandboxName}` } }));
114
+ return;
115
+ }
116
+ const client = {
117
+ sessionId,
118
+ sandboxName: sandboxName ?? null,
119
+ role: sandboxName ? 'agent' : 'orchestrator',
120
+ };
121
+ const result = handleRpc(store, request, client);
122
+ if (result instanceof Promise) {
123
+ result.then((response) => {
124
+ res.writeHead(200, responseHeaders);
125
+ res.end(JSON.stringify(response));
126
+ }).catch((err) => {
127
+ res.writeHead(500, responseHeaders);
128
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message: err.message } }));
129
+ });
130
+ }
131
+ else {
132
+ res.writeHead(200, responseHeaders);
133
+ res.end(JSON.stringify(result));
134
+ }
135
+ });
136
+ }
137
+ // --- WebSocket Handler ---
138
+ function handleWsConnection(store, ws) {
139
+ const client = {
140
+ ws,
141
+ sessionId: '',
142
+ sandboxName: null,
143
+ role: 'agent',
144
+ };
145
+ let authenticated = false;
146
+ ws.on('message', (data) => {
147
+ let request;
148
+ try {
149
+ request = JSON.parse(data.toString());
150
+ }
151
+ catch {
152
+ ws.send(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }));
153
+ return;
154
+ }
155
+ // 未认证时,只接受 session.auth
156
+ if (!authenticated) {
157
+ if (request.method !== 'session.auth') {
158
+ ws.send(JSON.stringify({
159
+ jsonrpc: '2.0',
160
+ id: request.id,
161
+ error: { code: -32600, message: 'Must authenticate first (session.auth)' },
162
+ }));
163
+ return;
164
+ }
165
+ const response = handleAuth(store, request, client);
166
+ authenticated = !response.error;
167
+ ws.send(JSON.stringify(response));
168
+ return;
169
+ }
170
+ // 已认证,处理 RPC
171
+ const result = handleRpc(store, request, {
172
+ sessionId: client.sessionId,
173
+ sandboxName: client.sandboxName,
174
+ role: client.role,
175
+ });
176
+ if (result instanceof Promise) {
177
+ result.then((response) => {
178
+ if (ws.readyState === ws.OPEN)
179
+ ws.send(JSON.stringify(response));
180
+ }).catch((err) => {
181
+ if (ws.readyState === ws.OPEN) {
182
+ ws.send(JSON.stringify({
183
+ jsonrpc: '2.0',
184
+ id: request.id,
185
+ error: { code: -32000, message: err.message },
186
+ }));
187
+ }
188
+ });
189
+ }
190
+ else {
191
+ if (ws.readyState === ws.OPEN)
192
+ ws.send(JSON.stringify(result));
193
+ }
194
+ });
195
+ ws.on('close', () => {
196
+ if (client.sessionId) {
197
+ const session = store.getSession(client.sessionId);
198
+ if (session) {
199
+ session.clients.delete(client);
200
+ }
201
+ }
202
+ });
203
+ }
@@ -0,0 +1,40 @@
1
+ import { ContextStoreServer } from './context-store.js';
2
+ import type { RelaySession, QueuedMessage, SessionStoreOptions } from './types.js';
3
+ export declare class SessionStore {
4
+ private sessions;
5
+ private contexts;
6
+ private sweepTimer;
7
+ readonly sessionTtlMs: number;
8
+ readonly maxSessions: number;
9
+ readonly maxQueueSize: number;
10
+ constructor(options?: SessionStoreOptions);
11
+ /** 停止清扫定时器(用于优雅关闭) */
12
+ dispose(): void;
13
+ /** 获取当前 session 数量 */
14
+ get size(): number;
15
+ createSession(id: string, token?: string): RelaySession;
16
+ getSession(id: string): RelaySession | undefined;
17
+ getOrCreateSession(id: string, token?: string): RelaySession;
18
+ /** 更新 session 的最后活跃时间 */
19
+ touch(session: RelaySession): void;
20
+ validateToken(sessionId: string, token: string): boolean;
21
+ getContext(sessionId: string): ContextStoreServer | undefined;
22
+ deleteSession(id: string): void;
23
+ /** 清扫过期且无活跃连接的 session */
24
+ sweep(): number;
25
+ /** 注册沙箱名 */
26
+ registerSandbox(sessionId: string, name: string, sandboxId: string): void;
27
+ /** 发送消息到指定沙箱的队列 */
28
+ enqueueMessage(session: RelaySession, to: string, msg: QueuedMessage): void;
29
+ /** 广播消息到所有沙箱 */
30
+ broadcastMessage(session: RelaySession, msg: QueuedMessage, excludeSender: string): void;
31
+ /** 消耗队列:steer 优先排序 */
32
+ drainQueue(session: RelaySession, sandboxName: string, limit?: number): QueuedMessage[];
33
+ /** Long polling:等待消息到达 */
34
+ waitForMessages(session: RelaySession, sandboxName: string, waitMs: number, limit: number): Promise<QueuedMessage[]>;
35
+ /** 标记沙箱完成 */
36
+ completeSandbox(session: RelaySession, sandboxName: string, status: string, summary: string): void;
37
+ private pushToWebSocketClient;
38
+ private pushToOrchestrator;
39
+ }
40
+ //# sourceMappingURL=session-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAgB,aAAa,EAAmB,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAOjH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,UAAU,CAA8C;IAEhE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;gBAEjB,OAAO,GAAE,mBAAwB;IAY7C,sBAAsB;IACtB,OAAO,IAAI,IAAI;IAOf,sBAAsB;IACtB,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,YAAY;IA2BvD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAIhD,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,YAAY;IAI5D,yBAAyB;IACzB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAIlC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAQxD,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAI7D,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAqB/B,0BAA0B;IAC1B,KAAK,IAAI,MAAM;IAqBf,YAAY;IACZ,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAWzE,mBAAmB;IACnB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,IAAI;IAoC3E,gBAAgB;IAChB,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAYxF,sBAAsB;IACtB,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,SAAM,GAAG,aAAa,EAAE;IAoBpF,0BAA0B;IAC1B,eAAe,CACb,OAAO,EAAE,YAAY,EACrB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,EAAE,CAAC;IA+B3B,aAAa;IACb,eAAe,CACb,OAAO,EAAE,YAAY,EACrB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,IAAI;IA+BP,OAAO,CAAC,qBAAqB;IAa7B,OAAO,CAAC,kBAAkB;CAY3B"}
@@ -0,0 +1,282 @@
1
+ import { randomUUID, timingSafeEqual } from 'node:crypto';
2
+ import { ContextStoreServer } from './context-store.js';
3
+ const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000; // 30 minutes
4
+ const DEFAULT_MAX_SESSIONS = 1000;
5
+ const DEFAULT_MAX_QUEUE_SIZE = 10_000;
6
+ const DEFAULT_SWEEP_INTERVAL_MS = 60 * 1000; // 60 seconds
7
+ export class SessionStore {
8
+ sessions = new Map();
9
+ contexts = new Map();
10
+ sweepTimer = null;
11
+ sessionTtlMs;
12
+ maxSessions;
13
+ maxQueueSize;
14
+ constructor(options = {}) {
15
+ this.sessionTtlMs = options.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS;
16
+ this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
17
+ this.maxQueueSize = options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
18
+ const sweepInterval = options.sweepIntervalMs ?? DEFAULT_SWEEP_INTERVAL_MS;
19
+ if (this.sessionTtlMs > 0) {
20
+ this.sweepTimer = setInterval(() => this.sweep(), sweepInterval);
21
+ this.sweepTimer.unref();
22
+ }
23
+ }
24
+ /** 停止清扫定时器(用于优雅关闭) */
25
+ dispose() {
26
+ if (this.sweepTimer) {
27
+ clearInterval(this.sweepTimer);
28
+ this.sweepTimer = null;
29
+ }
30
+ }
31
+ /** 获取当前 session 数量 */
32
+ get size() {
33
+ return this.sessions.size;
34
+ }
35
+ createSession(id, token) {
36
+ // 容量检查
37
+ if (this.sessions.size >= this.maxSessions) {
38
+ // 先尝试清扫过期 session
39
+ this.sweep();
40
+ if (this.sessions.size >= this.maxSessions) {
41
+ throw new Error(`Max sessions reached (${this.maxSessions})`);
42
+ }
43
+ }
44
+ const now = Date.now();
45
+ const session = {
46
+ id,
47
+ token: token ?? randomUUID(),
48
+ sandboxes: new Map(),
49
+ context: new Map(),
50
+ clients: new Set(),
51
+ messageQueues: new Map(),
52
+ pollWaiters: new Map(),
53
+ createdAt: now,
54
+ lastActivityAt: now,
55
+ };
56
+ this.sessions.set(id, session);
57
+ this.contexts.set(id, new ContextStoreServer());
58
+ return session;
59
+ }
60
+ getSession(id) {
61
+ return this.sessions.get(id);
62
+ }
63
+ getOrCreateSession(id, token) {
64
+ return this.getSession(id) ?? this.createSession(id, token);
65
+ }
66
+ /** 更新 session 的最后活跃时间 */
67
+ touch(session) {
68
+ session.lastActivityAt = Date.now();
69
+ }
70
+ validateToken(sessionId, token) {
71
+ const session = this.getSession(sessionId);
72
+ if (!session)
73
+ return false;
74
+ const a = Buffer.from(session.token);
75
+ const b = Buffer.from(token);
76
+ return a.length === b.length && timingSafeEqual(a, b);
77
+ }
78
+ getContext(sessionId) {
79
+ return this.contexts.get(sessionId);
80
+ }
81
+ deleteSession(id) {
82
+ const session = this.sessions.get(id);
83
+ if (session) {
84
+ // 清理 poll waiters
85
+ for (const waiters of session.pollWaiters.values()) {
86
+ for (const w of waiters) {
87
+ clearTimeout(w.timer);
88
+ w.resolve([]);
89
+ }
90
+ }
91
+ // 关闭所有客户端连接
92
+ for (const client of session.clients) {
93
+ if (client.ws.readyState === client.ws.OPEN) {
94
+ client.ws.close(1000, 'session closed');
95
+ }
96
+ }
97
+ }
98
+ this.sessions.delete(id);
99
+ this.contexts.delete(id);
100
+ }
101
+ /** 清扫过期且无活跃连接的 session */
102
+ sweep() {
103
+ if (this.sessionTtlMs <= 0)
104
+ return 0;
105
+ const now = Date.now();
106
+ let evicted = 0;
107
+ const toEvict = [];
108
+ for (const [id, session] of this.sessions) {
109
+ const idle = now - session.lastActivityAt;
110
+ if (idle > this.sessionTtlMs && session.clients.size === 0) {
111
+ toEvict.push(id);
112
+ }
113
+ }
114
+ for (const id of toEvict) {
115
+ this.deleteSession(id);
116
+ evicted++;
117
+ }
118
+ return evicted;
119
+ }
120
+ /** 注册沙箱名 */
121
+ registerSandbox(sessionId, name, sandboxId) {
122
+ const session = this.getSession(sessionId);
123
+ if (!session)
124
+ throw new Error(`Session not found: ${sessionId}`);
125
+ if (name === 'orchestrator')
126
+ throw new Error('Reserved name: orchestrator');
127
+ if (session.sandboxes.has(name))
128
+ throw new Error(`Sandbox already registered: ${name}`);
129
+ this.touch(session);
130
+ session.sandboxes.set(name, { name, sandboxId, state: 'running' });
131
+ session.messageQueues.set(name, []);
132
+ }
133
+ /** 发送消息到指定沙箱的队列 */
134
+ enqueueMessage(session, to, msg) {
135
+ const queue = session.messageQueues.get(to);
136
+ if (!queue) {
137
+ // 目标不是已注册沙箱 — 可能是发给 orchestrator
138
+ this.pushToOrchestrator(session, msg);
139
+ return;
140
+ }
141
+ // 队列大小限制:丢弃最旧的 normal 消息
142
+ if (queue.length >= this.maxQueueSize) {
143
+ const normalIdx = queue.findIndex(m => m.priority === 'normal');
144
+ if (normalIdx >= 0) {
145
+ queue.splice(normalIdx, 1);
146
+ }
147
+ else {
148
+ // 全是 steer 消息,丢弃最旧的
149
+ queue.shift();
150
+ }
151
+ }
152
+ queue.push(msg);
153
+ this.touch(session);
154
+ // 检查是否有 long-polling 等待者(优先于 WebSocket 推送,避免双重投递)
155
+ const waiters = session.pollWaiters.get(to);
156
+ if (waiters && waiters.length > 0) {
157
+ const waiter = waiters.shift();
158
+ clearTimeout(waiter.timer);
159
+ const msgs = this.drainQueue(session, to);
160
+ waiter.resolve(msgs);
161
+ return;
162
+ }
163
+ // WebSocket 实时推送
164
+ this.pushToWebSocketClient(session, to, msg);
165
+ }
166
+ /** 广播消息到所有沙箱 */
167
+ broadcastMessage(session, msg, excludeSender) {
168
+ for (const [name] of session.sandboxes) {
169
+ if (name !== excludeSender) {
170
+ this.enqueueMessage(session, name, msg);
171
+ }
172
+ }
173
+ // 也推送给编排者(除非发送者就是编排者)
174
+ if (excludeSender !== '') {
175
+ this.pushToOrchestrator(session, msg);
176
+ }
177
+ }
178
+ /** 消耗队列:steer 优先排序 */
179
+ drainQueue(session, sandboxName, limit = 100) {
180
+ const queue = session.messageQueues.get(sandboxName);
181
+ if (!queue || queue.length === 0)
182
+ return [];
183
+ // steer 优先,保持插入顺序(stable partition)
184
+ const steer = [];
185
+ const normal = [];
186
+ for (const m of queue) {
187
+ if (m.priority === 'steer')
188
+ steer.push(m);
189
+ else
190
+ normal.push(m);
191
+ }
192
+ const sorted = [...steer, ...normal];
193
+ queue.length = 0;
194
+ queue.push(...sorted);
195
+ this.touch(session);
196
+ const msgs = queue.splice(0, limit);
197
+ return msgs;
198
+ }
199
+ /** Long polling:等待消息到达 */
200
+ waitForMessages(session, sandboxName, waitMs, limit) {
201
+ // 先检查队列是否已有消息
202
+ const existing = this.drainQueue(session, sandboxName, limit);
203
+ if (existing.length > 0)
204
+ return Promise.resolve(existing);
205
+ // 限制每个沙箱的并发 long-poll waiter 数
206
+ const MAX_POLL_WAITERS = 10;
207
+ const currentWaiters = session.pollWaiters.get(sandboxName);
208
+ if (currentWaiters && currentWaiters.length >= MAX_POLL_WAITERS) {
209
+ return Promise.resolve([]);
210
+ }
211
+ // 没有消息,挂起等待
212
+ return new Promise((resolve) => {
213
+ const timer = setTimeout(() => {
214
+ // 超时,返回空数组
215
+ const waiters = session.pollWaiters.get(sandboxName);
216
+ if (waiters) {
217
+ const idx = waiters.findIndex((w) => w.resolve === resolve);
218
+ if (idx >= 0)
219
+ waiters.splice(idx, 1);
220
+ }
221
+ resolve([]);
222
+ }, waitMs);
223
+ if (!session.pollWaiters.has(sandboxName)) {
224
+ session.pollWaiters.set(sandboxName, []);
225
+ }
226
+ session.pollWaiters.get(sandboxName).push({ resolve, timer });
227
+ });
228
+ }
229
+ /** 标记沙箱完成 */
230
+ completeSandbox(session, sandboxName, status, summary) {
231
+ const entry = session.sandboxes.get(sandboxName);
232
+ if (!entry)
233
+ return;
234
+ entry.state = 'completed';
235
+ entry.completion = {
236
+ status,
237
+ summary,
238
+ timestamp: new Date().toISOString(),
239
+ };
240
+ this.touch(session);
241
+ // 通知编排者
242
+ const notification = JSON.stringify({
243
+ jsonrpc: '2.0',
244
+ method: 'sandbox.state',
245
+ params: {
246
+ name: sandboxName,
247
+ state: 'completed',
248
+ status,
249
+ summary,
250
+ },
251
+ });
252
+ for (const client of session.clients) {
253
+ if (client.role === 'orchestrator' && client.ws.readyState === client.ws.OPEN) {
254
+ client.ws.send(notification);
255
+ }
256
+ }
257
+ }
258
+ pushToWebSocketClient(session, targetName, msg) {
259
+ const notification = JSON.stringify({
260
+ jsonrpc: '2.0',
261
+ method: 'message',
262
+ params: msg,
263
+ });
264
+ for (const client of session.clients) {
265
+ if (client.sandboxName === targetName && client.ws.readyState === client.ws.OPEN) {
266
+ client.ws.send(notification);
267
+ }
268
+ }
269
+ }
270
+ pushToOrchestrator(session, msg) {
271
+ const notification = JSON.stringify({
272
+ jsonrpc: '2.0',
273
+ method: 'message',
274
+ params: msg,
275
+ });
276
+ for (const client of session.clients) {
277
+ if (client.role === 'orchestrator' && client.ws.readyState === client.ws.OPEN) {
278
+ client.ws.send(notification);
279
+ }
280
+ }
281
+ }
282
+ }
@@ -0,0 +1,56 @@
1
+ import type { WebSocket } from 'ws';
2
+ /** Session 内已注册的沙箱 */
3
+ export interface SandboxEntry {
4
+ name: string;
5
+ sandboxId: string;
6
+ state: 'running' | 'completed';
7
+ completion?: {
8
+ status: string;
9
+ summary: string;
10
+ timestamp: string;
11
+ };
12
+ }
13
+ /** 已认证的客户端连接 */
14
+ export interface ConnectedClient {
15
+ ws: WebSocket;
16
+ sessionId: string;
17
+ sandboxName: string | null;
18
+ role: 'orchestrator' | 'agent';
19
+ }
20
+ /** 消息队列中的消息 */
21
+ export interface QueuedMessage {
22
+ from: string;
23
+ to: string | null;
24
+ type: string;
25
+ payload: unknown;
26
+ priority: 'normal' | 'steer';
27
+ timestamp: string;
28
+ }
29
+ /** Relay 会话状态 */
30
+ export interface RelaySession {
31
+ id: string;
32
+ token: string;
33
+ sandboxes: Map<string, SandboxEntry>;
34
+ context: Map<string, unknown>;
35
+ clients: Set<ConnectedClient>;
36
+ messageQueues: Map<string, QueuedMessage[]>;
37
+ /** Long-polling 等待者:sandbox name -> resolve 函数 */
38
+ pollWaiters: Map<string, Array<{
39
+ resolve: (msgs: QueuedMessage[]) => void;
40
+ timer: ReturnType<typeof setTimeout>;
41
+ }>>;
42
+ createdAt: number;
43
+ lastActivityAt: number;
44
+ }
45
+ /** SessionStore 配置选项 */
46
+ export interface SessionStoreOptions {
47
+ /** Session 空闲超时(毫秒),默认 30 分钟 */
48
+ sessionTtlMs?: number;
49
+ /** 最大 session 数,默认 1000 */
50
+ maxSessions?: number;
51
+ /** 每个沙箱的最大消息队列长度,默认 10000 */
52
+ maxQueueSize?: number;
53
+ /** 清扫间隔(毫秒),默认 60 秒 */
54
+ sweepIntervalMs?: number;
55
+ }
56
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAA;AAEnC,sBAAsB;AACtB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,SAAS,GAAG,WAAW,CAAA;IAC9B,UAAU,CAAC,EAAE;QACX,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,MAAM,CAAA;QACf,SAAS,EAAE,MAAM,CAAA;KAClB,CAAA;CACF;AAED,gBAAgB;AAChB,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,SAAS,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,IAAI,EAAE,cAAc,GAAG,OAAO,CAAA;CAC/B;AAED,eAAe;AACf,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,iBAAiB;AACjB,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IACpC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,OAAO,EAAE,GAAG,CAAC,eAAe,CAAC,CAAA;IAC7B,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAA;IAC3C,kDAAkD;IAClD,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC;QAC7B,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,IAAI,CAAA;QACxC,KAAK,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;KACrC,CAAC,CAAC,CAAA;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAwB;AACxB,MAAM,WAAW,mBAAmB;IAClC,gCAAgC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2BAA2B;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6BAA6B;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,uBAAuB;IACvB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@sandbank.dev/relay",
3
+ "version": "0.1.0",
4
+ "description": "WebSocket relay server for multi-agent communication in Sandbank",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://sandbank.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/chekusu/sandbank.git",
11
+ "directory": "packages/relay"
12
+ },
13
+ "keywords": [
14
+ "sandbox",
15
+ "ai-agent",
16
+ "relay",
17
+ "websocket",
18
+ "multi-agent"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "ws": "^8.18.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/ws": "^8.18.0",
34
+ "typescript": "^5.7.3",
35
+ "@sandbank.dev/core": "0.1.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@sandbank.dev/core": "0.1.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "typecheck": "tsc --noEmit",
43
+ "clean": "rm -rf dist"
44
+ }
45
+ }