aegis-bridge 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/LICENSE +21 -0
- package/README.md +404 -0
- package/dashboard/dist/assets/index-BoZwGLAx.css +32 -0
- package/dashboard/dist/assets/index-C61BkKH-.js +312 -0
- package/dashboard/dist/assets/index-C61BkKH-.js.map +1 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/api-contracts.d.ts +229 -0
- package/dist/api-contracts.js +7 -0
- package/dist/api-contracts.typecheck.d.ts +14 -0
- package/dist/api-contracts.typecheck.js +1 -0
- package/dist/api-error-envelope.d.ts +15 -0
- package/dist/api-error-envelope.js +80 -0
- package/dist/auth.d.ts +87 -0
- package/dist/auth.js +276 -0
- package/dist/channels/index.d.ts +8 -0
- package/dist/channels/index.js +8 -0
- package/dist/channels/manager.d.ts +47 -0
- package/dist/channels/manager.js +115 -0
- package/dist/channels/telegram-style.d.ts +118 -0
- package/dist/channels/telegram-style.js +202 -0
- package/dist/channels/telegram.d.ts +91 -0
- package/dist/channels/telegram.js +1518 -0
- package/dist/channels/types.d.ts +77 -0
- package/dist/channels/types.js +8 -0
- package/dist/channels/webhook.d.ts +60 -0
- package/dist/channels/webhook.js +216 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +252 -0
- package/dist/config.d.ts +90 -0
- package/dist/config.js +214 -0
- package/dist/consensus.d.ts +16 -0
- package/dist/consensus.js +19 -0
- package/dist/continuation-pointer.d.ts +11 -0
- package/dist/continuation-pointer.js +65 -0
- package/dist/diagnostics.d.ts +27 -0
- package/dist/diagnostics.js +95 -0
- package/dist/error-categories.d.ts +39 -0
- package/dist/error-categories.js +73 -0
- package/dist/events.d.ts +133 -0
- package/dist/events.js +389 -0
- package/dist/fault-injection.d.ts +29 -0
- package/dist/fault-injection.js +115 -0
- package/dist/file-utils.d.ts +2 -0
- package/dist/file-utils.js +37 -0
- package/dist/handshake.d.ts +60 -0
- package/dist/handshake.js +124 -0
- package/dist/hook-settings.d.ts +80 -0
- package/dist/hook-settings.js +272 -0
- package/dist/hook.d.ts +19 -0
- package/dist/hook.js +231 -0
- package/dist/hooks.d.ts +32 -0
- package/dist/hooks.js +364 -0
- package/dist/jsonl-watcher.d.ts +59 -0
- package/dist/jsonl-watcher.js +166 -0
- package/dist/logger.d.ts +35 -0
- package/dist/logger.js +65 -0
- package/dist/mcp-server.d.ts +123 -0
- package/dist/mcp-server.js +869 -0
- package/dist/memory-bridge.d.ts +27 -0
- package/dist/memory-bridge.js +137 -0
- package/dist/memory-routes.d.ts +3 -0
- package/dist/memory-routes.js +100 -0
- package/dist/metrics.d.ts +126 -0
- package/dist/metrics.js +286 -0
- package/dist/model-router.d.ts +53 -0
- package/dist/model-router.js +150 -0
- package/dist/monitor.d.ts +103 -0
- package/dist/monitor.js +820 -0
- package/dist/path-utils.d.ts +11 -0
- package/dist/path-utils.js +21 -0
- package/dist/permission-evaluator.d.ts +10 -0
- package/dist/permission-evaluator.js +48 -0
- package/dist/permission-guard.d.ts +51 -0
- package/dist/permission-guard.js +196 -0
- package/dist/permission-request-manager.d.ts +12 -0
- package/dist/permission-request-manager.js +36 -0
- package/dist/permission-routes.d.ts +7 -0
- package/dist/permission-routes.js +28 -0
- package/dist/pipeline.d.ts +97 -0
- package/dist/pipeline.js +291 -0
- package/dist/process-utils.d.ts +4 -0
- package/dist/process-utils.js +73 -0
- package/dist/question-manager.d.ts +54 -0
- package/dist/question-manager.js +80 -0
- package/dist/retry.d.ts +11 -0
- package/dist/retry.js +34 -0
- package/dist/safe-json.d.ts +12 -0
- package/dist/safe-json.js +22 -0
- package/dist/screenshot.d.ts +28 -0
- package/dist/screenshot.js +60 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +1973 -0
- package/dist/session-cleanup.d.ts +18 -0
- package/dist/session-cleanup.js +11 -0
- package/dist/session.d.ts +379 -0
- package/dist/session.js +1568 -0
- package/dist/shutdown-utils.d.ts +5 -0
- package/dist/shutdown-utils.js +24 -0
- package/dist/signal-cleanup-helper.d.ts +48 -0
- package/dist/signal-cleanup-helper.js +117 -0
- package/dist/sse-limiter.d.ts +47 -0
- package/dist/sse-limiter.js +61 -0
- package/dist/sse-writer.d.ts +31 -0
- package/dist/sse-writer.js +94 -0
- package/dist/ssrf.d.ts +102 -0
- package/dist/ssrf.js +267 -0
- package/dist/startup.d.ts +6 -0
- package/dist/startup.js +162 -0
- package/dist/suppress.d.ts +33 -0
- package/dist/suppress.js +79 -0
- package/dist/swarm-monitor.d.ts +117 -0
- package/dist/swarm-monitor.js +300 -0
- package/dist/template-store.d.ts +45 -0
- package/dist/template-store.js +142 -0
- package/dist/terminal-parser.d.ts +16 -0
- package/dist/terminal-parser.js +346 -0
- package/dist/tmux-capture-cache.d.ts +18 -0
- package/dist/tmux-capture-cache.js +34 -0
- package/dist/tmux.d.ts +183 -0
- package/dist/tmux.js +906 -0
- package/dist/tool-registry.d.ts +40 -0
- package/dist/tool-registry.js +83 -0
- package/dist/transcript.d.ts +63 -0
- package/dist/transcript.js +284 -0
- package/dist/utils/circular-buffer.d.ts +11 -0
- package/dist/utils/circular-buffer.js +37 -0
- package/dist/utils/redact-headers.d.ts +13 -0
- package/dist/utils/redact-headers.js +54 -0
- package/dist/validation.d.ts +406 -0
- package/dist/validation.js +415 -0
- package/dist/verification.d.ts +2 -0
- package/dist/verification.js +72 -0
- package/dist/worktree-lookup.d.ts +24 -0
- package/dist/worktree-lookup.js +71 -0
- package/dist/ws-terminal.d.ts +32 -0
- package/dist/ws-terminal.js +348 -0
- package/package.json +83 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws-terminal.ts — WebSocket endpoint for live terminal streaming.
|
|
3
|
+
*
|
|
4
|
+
* WS /v1/sessions/:id/terminal
|
|
5
|
+
*
|
|
6
|
+
* Protocol:
|
|
7
|
+
* Server → Client: { type: "pane", content: "..." }
|
|
8
|
+
* Server → Client: { type: "status", status: "idle" }
|
|
9
|
+
* Server → Client: { type: "error", message: "..." }
|
|
10
|
+
* Client → Server: { type: "input", text: "..." }
|
|
11
|
+
* Client → Server: { type: "resize", cols: 80, rows: 24 }
|
|
12
|
+
*
|
|
13
|
+
* Security (Issue #303, #503):
|
|
14
|
+
* - Auth validation via first-message handshake: client sends
|
|
15
|
+
* { type: "auth", token: "..." } as first message (#503)
|
|
16
|
+
* - Bearer header auth still works for non-browser clients
|
|
17
|
+
* - 5s auth timeout — connection dropped if not authenticated
|
|
18
|
+
* - Per-connection message rate limiting (10 msg/sec)
|
|
19
|
+
* - Shared tmux capture polls (one per session, not per connection)
|
|
20
|
+
* - Ping/pong keep-alive with dead connection detection
|
|
21
|
+
*/
|
|
22
|
+
import { clamp, wsInboundMessageSchema, isValidUUID } from './validation.js';
|
|
23
|
+
import { safeJsonParse } from './safe-json.js';
|
|
24
|
+
const POLL_INTERVAL_MS = 500;
|
|
25
|
+
const KEEPALIVE_INTERVAL_TICKS = 60; // 30s at 500ms intervals
|
|
26
|
+
const KEEPALIVE_TIMEOUT_MS = 35_000; // 30s interval + 5s grace
|
|
27
|
+
const RATE_LIMIT_WINDOW_MS = 1000;
|
|
28
|
+
const RATE_LIMIT_MAX_MESSAGES = 10;
|
|
29
|
+
const AUTH_TIMEOUT_MS = 5_000;
|
|
30
|
+
// ── Module state ───────────────────────────────────────────────────
|
|
31
|
+
const sessionPolls = new Map();
|
|
32
|
+
/** Reset all internal state (for testing). */
|
|
33
|
+
export function _resetForTesting() {
|
|
34
|
+
for (const poll of sessionPolls.values()) {
|
|
35
|
+
if (poll.timer)
|
|
36
|
+
clearInterval(poll.timer);
|
|
37
|
+
}
|
|
38
|
+
sessionPolls.clear();
|
|
39
|
+
}
|
|
40
|
+
/** Get the number of active shared polls (for testing). */
|
|
41
|
+
export function _activePollCount() {
|
|
42
|
+
return sessionPolls.size;
|
|
43
|
+
}
|
|
44
|
+
/** Get subscriber count for a session (for testing). */
|
|
45
|
+
export function _subscriberCount(sessionId) {
|
|
46
|
+
return sessionPolls.get(sessionId)?.subscribers.size ?? 0;
|
|
47
|
+
}
|
|
48
|
+
// ── Route registration ─────────────────────────────────────────────
|
|
49
|
+
export function registerWsTerminalRoute(app, sessions, tmux, auth) {
|
|
50
|
+
app.get('/v1/sessions/:id/terminal', {
|
|
51
|
+
websocket: true,
|
|
52
|
+
preHandler: async (req, reply) => {
|
|
53
|
+
if (!auth.authEnabled)
|
|
54
|
+
return;
|
|
55
|
+
// Bearer header auth still works for non-browser clients
|
|
56
|
+
const header = req.headers.authorization;
|
|
57
|
+
if (header?.startsWith('Bearer ')) {
|
|
58
|
+
const token = header.slice(7);
|
|
59
|
+
const result = auth.validate(token);
|
|
60
|
+
if (!result.valid) {
|
|
61
|
+
return reply.status(401).send({ error: 'Unauthorized — invalid API key' });
|
|
62
|
+
}
|
|
63
|
+
if (result.rateLimited) {
|
|
64
|
+
return reply.status(429).send({ error: 'Rate limit exceeded' });
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// No Bearer header — allow connection through; auth will be validated
|
|
69
|
+
// via first-message handshake ({ type: "auth", token: "..." }).
|
|
70
|
+
// Issue #503: tokens must NOT appear in URLs.
|
|
71
|
+
},
|
|
72
|
+
}, (socket, req) => {
|
|
73
|
+
const sessionId = req.params.id;
|
|
74
|
+
// #412: Validate session ID is a UUID before lookup
|
|
75
|
+
if (!isValidUUID(sessionId)) {
|
|
76
|
+
sendError(socket, 'Invalid session ID — must be a UUID');
|
|
77
|
+
socket.close();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Check if already authenticated via Bearer header in preHandler
|
|
81
|
+
const preAuthed = auth.authEnabled && req.headers?.authorization?.startsWith('Bearer ');
|
|
82
|
+
// #1130: When auth is required but not yet provided, do NOT check session
|
|
83
|
+
// existence — that would leak whether a session ID is valid to unauthenticated clients.
|
|
84
|
+
// For pre-authenticated clients (Bearer header) or when auth is disabled, check immediately.
|
|
85
|
+
let session = null;
|
|
86
|
+
const deferSessionCheck = auth.authEnabled && !preAuthed;
|
|
87
|
+
if (!deferSessionCheck) {
|
|
88
|
+
session = sessions.getSession(sessionId);
|
|
89
|
+
if (!session) {
|
|
90
|
+
sendError(socket, 'Session not found');
|
|
91
|
+
socket.close();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Create subscriber
|
|
96
|
+
const subscriber = {
|
|
97
|
+
lastContent: '',
|
|
98
|
+
lastStatus: '',
|
|
99
|
+
closed: false,
|
|
100
|
+
lastPongAt: Date.now(),
|
|
101
|
+
messageTimestamps: [],
|
|
102
|
+
authenticated: !auth.authEnabled || !!preAuthed,
|
|
103
|
+
authTimer: null,
|
|
104
|
+
};
|
|
105
|
+
// If auth is required but not yet provided, set auth timeout
|
|
106
|
+
if (auth.authEnabled && !subscriber.authenticated) {
|
|
107
|
+
subscriber.authTimer = setTimeout(() => {
|
|
108
|
+
if (!subscriber.closed && !subscriber.authenticated) {
|
|
109
|
+
sendError(socket, 'Auth timeout — no auth message received');
|
|
110
|
+
evictSubscriber(sessionId, socket, subscriber);
|
|
111
|
+
}
|
|
112
|
+
}, AUTH_TIMEOUT_MS);
|
|
113
|
+
}
|
|
114
|
+
// Get or create shared session poll (only after session is confirmed to exist)
|
|
115
|
+
if (session) {
|
|
116
|
+
let poll = sessionPolls.get(sessionId);
|
|
117
|
+
if (!poll) {
|
|
118
|
+
poll = {
|
|
119
|
+
timer: null,
|
|
120
|
+
tickCount: 0,
|
|
121
|
+
subscribers: new Map(),
|
|
122
|
+
};
|
|
123
|
+
sessionPolls.set(sessionId, poll);
|
|
124
|
+
// Start the shared poll timer
|
|
125
|
+
poll.timer = setInterval(async () => {
|
|
126
|
+
poll.tickCount++;
|
|
127
|
+
await tickPoll(sessionId, sessions, tmux, poll);
|
|
128
|
+
}, POLL_INTERVAL_MS);
|
|
129
|
+
}
|
|
130
|
+
poll.subscribers.set(socket, subscriber);
|
|
131
|
+
}
|
|
132
|
+
// Handle pong responses for keep-alive
|
|
133
|
+
socket.on('pong', () => {
|
|
134
|
+
const sub = sessionPolls.get(sessionId)?.subscribers.get(socket);
|
|
135
|
+
if (sub)
|
|
136
|
+
sub.lastPongAt = Date.now();
|
|
137
|
+
});
|
|
138
|
+
// Handle incoming messages with rate limiting
|
|
139
|
+
socket.on('message', async (data) => {
|
|
140
|
+
if (subscriber.closed)
|
|
141
|
+
return;
|
|
142
|
+
// Rate limit check
|
|
143
|
+
if (!checkRateLimit(subscriber)) {
|
|
144
|
+
sendError(socket, 'Rate limit exceeded — max 10 messages per second');
|
|
145
|
+
evictSubscriber(sessionId, socket, subscriber);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const jsonParsed = safeJsonParse(data.toString(), 'WebSocket message');
|
|
149
|
+
if (!jsonParsed.ok) {
|
|
150
|
+
sendError(socket, `Invalid message: ${jsonParsed.error}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const parsed = wsInboundMessageSchema.safeParse(jsonParsed.data);
|
|
154
|
+
if (!parsed.success) {
|
|
155
|
+
sendError(socket, `Invalid message: ${parsed.error.issues.map(i => i.message).join(', ')}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const msg = parsed.data;
|
|
159
|
+
try {
|
|
160
|
+
// Handle auth handshake (Issue #503)
|
|
161
|
+
if (msg.type === 'auth') {
|
|
162
|
+
if (subscriber.authenticated) {
|
|
163
|
+
sendError(socket, 'Already authenticated');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (typeof msg.token !== 'string' || !msg.token) {
|
|
167
|
+
sendError(socket, 'Auth message requires a token field');
|
|
168
|
+
evictSubscriber(sessionId, socket, subscriber);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const result = auth.validate(msg.token);
|
|
172
|
+
if (!result.valid) {
|
|
173
|
+
sendError(socket, 'Unauthorized — invalid API key');
|
|
174
|
+
evictSubscriber(sessionId, socket, subscriber);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (result.rateLimited) {
|
|
178
|
+
sendError(socket, 'Rate limit exceeded');
|
|
179
|
+
evictSubscriber(sessionId, socket, subscriber);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Auth successful
|
|
183
|
+
subscriber.authenticated = true;
|
|
184
|
+
if (subscriber.authTimer) {
|
|
185
|
+
clearTimeout(subscriber.authTimer);
|
|
186
|
+
subscriber.authTimer = null;
|
|
187
|
+
}
|
|
188
|
+
// #1130: Now that the client is authenticated, check session existence.
|
|
189
|
+
// This was deferred to avoid leaking valid session IDs to unauthenticated clients.
|
|
190
|
+
const authedSession = sessions.getSession(sessionId);
|
|
191
|
+
if (!authedSession) {
|
|
192
|
+
sendError(socket, 'Session not found');
|
|
193
|
+
evictSubscriber(sessionId, socket, subscriber);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// Register subscriber to the session poll now that session is confirmed
|
|
197
|
+
let authedPoll = sessionPolls.get(sessionId);
|
|
198
|
+
if (!authedPoll) {
|
|
199
|
+
authedPoll = {
|
|
200
|
+
timer: null,
|
|
201
|
+
tickCount: 0,
|
|
202
|
+
subscribers: new Map(),
|
|
203
|
+
};
|
|
204
|
+
sessionPolls.set(sessionId, authedPoll);
|
|
205
|
+
authedPoll.timer = setInterval(async () => {
|
|
206
|
+
authedPoll.tickCount++;
|
|
207
|
+
await tickPoll(sessionId, sessions, tmux, authedPoll);
|
|
208
|
+
}, POLL_INTERVAL_MS);
|
|
209
|
+
}
|
|
210
|
+
authedPoll.subscribers.set(socket, subscriber);
|
|
211
|
+
send(socket, { type: 'status', status: 'authenticated' });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Reject non-auth messages when not yet authenticated
|
|
215
|
+
if (!subscriber.authenticated) {
|
|
216
|
+
sendError(socket, 'Not authenticated — send { type: "auth", token: "..." } first');
|
|
217
|
+
evictSubscriber(sessionId, socket, subscriber);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (msg.type === 'input' && typeof msg.text === 'string') {
|
|
221
|
+
await sessions.sendMessage(sessionId, msg.text);
|
|
222
|
+
}
|
|
223
|
+
else if (msg.type === 'resize') {
|
|
224
|
+
const resizeSession = sessions.getSession(sessionId);
|
|
225
|
+
if (!resizeSession) {
|
|
226
|
+
sendError(socket, 'Session no longer exists');
|
|
227
|
+
evictSubscriber(sessionId, socket, subscriber);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const cols = clamp(msg.cols ?? 80, 10, 500, 80);
|
|
231
|
+
const rows = clamp(msg.rows ?? 24, 5, 200, 24);
|
|
232
|
+
await tmux.resizePane(resizeSession.windowId, cols, rows);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
sendError(socket, `Failed to process message: ${e instanceof Error ? e.message : String(e)}`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
socket.on('close', () => {
|
|
240
|
+
evictSubscriber(sessionId, socket, subscriber);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// ── Shared poll logic ──────────────────────────────────────────────
|
|
245
|
+
async function tickPoll(sessionId, sessions, tmux, poll) {
|
|
246
|
+
const session = sessions.getSession(sessionId);
|
|
247
|
+
if (!session) {
|
|
248
|
+
// Session gone — evict all subscribers
|
|
249
|
+
for (const [socket, sub] of [...poll.subscribers]) {
|
|
250
|
+
if (!sub.closed) {
|
|
251
|
+
sendError(socket, 'Session no longer exists');
|
|
252
|
+
evictSubscriber(sessionId, socket, sub);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
let content;
|
|
258
|
+
try {
|
|
259
|
+
content = await tmux.capturePane(session.windowId);
|
|
260
|
+
}
|
|
261
|
+
catch { /* pane gone — evict all subscribers */
|
|
262
|
+
for (const [socket, sub] of [...poll.subscribers]) {
|
|
263
|
+
if (!sub.closed) {
|
|
264
|
+
sendError(socket, 'Failed to capture pane — session may have ended');
|
|
265
|
+
evictSubscriber(sessionId, socket, sub);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const currentStatus = session.status;
|
|
271
|
+
// Fan out to all subscribers with per-subscriber deduplication
|
|
272
|
+
for (const [socket, sub] of [...poll.subscribers]) {
|
|
273
|
+
if (sub.closed || !sub.authenticated)
|
|
274
|
+
continue;
|
|
275
|
+
if (content !== sub.lastContent) {
|
|
276
|
+
sub.lastContent = content;
|
|
277
|
+
send(socket, { type: 'pane', content });
|
|
278
|
+
}
|
|
279
|
+
if (currentStatus !== sub.lastStatus) {
|
|
280
|
+
sub.lastStatus = currentStatus;
|
|
281
|
+
send(socket, { type: 'status', status: currentStatus });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Keep-alive check (every 60 ticks ≈ 30s)
|
|
285
|
+
if (poll.tickCount % KEEPALIVE_INTERVAL_TICKS === 0) {
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
for (const [socket, sub] of [...poll.subscribers]) {
|
|
288
|
+
if (sub.closed)
|
|
289
|
+
continue;
|
|
290
|
+
// Evict dead connections
|
|
291
|
+
if (now - sub.lastPongAt > KEEPALIVE_TIMEOUT_MS) {
|
|
292
|
+
evictSubscriber(sessionId, socket, sub);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
// Send ping
|
|
296
|
+
try {
|
|
297
|
+
socket.ping();
|
|
298
|
+
}
|
|
299
|
+
catch { /* socket already closed */
|
|
300
|
+
evictSubscriber(sessionId, socket, sub);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// ── Rate limiting ──────────────────────────────────────────────────
|
|
306
|
+
function checkRateLimit(sub) {
|
|
307
|
+
const now = Date.now();
|
|
308
|
+
sub.messageTimestamps = sub.messageTimestamps.filter(t => now - t < RATE_LIMIT_WINDOW_MS);
|
|
309
|
+
if (sub.messageTimestamps.length >= RATE_LIMIT_MAX_MESSAGES) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
sub.messageTimestamps.push(now);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
// ── Subscriber management ──────────────────────────────────────────
|
|
316
|
+
function evictSubscriber(sessionId, socket, sub) {
|
|
317
|
+
if (sub.closed)
|
|
318
|
+
return;
|
|
319
|
+
sub.closed = true;
|
|
320
|
+
// Clean up auth timer if pending
|
|
321
|
+
if (sub.authTimer) {
|
|
322
|
+
clearTimeout(sub.authTimer);
|
|
323
|
+
sub.authTimer = null;
|
|
324
|
+
}
|
|
325
|
+
const poll = sessionPolls.get(sessionId);
|
|
326
|
+
if (poll) {
|
|
327
|
+
poll.subscribers.delete(socket);
|
|
328
|
+
// If no more subscribers, clean up the poll timer
|
|
329
|
+
if (poll.subscribers.size === 0) {
|
|
330
|
+
if (poll.timer)
|
|
331
|
+
clearInterval(poll.timer);
|
|
332
|
+
sessionPolls.delete(sessionId);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
socket.close();
|
|
337
|
+
}
|
|
338
|
+
catch { /* ignore */ }
|
|
339
|
+
}
|
|
340
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
341
|
+
function send(ws, msg) {
|
|
342
|
+
if (ws.readyState === ws.OPEN) {
|
|
343
|
+
ws.send(JSON.stringify(msg));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function sendError(ws, message) {
|
|
347
|
+
send(ws, { type: 'error', message });
|
|
348
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aegis-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"types": "dist/server.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/server.js",
|
|
11
|
+
"types": "./dist/server.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./cli": {
|
|
14
|
+
"import": "./dist/cli.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"aegis-bridge": "dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"dashboard/dist",
|
|
23
|
+
"!dist/__tests__",
|
|
24
|
+
"!dist/**/*.map"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc && npm run build:copy-dashboard",
|
|
28
|
+
"build:copy-dashboard": "node scripts/copy-dashboard.mjs",
|
|
29
|
+
"build:dashboard": "cd dashboard && npm ci && npm run build",
|
|
30
|
+
"docs": "typedoc",
|
|
31
|
+
"start": "node dist/cli.js",
|
|
32
|
+
"dev": "tsc && node dist/cli.js",
|
|
33
|
+
"prepublishOnly": "npm run build:dashboard && npm run build",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:smoke": "node scripts/uat-smoke.mjs",
|
|
36
|
+
"test:fault-harness": "vitest run src/__tests__/fault-injection-harness-901.test.ts"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"claude",
|
|
40
|
+
"claude-code",
|
|
41
|
+
"ai",
|
|
42
|
+
"orchestration",
|
|
43
|
+
"coding-agent",
|
|
44
|
+
"tmux",
|
|
45
|
+
"session-management"
|
|
46
|
+
],
|
|
47
|
+
"author": "Emanuele Santonastaso (@OneStepAt4time)",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@fastify/cors": "^11.2.0",
|
|
51
|
+
"@fastify/static": "^9.0.0",
|
|
52
|
+
"@fastify/websocket": "^11.2.0",
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
54
|
+
"@tanstack/react-virtual": "^3.13.23",
|
|
55
|
+
"async-mutex": "^0.5.0",
|
|
56
|
+
"fastify": "^5.8.2",
|
|
57
|
+
"zod": "^4.3.6"
|
|
58
|
+
},
|
|
59
|
+
"overrides": {
|
|
60
|
+
"zod": "^4.3.6"
|
|
61
|
+
},
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "https://github.com/OneStepAt4time/aegis.git"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://github.com/OneStepAt4time/aegis#readme",
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/OneStepAt4time/aegis/issues"
|
|
69
|
+
},
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=20.0.0"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@types/node": "^20.0.0",
|
|
75
|
+
"@types/ws": "^8.18.1",
|
|
76
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
77
|
+
"lockfile-lint": "5.0.0",
|
|
78
|
+
"ts-morph": "^27.0.2",
|
|
79
|
+
"typedoc": "^0.28.18",
|
|
80
|
+
"typescript": "^6.0.2",
|
|
81
|
+
"vitest": "^4.1.2"
|
|
82
|
+
}
|
|
83
|
+
}
|