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.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +404 -0
  3. package/dashboard/dist/assets/index-BoZwGLAx.css +32 -0
  4. package/dashboard/dist/assets/index-C61BkKH-.js +312 -0
  5. package/dashboard/dist/assets/index-C61BkKH-.js.map +1 -0
  6. package/dashboard/dist/index.html +14 -0
  7. package/dist/api-contracts.d.ts +229 -0
  8. package/dist/api-contracts.js +7 -0
  9. package/dist/api-contracts.typecheck.d.ts +14 -0
  10. package/dist/api-contracts.typecheck.js +1 -0
  11. package/dist/api-error-envelope.d.ts +15 -0
  12. package/dist/api-error-envelope.js +80 -0
  13. package/dist/auth.d.ts +87 -0
  14. package/dist/auth.js +276 -0
  15. package/dist/channels/index.d.ts +8 -0
  16. package/dist/channels/index.js +8 -0
  17. package/dist/channels/manager.d.ts +47 -0
  18. package/dist/channels/manager.js +115 -0
  19. package/dist/channels/telegram-style.d.ts +118 -0
  20. package/dist/channels/telegram-style.js +202 -0
  21. package/dist/channels/telegram.d.ts +91 -0
  22. package/dist/channels/telegram.js +1518 -0
  23. package/dist/channels/types.d.ts +77 -0
  24. package/dist/channels/types.js +8 -0
  25. package/dist/channels/webhook.d.ts +60 -0
  26. package/dist/channels/webhook.js +216 -0
  27. package/dist/cli.d.ts +8 -0
  28. package/dist/cli.js +252 -0
  29. package/dist/config.d.ts +90 -0
  30. package/dist/config.js +214 -0
  31. package/dist/consensus.d.ts +16 -0
  32. package/dist/consensus.js +19 -0
  33. package/dist/continuation-pointer.d.ts +11 -0
  34. package/dist/continuation-pointer.js +65 -0
  35. package/dist/diagnostics.d.ts +27 -0
  36. package/dist/diagnostics.js +95 -0
  37. package/dist/error-categories.d.ts +39 -0
  38. package/dist/error-categories.js +73 -0
  39. package/dist/events.d.ts +133 -0
  40. package/dist/events.js +389 -0
  41. package/dist/fault-injection.d.ts +29 -0
  42. package/dist/fault-injection.js +115 -0
  43. package/dist/file-utils.d.ts +2 -0
  44. package/dist/file-utils.js +37 -0
  45. package/dist/handshake.d.ts +60 -0
  46. package/dist/handshake.js +124 -0
  47. package/dist/hook-settings.d.ts +80 -0
  48. package/dist/hook-settings.js +272 -0
  49. package/dist/hook.d.ts +19 -0
  50. package/dist/hook.js +231 -0
  51. package/dist/hooks.d.ts +32 -0
  52. package/dist/hooks.js +364 -0
  53. package/dist/jsonl-watcher.d.ts +59 -0
  54. package/dist/jsonl-watcher.js +166 -0
  55. package/dist/logger.d.ts +35 -0
  56. package/dist/logger.js +65 -0
  57. package/dist/mcp-server.d.ts +123 -0
  58. package/dist/mcp-server.js +869 -0
  59. package/dist/memory-bridge.d.ts +27 -0
  60. package/dist/memory-bridge.js +137 -0
  61. package/dist/memory-routes.d.ts +3 -0
  62. package/dist/memory-routes.js +100 -0
  63. package/dist/metrics.d.ts +126 -0
  64. package/dist/metrics.js +286 -0
  65. package/dist/model-router.d.ts +53 -0
  66. package/dist/model-router.js +150 -0
  67. package/dist/monitor.d.ts +103 -0
  68. package/dist/monitor.js +820 -0
  69. package/dist/path-utils.d.ts +11 -0
  70. package/dist/path-utils.js +21 -0
  71. package/dist/permission-evaluator.d.ts +10 -0
  72. package/dist/permission-evaluator.js +48 -0
  73. package/dist/permission-guard.d.ts +51 -0
  74. package/dist/permission-guard.js +196 -0
  75. package/dist/permission-request-manager.d.ts +12 -0
  76. package/dist/permission-request-manager.js +36 -0
  77. package/dist/permission-routes.d.ts +7 -0
  78. package/dist/permission-routes.js +28 -0
  79. package/dist/pipeline.d.ts +97 -0
  80. package/dist/pipeline.js +291 -0
  81. package/dist/process-utils.d.ts +4 -0
  82. package/dist/process-utils.js +73 -0
  83. package/dist/question-manager.d.ts +54 -0
  84. package/dist/question-manager.js +80 -0
  85. package/dist/retry.d.ts +11 -0
  86. package/dist/retry.js +34 -0
  87. package/dist/safe-json.d.ts +12 -0
  88. package/dist/safe-json.js +22 -0
  89. package/dist/screenshot.d.ts +28 -0
  90. package/dist/screenshot.js +60 -0
  91. package/dist/server.d.ts +10 -0
  92. package/dist/server.js +1973 -0
  93. package/dist/session-cleanup.d.ts +18 -0
  94. package/dist/session-cleanup.js +11 -0
  95. package/dist/session.d.ts +379 -0
  96. package/dist/session.js +1568 -0
  97. package/dist/shutdown-utils.d.ts +5 -0
  98. package/dist/shutdown-utils.js +24 -0
  99. package/dist/signal-cleanup-helper.d.ts +48 -0
  100. package/dist/signal-cleanup-helper.js +117 -0
  101. package/dist/sse-limiter.d.ts +47 -0
  102. package/dist/sse-limiter.js +61 -0
  103. package/dist/sse-writer.d.ts +31 -0
  104. package/dist/sse-writer.js +94 -0
  105. package/dist/ssrf.d.ts +102 -0
  106. package/dist/ssrf.js +267 -0
  107. package/dist/startup.d.ts +6 -0
  108. package/dist/startup.js +162 -0
  109. package/dist/suppress.d.ts +33 -0
  110. package/dist/suppress.js +79 -0
  111. package/dist/swarm-monitor.d.ts +117 -0
  112. package/dist/swarm-monitor.js +300 -0
  113. package/dist/template-store.d.ts +45 -0
  114. package/dist/template-store.js +142 -0
  115. package/dist/terminal-parser.d.ts +16 -0
  116. package/dist/terminal-parser.js +346 -0
  117. package/dist/tmux-capture-cache.d.ts +18 -0
  118. package/dist/tmux-capture-cache.js +34 -0
  119. package/dist/tmux.d.ts +183 -0
  120. package/dist/tmux.js +906 -0
  121. package/dist/tool-registry.d.ts +40 -0
  122. package/dist/tool-registry.js +83 -0
  123. package/dist/transcript.d.ts +63 -0
  124. package/dist/transcript.js +284 -0
  125. package/dist/utils/circular-buffer.d.ts +11 -0
  126. package/dist/utils/circular-buffer.js +37 -0
  127. package/dist/utils/redact-headers.d.ts +13 -0
  128. package/dist/utils/redact-headers.js +54 -0
  129. package/dist/validation.d.ts +406 -0
  130. package/dist/validation.js +415 -0
  131. package/dist/verification.d.ts +2 -0
  132. package/dist/verification.js +72 -0
  133. package/dist/worktree-lookup.d.ts +24 -0
  134. package/dist/worktree-lookup.js +71 -0
  135. package/dist/ws-terminal.d.ts +32 -0
  136. package/dist/ws-terminal.js +348 -0
  137. 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
+ }