aegis-bridge 2.7.0 → 2.8.1

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.
@@ -0,0 +1,15 @@
1
+ export interface ApiErrorEnvelope {
2
+ code: string;
3
+ message: string;
4
+ details?: unknown;
5
+ requestId: string;
6
+ error: string;
7
+ }
8
+ interface NormalizeApiErrorInput {
9
+ payload: unknown;
10
+ statusCode: number;
11
+ requestId: string;
12
+ contentType?: string;
13
+ }
14
+ export declare function normalizeApiErrorPayload(input: NormalizeApiErrorInput): unknown;
15
+ export {};
@@ -0,0 +1,80 @@
1
+ function defaultErrorMessage(statusCode) {
2
+ if (statusCode >= 500)
3
+ return 'Internal server error';
4
+ if (statusCode === 404)
5
+ return 'Not found';
6
+ if (statusCode === 401)
7
+ return 'Unauthorized';
8
+ if (statusCode === 403)
9
+ return 'Forbidden';
10
+ if (statusCode === 429)
11
+ return 'Rate limit exceeded';
12
+ return 'Request failed';
13
+ }
14
+ function mapStatusToCode(statusCode) {
15
+ if (statusCode === 400)
16
+ return 'VALIDATION_ERROR';
17
+ if (statusCode === 401)
18
+ return 'UNAUTHORIZED';
19
+ if (statusCode === 403)
20
+ return 'FORBIDDEN';
21
+ if (statusCode === 404)
22
+ return 'NOT_FOUND';
23
+ if (statusCode === 409)
24
+ return 'CONFLICT';
25
+ if (statusCode === 429)
26
+ return 'RATE_LIMITED';
27
+ if (statusCode === 501)
28
+ return 'NOT_IMPLEMENTED';
29
+ if (statusCode >= 500)
30
+ return 'INTERNAL_ERROR';
31
+ return `HTTP_${statusCode}`;
32
+ }
33
+ function parseJsonObjectString(payload) {
34
+ try {
35
+ const parsed = JSON.parse(payload);
36
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
37
+ return parsed;
38
+ }
39
+ return null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function isJsonContentType(contentType) {
46
+ return typeof contentType === 'string' && contentType.toLowerCase().includes('application/json');
47
+ }
48
+ export function normalizeApiErrorPayload(input) {
49
+ const { payload, statusCode, requestId, contentType } = input;
50
+ if (statusCode < 400)
51
+ return payload;
52
+ if (typeof contentType === 'string' && contentType.includes('text/event-stream'))
53
+ return payload;
54
+ let source = null;
55
+ if (payload && typeof payload === 'object' && !Array.isArray(payload) && !(payload instanceof Buffer)) {
56
+ source = payload;
57
+ }
58
+ else if (typeof payload === 'string' && isJsonContentType(contentType)) {
59
+ source = parseJsonObjectString(payload);
60
+ }
61
+ if (!source)
62
+ return payload;
63
+ const legacyError = typeof source.error === 'string' ? source.error : undefined;
64
+ const sourceMessage = typeof source.message === 'string' ? source.message : undefined;
65
+ const message = sourceMessage ?? legacyError ?? defaultErrorMessage(statusCode);
66
+ const code = typeof source.code === 'string' ? source.code : mapStatusToCode(statusCode);
67
+ const envelope = {
68
+ code,
69
+ message,
70
+ requestId,
71
+ error: legacyError ?? message,
72
+ };
73
+ if (source.details !== undefined) {
74
+ envelope.details = source.details;
75
+ }
76
+ if (typeof payload === 'string') {
77
+ return JSON.stringify(envelope);
78
+ }
79
+ return envelope;
80
+ }
@@ -0,0 +1,27 @@
1
+ interface MemoryEntry {
2
+ value: string;
3
+ namespace: string;
4
+ key: string;
5
+ created_at: number;
6
+ updated_at: number;
7
+ expires_at?: number;
8
+ }
9
+ export declare class MemoryBridge {
10
+ private reaperIntervalMs;
11
+ private store;
12
+ private persistPath;
13
+ private reaperTimer;
14
+ private saveTimer;
15
+ constructor(persistPath?: string | null, reaperIntervalMs?: number);
16
+ set(key: string, value: string, ttlSeconds?: number): MemoryEntry;
17
+ get(key: string): MemoryEntry | null;
18
+ delete(key: string): boolean;
19
+ list(prefix?: string): MemoryEntry[];
20
+ resolveKeys(keys: string[]): Map<string, string>;
21
+ load(): Promise<void>;
22
+ save(): Promise<void>;
23
+ private scheduleSave;
24
+ startReaper(): void;
25
+ stopReaper(): void;
26
+ }
27
+ export {};
@@ -0,0 +1,118 @@
1
+ import { existsSync, renameSync, writeFileSync, readFileSync } from "fs";
2
+ const KEY_REGEX = /^(.+?)\/(.+)$/;
3
+ const MAX_KEY_LEN = 256;
4
+ const MAX_VALUE_SIZE = 100 * 1024; // 100KB
5
+ export class MemoryBridge {
6
+ reaperIntervalMs;
7
+ store = new Map();
8
+ persistPath = null;
9
+ reaperTimer = null;
10
+ saveTimer = null;
11
+ constructor(persistPath = null, reaperIntervalMs = 60_000) {
12
+ this.reaperIntervalMs = reaperIntervalMs;
13
+ this.persistPath = persistPath;
14
+ }
15
+ set(key, value, ttlSeconds) {
16
+ if (value.length > MAX_VALUE_SIZE)
17
+ throw new Error("Value exceeds maximum size");
18
+ const m = KEY_REGEX.exec(key);
19
+ if (!m)
20
+ throw new Error(`Invalid key format: must be namespace/key, got "${key}"`);
21
+ const [, namespace, keyName] = m;
22
+ if (key.length > MAX_KEY_LEN)
23
+ throw new Error("Key exceeds maximum length");
24
+ const now = Date.now();
25
+ const entry = {
26
+ value, namespace, key,
27
+ created_at: this.store.has(key) ? this.store.get(key).created_at : now,
28
+ updated_at: now,
29
+ expires_at: ttlSeconds ? now + ttlSeconds * 1000 : undefined,
30
+ };
31
+ this.store.set(key, entry);
32
+ this.scheduleSave();
33
+ return entry;
34
+ }
35
+ get(key) {
36
+ const entry = this.store.get(key);
37
+ if (!entry)
38
+ return null;
39
+ if (entry.expires_at && Date.now() > entry.expires_at) {
40
+ this.store.delete(key);
41
+ return null;
42
+ }
43
+ return entry;
44
+ }
45
+ delete(key) {
46
+ const deleted = this.store.delete(key);
47
+ if (deleted)
48
+ this.scheduleSave();
49
+ return deleted;
50
+ }
51
+ list(prefix) {
52
+ const now = Date.now();
53
+ const entries = [...this.store.values()].filter(e => !e.expires_at || now <= e.expires_at);
54
+ if (!prefix)
55
+ return entries;
56
+ return entries.filter(e => e.key.startsWith(prefix));
57
+ }
58
+ resolveKeys(keys) {
59
+ const result = new Map();
60
+ for (const k of keys) {
61
+ const e = this.get(k);
62
+ if (e)
63
+ result.set(k, e.value);
64
+ }
65
+ return result;
66
+ }
67
+ async load() {
68
+ if (!this.persistPath || !existsSync(this.persistPath))
69
+ return;
70
+ try {
71
+ const raw = JSON.parse(readFileSync(this.persistPath, "utf-8"));
72
+ if (Array.isArray(raw)) {
73
+ for (const e of raw) {
74
+ if (e.key && e.value)
75
+ this.store.set(e.key, e);
76
+ }
77
+ }
78
+ }
79
+ catch { /* ignore corrupt file */ }
80
+ }
81
+ async save() {
82
+ if (!this.persistPath)
83
+ return;
84
+ const entries = [...this.store.values()];
85
+ const tmp = this.persistPath + ".tmp";
86
+ writeFileSync(tmp, JSON.stringify(entries, null, 2));
87
+ renameSync(tmp, this.persistPath);
88
+ }
89
+ scheduleSave() {
90
+ if (this.saveTimer)
91
+ return;
92
+ this.saveTimer = setTimeout(async () => {
93
+ this.saveTimer = null;
94
+ await this.save();
95
+ }, 1000);
96
+ }
97
+ startReaper() {
98
+ if (this.reaperTimer)
99
+ return;
100
+ this.reaperTimer = setInterval(() => {
101
+ const now = Date.now();
102
+ for (const [k, e] of this.store) {
103
+ if (e.expires_at && now > e.expires_at)
104
+ this.store.delete(k);
105
+ }
106
+ }, this.reaperIntervalMs);
107
+ }
108
+ stopReaper() {
109
+ if (this.reaperTimer) {
110
+ clearInterval(this.reaperTimer);
111
+ this.reaperTimer = null;
112
+ }
113
+ if (this.saveTimer) {
114
+ clearTimeout(this.saveTimer);
115
+ this.saveTimer = null;
116
+ }
117
+ }
118
+ }
package/dist/server.js CHANGED
@@ -41,6 +41,7 @@ import { execFileSync } from 'node:child_process';
41
41
  import { negotiate } from './handshake.js';
42
42
  import { diagnosticsBus } from './diagnostics.js';
43
43
  import { setStructuredLogSink } from './logger.js';
44
+ import { normalizeApiErrorPayload } from './api-error-envelope.js';
44
45
  import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, handshakeRequestSchema, parseIntSafe, isValidUUID, compareSemver, extractCCVersion, MIN_CC_VERSION, } from './validation.js';
45
46
  const __filename = fileURLToPath(import.meta.url);
46
47
  const __dirname = path.dirname(__filename);
@@ -129,7 +130,8 @@ app.addHook('onSend', (req, reply, payload, done) => {
129
130
  reply.header('X-Frame-Options', 'DENY');
130
131
  reply.header('Referrer-Policy', 'strict-origin-when-cross-origin');
131
132
  reply.header('Permissions-Policy', 'camera=(), microphone=()');
132
- done();
133
+ const normalizedPayload = normalizeApiErrorPayload({ payload, statusCode: reply.statusCode, requestId: req.id, contentType: typeof contentType === "string" ? contentType : undefined });
134
+ done(null, normalizedPayload);
133
135
  });
134
136
  const ipRateLimits = new Map();
135
137
  const IP_WINDOW_MS = 60_000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.7.0",
3
+ "version": "2.8.1",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",