@mhingston5/conduit 1.0.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 (87) hide show
  1. package/.env.example +13 -0
  2. package/.github/workflows/ci.yml +88 -0
  3. package/.github/workflows/pr-checks.yml +90 -0
  4. package/.tool-versions +2 -0
  5. package/README.md +177 -0
  6. package/conduit.yaml.test +3 -0
  7. package/docs/ARCHITECTURE.md +35 -0
  8. package/docs/CODE_MODE.md +33 -0
  9. package/docs/SECURITY.md +52 -0
  10. package/logo.png +0 -0
  11. package/package.json +74 -0
  12. package/src/assets/deno-shim.ts +93 -0
  13. package/src/assets/python-shim.py +21 -0
  14. package/src/core/asset.utils.ts +42 -0
  15. package/src/core/concurrency.service.ts +70 -0
  16. package/src/core/config.service.ts +147 -0
  17. package/src/core/execution.context.ts +37 -0
  18. package/src/core/execution.service.ts +209 -0
  19. package/src/core/interfaces/app.config.ts +17 -0
  20. package/src/core/interfaces/executor.interface.ts +31 -0
  21. package/src/core/interfaces/middleware.interface.ts +12 -0
  22. package/src/core/interfaces/url.validator.interface.ts +3 -0
  23. package/src/core/logger.ts +64 -0
  24. package/src/core/metrics.service.ts +112 -0
  25. package/src/core/middleware/auth.middleware.ts +56 -0
  26. package/src/core/middleware/error.middleware.ts +21 -0
  27. package/src/core/middleware/logging.middleware.ts +25 -0
  28. package/src/core/middleware/middleware.builder.ts +24 -0
  29. package/src/core/middleware/ratelimit.middleware.ts +31 -0
  30. package/src/core/network.policy.service.ts +106 -0
  31. package/src/core/ops.server.ts +74 -0
  32. package/src/core/otel.service.ts +41 -0
  33. package/src/core/policy.service.ts +77 -0
  34. package/src/core/registries/executor.registry.ts +26 -0
  35. package/src/core/request.controller.ts +297 -0
  36. package/src/core/security.service.ts +68 -0
  37. package/src/core/session.manager.ts +44 -0
  38. package/src/core/types.ts +47 -0
  39. package/src/executors/deno.executor.ts +342 -0
  40. package/src/executors/isolate.executor.ts +281 -0
  41. package/src/executors/pyodide.executor.ts +327 -0
  42. package/src/executors/pyodide.worker.ts +195 -0
  43. package/src/gateway/auth.service.ts +104 -0
  44. package/src/gateway/gateway.service.ts +345 -0
  45. package/src/gateway/schema.cache.ts +46 -0
  46. package/src/gateway/upstream.client.ts +244 -0
  47. package/src/index.ts +92 -0
  48. package/src/sdk/index.ts +2 -0
  49. package/src/sdk/sdk-generator.ts +245 -0
  50. package/src/sdk/tool-binding.ts +86 -0
  51. package/src/transport/socket.transport.ts +203 -0
  52. package/tests/__snapshots__/assets.test.ts.snap +97 -0
  53. package/tests/assets.test.ts +50 -0
  54. package/tests/auth.service.test.ts +78 -0
  55. package/tests/code-mode-lite-execution.test.ts +84 -0
  56. package/tests/code-mode-lite-gateway.test.ts +150 -0
  57. package/tests/concurrency.service.test.ts +50 -0
  58. package/tests/concurrency.test.ts +41 -0
  59. package/tests/config.service.test.ts +70 -0
  60. package/tests/contract.test.ts +43 -0
  61. package/tests/deno.executor.test.ts +68 -0
  62. package/tests/deno_hardening.test.ts +45 -0
  63. package/tests/dynamic.tool.test.ts +237 -0
  64. package/tests/e2e_stdio_upstream.test.ts +197 -0
  65. package/tests/fixtures/stdio-server.ts +42 -0
  66. package/tests/gateway.manifest.test.ts +82 -0
  67. package/tests/gateway.service.test.ts +58 -0
  68. package/tests/gateway.strict.unit.test.ts +74 -0
  69. package/tests/gateway.validation.unit.test.ts +89 -0
  70. package/tests/gateway_validation.test.ts +86 -0
  71. package/tests/hardening.test.ts +139 -0
  72. package/tests/hardening_v1.test.ts +72 -0
  73. package/tests/isolate.executor.test.ts +100 -0
  74. package/tests/log-limit.test.ts +55 -0
  75. package/tests/middleware.test.ts +106 -0
  76. package/tests/ops.server.test.ts +65 -0
  77. package/tests/policy.service.test.ts +90 -0
  78. package/tests/pyodide.executor.test.ts +101 -0
  79. package/tests/reference_mcp.ts +40 -0
  80. package/tests/remediation.test.ts +119 -0
  81. package/tests/routing.test.ts +148 -0
  82. package/tests/schema.cache.test.ts +27 -0
  83. package/tests/sdk/sdk-generator.test.ts +205 -0
  84. package/tests/socket.transport.test.ts +182 -0
  85. package/tests/stdio_upstream.test.ts +54 -0
  86. package/tsconfig.json +25 -0
  87. package/tsup.config.ts +22 -0
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { ConfigService } from './core/config.service.js';
2
+ import { createLogger, loggerStorage } from './core/logger.js';
3
+ import { SocketTransport } from './transport/socket.transport.js';
4
+ import { OpsServer } from './core/ops.server.js';
5
+ import { ConcurrencyService } from './core/concurrency.service.js';
6
+ import { RequestController } from './core/request.controller.js';
7
+ import { GatewayService } from './gateway/gateway.service.js';
8
+ import { SecurityService } from './core/security.service.js';
9
+ import { OtelService } from './core/otel.service.js';
10
+ import { DenoExecutor } from './executors/deno.executor.js';
11
+ import { PyodideExecutor } from './executors/pyodide.executor.js';
12
+ import { IsolateExecutor } from './executors/isolate.executor.js';
13
+ import { ExecutorRegistry } from './core/registries/executor.registry.js';
14
+ import { ExecutionService } from './core/execution.service.js';
15
+ import { buildDefaultMiddleware } from './core/middleware/middleware.builder.js';
16
+ async function main() {
17
+ const configService = new ConfigService();
18
+ const logger = createLogger(configService);
19
+
20
+ const otelService = new OtelService(logger);
21
+ await otelService.start();
22
+
23
+ await loggerStorage.run({ correlationId: 'system' }, async () => {
24
+ const securityService = new SecurityService(logger, configService.get('ipcBearerToken'));
25
+
26
+ const gatewayService = new GatewayService(logger, securityService);
27
+ const upstreams = configService.get('upstreams') || [];
28
+ for (const upstream of upstreams) {
29
+ gatewayService.registerUpstream(upstream);
30
+ }
31
+
32
+ const executorRegistry = new ExecutorRegistry();
33
+ executorRegistry.register('deno', new DenoExecutor(configService.get('denoMaxPoolSize')));
34
+ executorRegistry.register('python', new PyodideExecutor(configService.get('pyodideMaxPoolSize')));
35
+
36
+ // IsolateExecutor needs gatewayService
37
+ const isolateExecutor = new IsolateExecutor(logger, gatewayService);
38
+ executorRegistry.register('isolate', isolateExecutor);
39
+
40
+ const executionService = new ExecutionService(
41
+ logger,
42
+ configService.get('resourceLimits'),
43
+ gatewayService,
44
+ securityService,
45
+ executorRegistry
46
+ );
47
+
48
+ const requestController = new RequestController(
49
+ logger,
50
+ executionService,
51
+ gatewayService,
52
+ buildDefaultMiddleware(securityService)
53
+ );
54
+
55
+ const opsServer = new OpsServer(logger, configService.all, gatewayService, requestController);
56
+ await opsServer.listen();
57
+
58
+ const concurrencyService = new ConcurrencyService(logger, {
59
+ maxConcurrent: configService.get('maxConcurrent')
60
+ });
61
+
62
+ const transport = new SocketTransport(logger, requestController, concurrencyService);
63
+ const port = configService.get('port');
64
+ const address = await transport.listen({ port });
65
+ executionService.ipcAddress = address; // Update IPC address on ExecutionService instead of RequestController
66
+
67
+ // Pre-warm workers
68
+ await requestController.warmup();
69
+
70
+ logger.info('Conduit server started');
71
+
72
+ // Handle graceful shutdown
73
+ const shutdown = async () => {
74
+ logger.info('Shutting down...');
75
+ await Promise.all([
76
+ transport.close(),
77
+ opsServer.close(),
78
+ requestController.shutdown(),
79
+ otelService.shutdown(),
80
+ ]);
81
+ process.exit(0);
82
+ };
83
+
84
+ process.on('SIGINT', shutdown);
85
+ process.on('SIGTERM', shutdown);
86
+ });
87
+ }
88
+
89
+ main().catch((err) => {
90
+ console.error('Failed to start Conduit:', err);
91
+ process.exit(1);
92
+ });
@@ -0,0 +1,2 @@
1
+ export { ToolBinding, SDKGeneratorOptions, toToolBinding, groupByNamespace } from './tool-binding.js';
2
+ export { SDKGenerator } from './sdk-generator.js';
@@ -0,0 +1,245 @@
1
+ import { ToolBinding, groupByNamespace } from './tool-binding.js';
2
+
3
+ /**
4
+ * Generates in-memory SDK code from discovered tool bindings.
5
+ * The generated code creates a nested object structure where
6
+ * tools.namespace.method(args) => callTool("namespace__method", args)
7
+ */
8
+ export class SDKGenerator {
9
+ /**
10
+ * Convert camelCase to snake_case for Python
11
+ */
12
+ private toSnakeCase(str: string): string {
13
+ return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
14
+ }
15
+
16
+ /**
17
+ * Escape a string for use in generated code
18
+ */
19
+ private escapeString(str: string): string {
20
+ return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
21
+ }
22
+
23
+ /**
24
+ * Generate TypeScript SDK code to be injected into Deno sandbox.
25
+ * Creates: tools.namespace.method(args) => __internalCallTool("namespace__method", args)
26
+ * @param bindings Tool bindings to generate SDK for
27
+ * @param allowedTools Optional allowlist for $raw() enforcement
28
+ * @param enableRawFallback Enable $raw() escape hatch (default: true)
29
+ */
30
+ generateTypeScript(bindings: ToolBinding[], allowedTools?: string[], enableRawFallback = true): string {
31
+ const grouped = groupByNamespace(bindings);
32
+ const lines: string[] = [];
33
+
34
+ lines.push('// Generated SDK - Do not edit');
35
+
36
+ // Inject allowlist for SDK-level enforcement
37
+ if (allowedTools && allowedTools.length > 0) {
38
+ const normalizedList = allowedTools.map(t => t.replace(/\./g, '__'));
39
+ lines.push(`const __allowedTools = ${JSON.stringify(normalizedList)};`);
40
+ } else {
41
+ lines.push('const __allowedTools = null;');
42
+ }
43
+
44
+ lines.push('const tools = {');
45
+
46
+ for (const [namespace, tools] of grouped.entries()) {
47
+ // Validate namespace is a valid identifier
48
+ const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
49
+
50
+ if (this.isValidIdentifier(namespace)) {
51
+ lines.push(` ${namespace}: {`);
52
+ } else {
53
+ lines.push(` "${this.escapeString(namespace)}": {`);
54
+ }
55
+
56
+ for (const tool of tools) {
57
+ const methodName = this.isValidIdentifier(tool.methodName)
58
+ ? tool.methodName
59
+ : `["${this.escapeString(tool.methodName)}"]`;
60
+
61
+ // Add JSDoc if description available
62
+ if (tool.description) {
63
+ lines.push(` /** ${this.escapeString(tool.description)} */`);
64
+ }
65
+
66
+ if (this.isValidIdentifier(tool.methodName)) {
67
+ lines.push(` async ${tool.methodName}(args) {`);
68
+ } else {
69
+ lines.push(` "${this.escapeString(tool.methodName)}": async function(args) {`);
70
+ }
71
+ lines.push(` return await __internalCallTool("${this.escapeString(tool.name)}", args);`);
72
+ lines.push(` },`);
73
+ }
74
+
75
+ lines.push(` },`);
76
+ }
77
+
78
+ // Add $raw escape hatch with allowlist validation
79
+ if (enableRawFallback) {
80
+ lines.push(` /** Call a tool by its full name (escape hatch for dynamic/unknown tools) */`);
81
+ lines.push(` async $raw(name, args) {`);
82
+ lines.push(` const normalized = name.replace(/\\./g, '__');`);
83
+ lines.push(` if (__allowedTools) {`);
84
+ lines.push(` const allowed = __allowedTools.some(p => {`);
85
+ lines.push(` if (p.endsWith('__*')) return normalized.startsWith(p.slice(0, -1));`);
86
+ lines.push(` return normalized === p;`);
87
+ lines.push(` });`);
88
+ lines.push(` if (!allowed) throw new Error(\`Tool \${name} is not in the allowlist\`);`);
89
+ lines.push(` }`);
90
+ lines.push(` return await __internalCallTool(normalized, args);`);
91
+ lines.push(` },`);
92
+ }
93
+
94
+ lines.push('};');
95
+ lines.push('(globalThis as any).tools = tools;');
96
+
97
+ return lines.join('\n');
98
+ }
99
+
100
+ /**
101
+ * Generate Python SDK code to be injected into Pyodide sandbox.
102
+ * Creates: tools.namespace.method(args) => _internal_call_tool("namespace__method", args)
103
+ * @param bindings Tool bindings to generate SDK for
104
+ * @param allowedTools Optional allowlist for raw() enforcement
105
+ * @param enableRawFallback Enable raw() escape hatch (default: true)
106
+ */
107
+ generatePython(bindings: ToolBinding[], allowedTools?: string[], enableRawFallback = true): string {
108
+ const grouped = groupByNamespace(bindings);
109
+ const lines: string[] = [];
110
+
111
+ lines.push('# Generated SDK - Do not edit');
112
+
113
+ // Inject allowlist for SDK-level enforcement
114
+ if (allowedTools && allowedTools.length > 0) {
115
+ const normalizedList = allowedTools.map(t => t.replace(/\./g, '__'));
116
+ lines.push(`_allowed_tools = ${JSON.stringify(normalizedList)}`);
117
+ } else {
118
+ lines.push('_allowed_tools = None');
119
+ }
120
+
121
+ lines.push('');
122
+ lines.push('class _ToolNamespace:');
123
+ lines.push(' def __init__(self, methods):');
124
+ lines.push(' for name, fn in methods.items():');
125
+ lines.push(' setattr(self, name, fn)');
126
+ lines.push('');
127
+ lines.push('class _Tools:');
128
+ lines.push(' def __init__(self):');
129
+
130
+ for (const [namespace, tools] of grouped.entries()) {
131
+ const safeNamespace = this.toSnakeCase(namespace);
132
+ const methodsDict: string[] = [];
133
+
134
+ for (const tool of tools) {
135
+ const methodName = this.toSnakeCase(tool.methodName);
136
+ const fullName = tool.name;
137
+ // Use async lambda - Python doesn't have async lambdas natively,
138
+ // so we define methods that return awaitable coroutines
139
+ methodsDict.push(` "${methodName}": lambda args, n="${this.escapeString(fullName)}": _internal_call_tool(n, args)`);
140
+ }
141
+
142
+ lines.push(` self.${safeNamespace} = _ToolNamespace({`);
143
+ lines.push(methodsDict.join(',\n'));
144
+ lines.push(` })`);
145
+ }
146
+
147
+ // Add raw escape hatch with allowlist validation
148
+ if (enableRawFallback) {
149
+ lines.push('');
150
+ lines.push(' async def raw(self, name, args):');
151
+ lines.push(' """Call a tool by its full name (escape hatch for dynamic/unknown tools)"""');
152
+ lines.push(' normalized = name.replace(".", "__")');
153
+ lines.push(' if _allowed_tools is not None:');
154
+ lines.push(' allowed = any(');
155
+ lines.push(' normalized.startswith(p[:-1]) if p.endswith("__*") else normalized == p');
156
+ lines.push(' for p in _allowed_tools');
157
+ lines.push(' )');
158
+ lines.push(' if not allowed:');
159
+ lines.push(' raise PermissionError(f"Tool {name} is not in the allowlist")');
160
+ lines.push(' return await _internal_call_tool(normalized, args)');
161
+ }
162
+
163
+ lines.push('');
164
+ lines.push('tools = _Tools()');
165
+
166
+ return lines.join('\n');
167
+ }
168
+
169
+ /**
170
+ * Generate JavaScript SDK code for isolated-vm (V8 Isolate).
171
+ * Creates: tools.namespace.method(args) => __callToolSync("namespace__method", JSON.stringify(args))
172
+ * @param bindings Tool bindings to generate SDK for
173
+ * @param allowedTools Optional allowlist for $raw() enforcement
174
+ * @param enableRawFallback Enable $raw() escape hatch (default: true)
175
+ */
176
+ generateIsolateSDK(bindings: ToolBinding[], allowedTools?: string[], enableRawFallback = true): string {
177
+ const grouped = groupByNamespace(bindings);
178
+ const lines: string[] = [];
179
+
180
+ lines.push('// Generated SDK for isolated-vm');
181
+
182
+ // Inject allowlist for SDK-level enforcement (optional, as Gateway also enforces)
183
+ if (allowedTools && allowedTools.length > 0) {
184
+ const normalizedList = allowedTools.map(t => t.replace(/\./g, '__'));
185
+ lines.push(`const __allowedTools = ${JSON.stringify(normalizedList)};`);
186
+ } else {
187
+ lines.push('const __allowedTools = null;');
188
+ }
189
+
190
+ lines.push('const tools = {');
191
+
192
+ for (const [namespace, tools] of grouped.entries()) {
193
+ const safeNamespace = this.isValidIdentifier(namespace) ? namespace : `["${this.escapeString(namespace)}"]`;
194
+
195
+ if (this.isValidIdentifier(namespace)) {
196
+ lines.push(` ${namespace}: {`);
197
+ } else {
198
+ lines.push(` "${this.escapeString(namespace)}": {`);
199
+ }
200
+
201
+ for (const tool of tools) {
202
+ const methodName = this.isValidIdentifier(tool.methodName) ? tool.methodName : `["${this.escapeString(tool.methodName)}"]`;
203
+
204
+ if (this.isValidIdentifier(tool.methodName)) {
205
+ lines.push(` async ${methodName}(args) {`);
206
+ } else {
207
+ lines.push(` "${this.escapeString(tool.methodName)}": async function(args) {`);
208
+ }
209
+
210
+ lines.push(` const resStr = await __callTool("${this.escapeString(tool.name)}", JSON.stringify(args || {}));`);
211
+ lines.push(` return JSON.parse(resStr);`);
212
+ lines.push(` },`);
213
+ }
214
+
215
+ lines.push(` },`);
216
+ }
217
+
218
+ // Add $raw escape hatch
219
+ if (enableRawFallback) {
220
+ lines.push(` async $raw(name, args) {`);
221
+ lines.push(` const normalized = name.replace(/\\./g, '__');`);
222
+ lines.push(` if (__allowedTools) {`);
223
+ lines.push(` const allowed = __allowedTools.some(p => {`);
224
+ lines.push(` if (p.endsWith('__*')) return normalized.startsWith(p.slice(0, -1));`);
225
+ lines.push(` return normalized === p;`);
226
+ lines.push(` });`);
227
+ lines.push(` if (!allowed) throw new Error(\`Tool \${name} is not in the allowlist\`);`);
228
+ lines.push(` }`);
229
+ lines.push(` const resStr = await __callTool(normalized, JSON.stringify(args || {}));`);
230
+ lines.push(` return JSON.parse(resStr);`);
231
+ lines.push(` },`);
232
+ }
233
+
234
+ lines.push('};');
235
+
236
+ return lines.join('\n');
237
+ }
238
+
239
+ /**
240
+ * Check if a string is a valid JavaScript/Python identifier
241
+ */
242
+ private isValidIdentifier(str: string): boolean {
243
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
244
+ }
245
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Language-agnostic interface for tool bindings.
3
+ * Used to generate typed SDK code for sandbox injection.
4
+ */
5
+ export interface ToolBinding {
6
+ /** Full qualified name: "github__createIssue" */
7
+ name: string;
8
+ /** Upstream ID / namespace: "github" */
9
+ namespace: string;
10
+ /** Tool method name: "createIssue" */
11
+ methodName: string;
12
+ /** JSON Schema for input validation */
13
+ inputSchema?: object;
14
+ /** Human-readable description */
15
+ description?: string;
16
+ }
17
+
18
+ export interface SDKGeneratorOptions {
19
+ /** Tool bindings to include in SDK */
20
+ tools: ToolBinding[];
21
+ /** Allow $raw escape hatch for dynamic calls (default: true) */
22
+ enableRawFallback?: boolean;
23
+ }
24
+
25
+ // Inline parsing to avoid circular dependency with PolicyService
26
+ function parseToolName(qualifiedName: string): { namespace: string; name: string } {
27
+ const separatorIndex = qualifiedName.indexOf('__');
28
+ if (separatorIndex === -1) {
29
+ return { namespace: '', name: qualifiedName };
30
+ }
31
+ return {
32
+ namespace: qualifiedName.substring(0, separatorIndex),
33
+ name: qualifiedName.substring(separatorIndex + 2)
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Convert a prefixed tool name to a ToolBinding.
39
+ * @param name Full tool name in format "namespace__methodName"
40
+ * @param inputSchema Optional JSON Schema
41
+ * @param description Optional description
42
+ */
43
+ export function toToolBinding(
44
+ name: string,
45
+ inputSchema?: object,
46
+ description?: string
47
+ ): ToolBinding {
48
+ const toolId = parseToolName(name);
49
+
50
+ return {
51
+ name,
52
+ namespace: toolId.namespace || 'default',
53
+ methodName: toolId.name || name,
54
+ inputSchema,
55
+ description,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Convert a ToolStub to a ToolBinding.
61
+ * @param stub ToolStub from GatewayService
62
+ */
63
+ export function fromToolStub(stub: { id: string; name: string; description?: string }): ToolBinding {
64
+ const toolId = parseToolName(stub.id);
65
+ return {
66
+ name: stub.id,
67
+ namespace: toolId.namespace || 'default',
68
+ methodName: stub.name, // stub.name is already the method name (e.g. "create_issue")
69
+ description: stub.description,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Group tool bindings by namespace for SDK generation.
75
+ */
76
+ export function groupByNamespace(bindings: ToolBinding[]): Map<string, ToolBinding[]> {
77
+ const groups = new Map<string, ToolBinding[]>();
78
+
79
+ for (const binding of bindings) {
80
+ const existing = groups.get(binding.namespace) || [];
81
+ existing.push(binding);
82
+ groups.set(binding.namespace, existing);
83
+ }
84
+
85
+ return groups;
86
+ }
@@ -0,0 +1,203 @@
1
+ import net from 'node:net';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { Logger } from 'pino';
5
+ import { RequestController } from '../core/request.controller.js';
6
+ import { JSONRPCRequest, ConduitError } from '../core/types.js';
7
+ import { ExecutionContext } from '../core/execution.context.js';
8
+ import { ConcurrencyService } from '../core/concurrency.service.js';
9
+ import { loggerStorage } from '../core/logger.js';
10
+
11
+ export interface TransportOptions {
12
+ path?: string; // For Unix Socket or Named Pipe
13
+ port?: number; // For TCP (development)
14
+ host?: string;
15
+ }
16
+
17
+ export class SocketTransport {
18
+ private server: net.Server;
19
+ private logger: Logger;
20
+ private requestController: RequestController;
21
+ private concurrencyService: ConcurrencyService;
22
+
23
+ constructor(
24
+ logger: Logger,
25
+ requestController: RequestController,
26
+ concurrencyService: ConcurrencyService
27
+ ) {
28
+ this.logger = logger;
29
+ this.requestController = requestController;
30
+ this.concurrencyService = concurrencyService;
31
+ this.server = net.createServer((socket) => {
32
+ this.handleConnection(socket);
33
+ });
34
+
35
+ this.server.on('error', (err) => {
36
+ this.logger.error({ err }, 'Server error');
37
+ });
38
+ }
39
+
40
+ async listen(options: TransportOptions): Promise<string> {
41
+ return new Promise((resolve, reject) => {
42
+ if (options.path) {
43
+ // Strict IPC mode
44
+ const socketPath = this.formatSocketPath(options.path);
45
+ this.logger.info({ socketPath }, 'Binding to IPC socket');
46
+
47
+ // Cleanup existing socket if needed (unlikely on Windows, but good for Unix)
48
+ if (os.platform() !== 'win32' && path.isAbsolute(socketPath)) {
49
+ // We rely on caller or deployment to clean up, or error out.
50
+ // Trying to unlink here might be dangerous if we don't own it.
51
+ // But strictly, we should just listen.
52
+ }
53
+
54
+ this.server.listen(socketPath, () => {
55
+ this.resolveAddress(resolve);
56
+ });
57
+ } else if (options.port !== undefined) {
58
+ // Strict TCP mode
59
+ this.logger.info({ port: options.port, host: options.host }, 'Binding to TCP port');
60
+ this.server.listen(options.port, options.host || '127.0.0.1', () => {
61
+ this.resolveAddress(resolve);
62
+ });
63
+ } else {
64
+ reject(new Error('Invalid transport configuration: neither path nor port provided'));
65
+ return;
66
+ }
67
+
68
+ this.server.on('error', reject);
69
+ });
70
+ }
71
+
72
+ private resolveAddress(resolve: (value: string) => void) {
73
+ const address = this.server.address();
74
+ const addressStr = typeof address === 'string' ? address : `${address?.address}:${address?.port}`;
75
+ this.logger.info({ address: addressStr }, 'Transport server listening');
76
+ resolve(addressStr);
77
+ }
78
+
79
+ private formatSocketPath(inputPath: string): string {
80
+ if (os.platform() === 'win32') {
81
+ // Windows Named Pipe format: \\.\pipe\conduit-[id]
82
+ if (!inputPath.startsWith('\\\\.\\pipe\\')) {
83
+ return `\\\\.\\pipe\\${inputPath}`;
84
+ }
85
+ return inputPath;
86
+ } else {
87
+ // Unix Socket path
88
+ return path.isAbsolute(inputPath) ? inputPath : path.join(os.tmpdir(), inputPath);
89
+ }
90
+ }
91
+
92
+ private handleConnection(socket: net.Socket) {
93
+ const remoteAddress = socket.remoteAddress || 'pipe';
94
+ this.logger.debug({ remoteAddress }, 'New connection established');
95
+
96
+ socket.setEncoding('utf8');
97
+ let buffer = '';
98
+ const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB limit
99
+
100
+ socket.on('data', async (chunk) => {
101
+ buffer += chunk;
102
+
103
+ if (buffer.length > MAX_BUFFER_SIZE) {
104
+ this.logger.error({ remoteAddress }, 'Connection exceeded max buffer size, closing');
105
+ socket.destroy();
106
+ return;
107
+ }
108
+
109
+ // Backpressure: pause processing new chunks until this buffer is handled
110
+ socket.pause();
111
+
112
+ try {
113
+ let pos: number;
114
+ while ((pos = buffer.indexOf('\n')) >= 0) {
115
+ const line = buffer.substring(0, pos).trim();
116
+ buffer = buffer.substring(pos + 1);
117
+
118
+ if (!line) continue;
119
+
120
+ let request: JSONRPCRequest;
121
+ try {
122
+ request = JSON.parse(line) as JSONRPCRequest;
123
+ } catch (err) {
124
+ this.logger.error({ err, line }, 'Failed to parse JSON-RPC request');
125
+ const errorResponse = {
126
+ jsonrpc: '2.0',
127
+ id: null,
128
+ error: {
129
+ code: -32700,
130
+ message: 'Parse error',
131
+ },
132
+ };
133
+ socket.write(JSON.stringify(errorResponse) + '\n');
134
+ continue;
135
+ }
136
+
137
+ const context = new ExecutionContext({
138
+ logger: this.logger,
139
+ remoteAddress: remoteAddress,
140
+ });
141
+
142
+ await loggerStorage.run({ correlationId: context.correlationId }, async () => {
143
+ try {
144
+ const response = await this.concurrencyService.run(() =>
145
+ this.requestController.handleRequest(request, context)
146
+ );
147
+ socket.write(JSON.stringify(response) + '\n');
148
+ } catch (err: any) {
149
+ if (err.name === 'QueueFullError') {
150
+ socket.write(JSON.stringify({
151
+ jsonrpc: '2.0',
152
+ id: request.id,
153
+ error: {
154
+ code: ConduitError.ServerBusy,
155
+ message: 'Server busy'
156
+ }
157
+ }) + '\n');
158
+ } else {
159
+ this.logger.error({ err, requestId: request.id }, 'Request handling failed');
160
+ // Internal error handling usually done by Middleware/RequestController return
161
+ // But if something crashed outside standard flow:
162
+ socket.write(JSON.stringify({
163
+ jsonrpc: '2.0',
164
+ id: request.id,
165
+ error: {
166
+ code: ConduitError.InternalError,
167
+ message: 'Internal server error'
168
+ }
169
+ }) + '\n');
170
+ }
171
+ }
172
+ });
173
+ }
174
+ } catch (err) {
175
+ this.logger.error({ err }, 'Unexpected error in socket data handler');
176
+ socket.destroy();
177
+ } finally {
178
+ socket.resume();
179
+ }
180
+ });
181
+
182
+ socket.on('close', () => {
183
+ this.logger.debug({ remoteAddress }, 'Connection closed');
184
+ });
185
+
186
+ socket.on('error', (err) => {
187
+ this.logger.error({ err, remoteAddress }, 'Socket error');
188
+ });
189
+ }
190
+
191
+ async close(): Promise<void> {
192
+ return new Promise((resolve) => {
193
+ if (this.server.listening) {
194
+ this.server.close(() => {
195
+ this.logger.info('Transport server closed');
196
+ resolve();
197
+ });
198
+ } else {
199
+ resolve();
200
+ }
201
+ });
202
+ }
203
+ }