aquaman-plugin 0.6.0 → 0.7.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.
@@ -6,16 +6,6 @@
6
6
 
7
7
  import { Type, type Static } from '@sinclair/typebox';
8
8
 
9
- /**
10
- * Plugin operation mode
11
- * - embedded: Direct vault access within OpenClaw process (simpler, less isolation)
12
- * - proxy: Separate proxy process (stronger isolation, credentials never in Gateway)
13
- */
14
- export const PluginMode = Type.Union([
15
- Type.Literal('embedded'),
16
- Type.Literal('proxy')
17
- ], { default: 'embedded' });
18
-
19
9
  /**
20
10
  * Credential backend type
21
11
  */
@@ -38,19 +28,12 @@ export const ProxiedServices = Type.Array(Type.String(), {
38
28
  * Complete plugin configuration schema
39
29
  */
40
30
  export const ConfigSchema = Type.Object({
41
- // Mode selection
42
- mode: Type.Optional(PluginMode),
43
-
44
31
  // Credential backend
45
32
  backend: Type.Optional(CredentialBackend),
46
33
 
47
34
  // Services to proxy
48
35
  services: Type.Optional(ProxiedServices),
49
36
 
50
- // Proxy mode options
51
- proxyPort: Type.Optional(Type.Number({ default: 8081, minimum: 1024, maximum: 65535 })),
52
- proxyAutoStart: Type.Optional(Type.Boolean({ default: true })),
53
-
54
37
  // 1Password options
55
38
  onePasswordVault: Type.Optional(Type.String()),
56
39
  onePasswordAccount: Type.Optional(Type.String()),
@@ -60,15 +43,6 @@ export const ConfigSchema = Type.Object({
60
43
  vaultToken: Type.Optional(Type.String()),
61
44
  vaultNamespace: Type.Optional(Type.String()),
62
45
  vaultMountPath: Type.Optional(Type.String({ default: 'secret' })),
63
-
64
- // TLS options
65
- tlsEnabled: Type.Optional(Type.Boolean({ default: true })),
66
- tlsCertPath: Type.Optional(Type.String()),
67
- tlsKeyPath: Type.Optional(Type.String()),
68
-
69
- // Audit options
70
- auditEnabled: Type.Optional(Type.Boolean({ default: true })),
71
- auditLogDir: Type.Optional(Type.String())
72
46
  });
73
47
 
74
48
  export type PluginConfig = Static<typeof ConfigSchema>;
@@ -77,14 +51,9 @@ export type PluginConfig = Static<typeof ConfigSchema>;
77
51
  * Default configuration values
78
52
  */
79
53
  export const defaultConfig: PluginConfig = {
80
- mode: 'embedded',
81
54
  backend: 'keychain',
82
55
  services: ['anthropic', 'openai'],
83
- proxyPort: 8081,
84
- proxyAutoStart: true,
85
56
  vaultMountPath: 'secret',
86
- tlsEnabled: true,
87
- auditEnabled: true
88
57
  };
89
58
 
90
59
  /**
@@ -1,47 +1,44 @@
1
1
  /**
2
- * HTTP fetch interceptor for channel credential isolation.
2
+ * HTTP interceptor for channel credential isolation.
3
3
  *
4
4
  * Overrides globalThis.fetch to redirect requests targeting known channel API
5
- * hosts through the aquaman credential proxy. The proxy injects the real
6
- * credentials, so the Gateway process never sees them.
5
+ * hosts through the aquaman credential proxy via Unix domain socket.
6
+ * The proxy injects the real credentials, so the Gateway process never sees them.
7
7
  *
8
8
  * OpenClaw channels use globalThis.fetch (backed by undici) for all HTTP calls.
9
9
  * Many channel monitor functions also accept a `proxyFetch` parameter that
10
10
  * falls back to globalThis.fetch, so overriding it covers both paths.
11
+ *
12
+ * Uses sentinel hostname `aquaman.local` — SDK base URLs are set to
13
+ * `http://aquaman.local/<service>` and the interceptor routes them through UDS.
11
14
  */
12
15
 
16
+ import { Agent } from 'undici';
17
+
18
+ /** Sentinel hostname used in SDK base URLs to route through UDS */
19
+ const PROXY_HOST = 'aquaman.local';
20
+
13
21
  export interface HttpInterceptorOptions {
14
- /** Base URL of the aquaman proxy, e.g. "http://127.0.0.1:8081" */
15
- proxyBaseUrl: string;
22
+ /** Unix domain socket path for the proxy */
23
+ socketPath: string;
16
24
  /** Map of hostname (or *.domain wildcard) → service name */
17
25
  hostMap: Map<string, string>;
18
- /** Client authentication token for the proxy */
19
- clientToken?: string;
20
26
  /** Optional logger */
21
27
  log?: (msg: string) => void;
22
28
  }
23
29
 
24
30
  export class HttpInterceptor {
25
- private proxyBaseUrl: string;
26
- private proxyHost: string;
31
+ private socketPath: string;
27
32
  private hostMap: Map<string, string>;
28
- private clientToken: string | null;
29
33
  private originalFetch: typeof globalThis.fetch | null = null;
30
34
  private active = false;
31
35
  private log: (msg: string) => void;
36
+ private socketAgent: Agent | null = null;
32
37
 
33
38
  constructor(options: HttpInterceptorOptions) {
34
- this.proxyBaseUrl = options.proxyBaseUrl.replace(/\/$/, '');
39
+ this.socketPath = options.socketPath;
35
40
  this.hostMap = options.hostMap;
36
- this.clientToken = options.clientToken || null;
37
41
  this.log = options.log || (() => {});
38
-
39
- // Extract proxy hostname to avoid intercepting requests to the proxy itself
40
- try {
41
- this.proxyHost = new URL(this.proxyBaseUrl).hostname;
42
- } catch {
43
- this.proxyHost = '127.0.0.1';
44
- }
45
42
  }
46
43
 
47
44
  /**
@@ -51,15 +48,13 @@ export class HttpInterceptor {
51
48
  if (this.active) return;
52
49
 
53
50
  this.originalFetch = globalThis.fetch;
51
+ this.socketAgent = new Agent({ connect: { socketPath: this.socketPath } });
54
52
 
55
53
  const origFetch = this.originalFetch;
56
- const proxyBase = this.proxyBaseUrl;
57
- const proxyHostname = this.proxyHost;
58
- const token = this.clientToken;
54
+ const socketAgent = this.socketAgent;
59
55
  const matchHost = this.matchHost.bind(this);
60
56
  const extractUrl = this.extractUrl.bind(this);
61
57
  const stripAuthHeaders = this.stripAuthHeaders.bind(this);
62
- const injectToken = this.injectTokenHeader.bind(this);
63
58
  const logFn = this.log;
64
59
 
65
60
  (globalThis as any).fetch = (
@@ -71,22 +66,19 @@ export class HttpInterceptor {
71
66
  return origFetch.call(globalThis, input, init);
72
67
  }
73
68
 
74
- // Requests to the proxy itself (SDK traffic via env vars) — inject token, pass through
75
- if (url.hostname === proxyHostname || url.hostname === 'localhost') {
76
- if (token) {
77
- const tokenInit = injectToken(init, token);
78
- return origFetch.call(globalThis, input, tokenInit);
79
- }
80
- return origFetch.call(globalThis, input, init);
69
+ // SDK traffic via env vars (hostname = aquaman.local) — route through UDS
70
+ if (url.hostname === PROXY_HOST) {
71
+ return origFetch.call(globalThis, input, { ...init, dispatcher: socketAgent } as any);
81
72
  }
82
73
 
74
+ // Channel traffic — match against host map
83
75
  const service = matchHost(url.hostname);
84
76
  if (!service) {
85
77
  return origFetch.call(globalThis, input, init);
86
78
  }
87
79
 
88
80
  // Rewrite the URL to go through the proxy
89
- const proxyUrl = `${proxyBase}/${service}${url.pathname}${url.search}`;
81
+ const proxyUrl = `http://${PROXY_HOST}/${service}${url.pathname}${url.search}`;
90
82
  logFn(`[aquaman] Intercepted ${url.hostname}${url.pathname} → ${service}`);
91
83
 
92
84
  // Strip any existing authorization headers — the proxy will inject the real ones
@@ -96,12 +88,7 @@ export class HttpInterceptor {
96
88
  newInit = { ...init, headers: stripped };
97
89
  }
98
90
 
99
- // Inject client token for proxy authentication
100
- if (token) {
101
- newInit = injectToken(newInit, token);
102
- }
103
-
104
- return origFetch.call(globalThis, proxyUrl, { ...newInit, redirect: 'manual' });
91
+ return origFetch.call(globalThis, proxyUrl, { ...newInit, redirect: 'manual', dispatcher: socketAgent } as any);
105
92
  };
106
93
 
107
94
  this.active = true;
@@ -117,6 +104,12 @@ export class HttpInterceptor {
117
104
  globalThis.fetch = this.originalFetch;
118
105
  this.originalFetch = null;
119
106
  this.active = false;
107
+
108
+ if (this.socketAgent) {
109
+ this.socketAgent.close();
110
+ this.socketAgent = null;
111
+ }
112
+
120
113
  this.log('[aquaman] HTTP interceptor deactivated');
121
114
  }
122
115
 
@@ -153,28 +146,6 @@ export class HttpInterceptor {
153
146
  return null;
154
147
  }
155
148
 
156
- private injectTokenHeader(init: RequestInit | undefined, token: string): RequestInit {
157
- const base = init || {};
158
- const headers = base.headers;
159
-
160
- if (!headers) {
161
- return { ...base, headers: { 'x-aquaman-token': token } };
162
- }
163
-
164
- if (headers instanceof Headers) {
165
- const h = new Headers(headers);
166
- h.set('x-aquaman-token', token);
167
- return { ...base, headers: h };
168
- }
169
-
170
- if (Array.isArray(headers)) {
171
- return { ...base, headers: [...headers, ['x-aquaman-token', token]] };
172
- }
173
-
174
- // Plain object
175
- return { ...base, headers: { ...headers, 'x-aquaman-token': token } };
176
- }
177
-
178
149
  private stripAuthHeaders(headers: HeadersInit): HeadersInit {
179
150
  if (headers instanceof Headers) {
180
151
  const h = new Headers(headers);
package/src/index.ts CHANGED
@@ -3,9 +3,7 @@
3
3
  *
4
4
  * This package provides an OpenClaw plugin that enables:
5
5
  * - Secure credential storage using enterprise backends (Keychain, 1Password, Vault)
6
- * - Two operation modes:
7
- * - Embedded: Direct vault access (simpler, credentials in Gateway memory)
8
- * - Proxy: Separate process (stronger isolation, credentials never in Gateway)
6
+ * - Proxy mode: Separate process, credentials never in Gateway memory
9
7
  * - Hash-chained tamper-evident audit logs
10
8
  * - Slash commands for credential management
11
9
  *
@@ -16,7 +14,6 @@
16
14
  * {
17
15
  * "plugins": {
18
16
  * "aquaman-plugin": {
19
- * "mode": "embedded",
20
17
  * "backend": "keychain",
21
18
  * "services": ["anthropic", "openai"]
22
19
  * }
@@ -34,7 +31,6 @@ export {
34
31
  // Config Schema
35
32
  export {
36
33
  ConfigSchema,
37
- PluginMode,
38
34
  CredentialBackend,
39
35
  ProxiedServices,
40
36
  type PluginConfig,
@@ -42,13 +38,6 @@ export {
42
38
  mergeConfig
43
39
  } from './config-schema.js';
44
40
 
45
- // Embedded Mode
46
- export {
47
- type EmbeddedModeOptions,
48
- EmbeddedMode,
49
- createEmbeddedMode
50
- } from './embedded.js';
51
-
52
41
  // Proxy Manager
53
42
  export {
54
43
  type ProxyConnectionInfo,
@@ -66,7 +55,6 @@ export {
66
55
  listCommand,
67
56
  logsCommand,
68
57
  verifyCommand,
69
- modeCommand,
70
58
  executeCommand
71
59
  } from './commands.js';
72
60
 
package/src/plugin.ts CHANGED
@@ -2,14 +2,11 @@
2
2
  * OpenClaw Plugin Entry Point
3
3
  *
4
4
  * This is the main plugin that implements the OpenClaw plugin interface.
5
- * It provides credential isolation through two modes:
6
- *
7
- * 1. Embedded Mode (default): Direct vault access, simpler setup
8
- * 2. Proxy Mode: Separate process, stronger credential isolation
5
+ * It provides credential isolation through proxy mode:
6
+ * credentials are held in a separate process and never enter the Gateway.
9
7
  */
10
8
 
11
9
  import { type PluginConfig, mergeConfig, defaultConfig } from './config-schema.js';
12
- import { createEmbeddedMode, type EmbeddedMode } from './embedded.js';
13
10
  import { createProxyManager, type ProxyManager, type ProxyConnectionInfo } from './proxy-manager.js';
14
11
  import { executeCommand, type CommandContext, type CommandResult, getAvailableCommands, type PluginCommand } from './commands.js';
15
12
  import { HttpInterceptor, createHttpInterceptor } from './http-interceptor.js';
@@ -30,7 +27,6 @@ export class AquamanPlugin {
30
27
  readonly name = 'aquaman-plugin';
31
28
 
32
29
  private config: PluginConfig;
33
- private embeddedMode: EmbeddedMode | null = null;
34
30
  private proxyManager: ProxyManager | null = null;
35
31
  private httpInterceptor: HttpInterceptor | null = null;
36
32
  private initialized = false;
@@ -61,16 +57,10 @@ export class AquamanPlugin {
61
57
 
62
58
  console.log('[aquaman] Initializing plugin...');
63
59
 
64
- if (this.config.mode === 'proxy') {
65
- // Proxy mode: Start separate process
66
- await this.initProxyMode();
67
- } else {
68
- // Embedded mode: Direct vault access
69
- await this.initEmbeddedMode();
70
- }
60
+ await this.initProxyMode();
71
61
 
72
62
  this.initialized = true;
73
- console.log(`[aquaman] Plugin initialized in ${this.config.mode || 'embedded'} mode`);
63
+ console.log('[aquaman] Plugin initialized in proxy mode');
74
64
  }
75
65
 
76
66
  /**
@@ -100,40 +90,20 @@ export class AquamanPlugin {
100
90
  this.proxyManager = null;
101
91
  }
102
92
 
103
- this.embeddedMode = null;
104
93
  this.initialized = false;
105
94
 
106
95
  console.log('[aquaman] Plugin unloaded');
107
96
  }
108
97
 
109
- /**
110
- * Initialize embedded mode
111
- */
112
- private async initEmbeddedMode(): Promise<void> {
113
- this.embeddedMode = createEmbeddedMode({
114
- config: this.config
115
- });
116
-
117
- await this.embeddedMode.initialize();
118
-
119
- // Set environment variables for OpenClaw
120
- this.configureEnvironment();
121
- }
122
-
123
98
  /**
124
99
  * Initialize proxy mode
125
100
  */
126
101
  private async initProxyMode(): Promise<void> {
127
- if (!this.config.proxyAutoStart) {
128
- console.log('[aquaman] Proxy auto-start disabled');
129
- return;
130
- }
131
-
132
102
  this.proxyManager = createProxyManager({
133
103
  config: this.config,
134
104
  onReady: (info) => {
135
- console.log(`[aquaman] Proxy ready at ${info.baseUrl}`);
136
- this.configureEnvironmentForProxy(info);
105
+ console.log(`[aquaman] Proxy ready on ${info.socketPath}`);
106
+ this.configureEnvironmentForProxy();
137
107
  },
138
108
  onError: (error) => {
139
109
  console.error('[aquaman] Proxy error:', error);
@@ -143,28 +113,20 @@ export class AquamanPlugin {
143
113
  }
144
114
  });
145
115
 
146
- // Also initialize embedded mode for credential management
147
- this.embeddedMode = createEmbeddedMode({
148
- config: this.config
149
- });
150
- await this.embeddedMode.initialize();
151
-
152
116
  // Start proxy
153
117
  try {
154
118
  const info = await this.proxyManager.start();
155
- this.configureEnvironmentForProxy(info);
156
- this.activateHttpInterceptor(info.baseUrl);
119
+ this.configureEnvironmentForProxy();
120
+ this.activateHttpInterceptor(info.socketPath);
157
121
  } catch (error) {
158
122
  console.error('[aquaman] Failed to start proxy:', error);
159
- console.log('[aquaman] Falling back to embedded mode');
160
- this.configureEnvironment();
161
123
  }
162
124
  }
163
125
 
164
126
  /**
165
127
  * Activate HTTP interceptor for channel credential isolation.
166
128
  */
167
- private activateHttpInterceptor(proxyBaseUrl: string): void {
129
+ private activateHttpInterceptor(proxySocketPath: string): void {
168
130
  // Build host map from the service registry's host patterns
169
131
  const hostMap = new Map<string, string>([
170
132
  ['api.anthropic.com', 'anthropic'],
@@ -193,7 +155,7 @@ export class AquamanPlugin {
193
155
  ]);
194
156
 
195
157
  this.httpInterceptor = createHttpInterceptor({
196
- proxyBaseUrl,
158
+ socketPath: proxySocketPath,
197
159
  hostMap,
198
160
  log: (msg) => console.log(msg),
199
161
  });
@@ -202,68 +164,27 @@ export class AquamanPlugin {
202
164
  }
203
165
 
204
166
  /**
205
- * Configure environment variables for embedded mode
206
- * In embedded mode, we still set base URLs pointing to a local proxy
207
- * so credential injection works consistently.
208
- */
209
- private configureEnvironment(): void {
210
- const services = this.config.services || defaultConfig.services;
211
- const port = this.config.proxyPort || 8081;
212
- const baseUrl = `http://127.0.0.1:${port}`;
213
-
214
- this.setServiceEnvironmentVariables(services!, baseUrl);
215
- console.log('[aquaman] Embedded mode active - credentials available via plugin');
216
- }
217
-
218
- /**
219
- * Configure environment variables to route through proxy
167
+ * Configure environment variables using sentinel hostname
220
168
  */
221
- private configureEnvironmentForProxy(info: ProxyConnectionInfo): void {
169
+ private configureEnvironmentForProxy(): void {
222
170
  const services = this.config.services || defaultConfig.services;
223
- this.setServiceEnvironmentVariables(services!, info.baseUrl);
224
-
225
- // Handle TLS
226
- if (info.protocol === 'https') {
227
- if (this.config.tlsCertPath) {
228
- this.setEnvVar('NODE_EXTRA_CA_CERTS', this.config.tlsCertPath);
229
- } else {
230
- // Development: disable TLS verification for self-signed certs
231
- this.setEnvVar('NODE_TLS_REJECT_UNAUTHORIZED', '0');
232
- }
233
- }
234
- }
235
-
236
- /**
237
- * Set environment variables for configured services
238
- */
239
- private setServiceEnvironmentVariables(services: string[], baseUrl: string): void {
240
- for (const service of services) {
241
- const serviceUrl = `${baseUrl}/${service}`;
171
+ for (const service of services!) {
172
+ const serviceUrl = `http://aquaman.local/${service}`;
242
173
 
243
174
  switch (service) {
244
175
  case 'anthropic':
245
176
  this.setEnvVar('ANTHROPIC_BASE_URL', serviceUrl);
246
- this.setEnvVar('ANTHROPIC_API_KEY', 'aquaman-proxy-managed');
247
177
  break;
248
178
  case 'openai':
249
179
  this.setEnvVar('OPENAI_BASE_URL', serviceUrl);
250
- this.setEnvVar('OPENAI_API_KEY', 'aquaman-proxy-managed');
251
180
  break;
252
181
  case 'github':
253
182
  this.setEnvVar('GITHUB_API_URL', serviceUrl);
254
- this.setEnvVar('GITHUB_TOKEN', 'aquaman-proxy-managed');
255
183
  break;
256
- case 'slack':
257
- this.setEnvVar('SLACK_API_URL', serviceUrl);
258
- this.setEnvVar('SLACK_BOT_TOKEN', 'aquaman-proxy-managed');
259
- break;
260
- case 'discord':
261
- this.setEnvVar('DISCORD_API_URL', serviceUrl);
262
- this.setEnvVar('DISCORD_BOT_TOKEN', 'aquaman-proxy-managed');
263
- break;
264
- default:
184
+ default: {
265
185
  const envKey = `${service.toUpperCase().replace(/-/g, '_')}_BASE_URL`;
266
186
  this.setEnvVar(envKey, serviceUrl);
187
+ }
267
188
  }
268
189
  }
269
190
  }
@@ -283,69 +204,29 @@ export class AquamanPlugin {
283
204
  async executeCommand(command: string, args: string[] = []): Promise<CommandResult> {
284
205
  const ctx: CommandContext = {
285
206
  config: this.config,
286
- embeddedMode: this.embeddedMode || undefined,
287
207
  proxyManager: this.proxyManager || undefined
288
208
  };
289
209
 
290
210
  return executeCommand(ctx, command, args);
291
211
  }
292
212
 
293
- /**
294
- * Get credential (for embedded mode)
295
- */
296
- async getCredential(service: string, key: string): Promise<string | null> {
297
- if (!this.embeddedMode) {
298
- throw new Error('Plugin not initialized');
299
- }
300
- return this.embeddedMode.getCredential(service, key);
301
- }
302
-
303
- /**
304
- * Set credential (for embedded mode)
305
- */
306
- async setCredential(service: string, key: string, value: string): Promise<void> {
307
- if (!this.embeddedMode) {
308
- throw new Error('Plugin not initialized');
309
- }
310
- return this.embeddedMode.setCredential(service, key, value);
311
- }
312
-
313
- /**
314
- * List credentials
315
- */
316
- async listCredentials(service?: string): Promise<Array<{ service: string; key: string }>> {
317
- if (!this.embeddedMode) {
318
- throw new Error('Plugin not initialized');
319
- }
320
- return this.embeddedMode.listCredentials(service);
321
- }
322
-
323
213
  /**
324
214
  * Get plugin status
325
215
  */
326
216
  getStatus(): {
327
217
  initialized: boolean;
328
- mode: string;
329
218
  backend: string;
330
219
  proxyRunning: boolean;
331
220
  services: string[];
332
221
  } {
333
222
  return {
334
223
  initialized: this.initialized,
335
- mode: this.config.mode || 'embedded',
336
224
  backend: this.config.backend || 'keychain',
337
225
  proxyRunning: this.proxyManager?.isRunning() || false,
338
226
  services: this.config.services || []
339
227
  };
340
228
  }
341
229
 
342
- /**
343
- * Get current operating mode
344
- */
345
- getMode(): 'embedded' | 'proxy' {
346
- return (this.config.mode as 'embedded' | 'proxy') || 'embedded';
347
- }
348
-
349
230
  /**
350
231
  * Get configured backend
351
232
  */
@@ -373,20 +254,12 @@ export class AquamanPlugin {
373
254
  getCommands(): PluginCommand[] {
374
255
  const ctx: CommandContext = {
375
256
  config: this.config,
376
- embeddedMode: this.embeddedMode || undefined,
377
257
  proxyManager: this.proxyManager || undefined
378
258
  };
379
259
 
380
260
  return getAvailableCommands(ctx);
381
261
  }
382
262
 
383
- /**
384
- * Get proxy URL for a service (proxy mode only)
385
- */
386
- getProxyUrl(service: string): string | null {
387
- return this.proxyManager?.getServiceUrl(service) || null;
388
- }
389
-
390
263
  /**
391
264
  * Check if proxy is healthy
392
265
  */
@@ -3,60 +3,65 @@
3
3
  *
4
4
  * Separated from index.ts to avoid co-locating network calls with env reads
5
5
  * (triggers OpenClaw code safety scanner env-harvesting false positive).
6
+ *
7
+ * Uses http.request with socketPath for UDS communication.
6
8
  */
7
9
 
10
+ import * as http from 'node:http';
11
+
8
12
  /**
9
- * Request host map from proxy's /_hostmap endpoint.
13
+ * Make an HTTP request over a Unix domain socket.
14
+ */
15
+ function udsRequest(socketPath: string, urlPath: string, timeoutMs: number = 3000): Promise<{ ok: boolean; data: any }> {
16
+ return new Promise((resolve) => {
17
+ const req = http.request(
18
+ { socketPath, path: urlPath, method: 'GET' },
19
+ (res) => {
20
+ let body = '';
21
+ res.on('data', (chunk) => { body += chunk; });
22
+ res.on('end', () => {
23
+ try {
24
+ resolve({ ok: res.statusCode === 200, data: JSON.parse(body) });
25
+ } catch {
26
+ resolve({ ok: false, data: null });
27
+ }
28
+ });
29
+ }
30
+ );
31
+ req.on('error', () => resolve({ ok: false, data: null }));
32
+ req.setTimeout(timeoutMs, () => { req.destroy(); resolve({ ok: false, data: null }); });
33
+ req.end();
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Request host map from proxy's /_hostmap endpoint via UDS.
10
39
  * Returns an empty map if the endpoint is unavailable (caller handles fallback).
11
40
  */
12
- export async function loadHostMap(
13
- baseUrl: string,
14
- token: string | null,
15
- ): Promise<Map<string, string>> {
16
- try {
17
- const headers: Record<string, string> = {};
18
- if (token) headers['X-Aquaman-Token'] = token;
19
- const resp = await fetch(`${baseUrl}/_hostmap`, {
20
- headers,
21
- signal: AbortSignal.timeout(3000),
22
- });
23
- if (resp.ok) {
24
- const obj = (await resp.json()) as Record<string, string>;
25
- return new Map(Object.entries(obj));
26
- }
27
- } catch {
28
- // Proxy may be older version without /_hostmap — caller uses fallback
41
+ export async function loadHostMap(socketPath: string): Promise<Map<string, string>> {
42
+ const result = await udsRequest(socketPath, '/_hostmap');
43
+ if (result.ok && result.data) {
44
+ return new Map(Object.entries(result.data as Record<string, string>));
29
45
  }
30
46
  return new Map();
31
47
  }
32
48
 
33
49
  /**
34
- * Check if a proxy is already running on the given port.
50
+ * Check if a proxy is running on the given socket path.
35
51
  */
36
- export async function isProxyRunning(port: number): Promise<boolean> {
37
- try {
38
- const resp = await fetch(`http://127.0.0.1:${port}/_health`);
39
- return resp.ok;
40
- } catch {
41
- return false;
42
- }
52
+ export async function isProxyRunning(socketPath: string): Promise<boolean> {
53
+ const result = await udsRequest(socketPath, '/_health');
54
+ return result.ok;
43
55
  }
44
56
 
45
57
  /**
46
- * Get the version of a running proxy from its /_health endpoint.
58
+ * Get the version of a running proxy from its /_health endpoint via UDS.
47
59
  * Returns null if the proxy is not running or doesn't report version.
48
60
  */
49
- export async function getProxyVersion(proxyUrl: string): Promise<string | null> {
50
- try {
51
- const resp = await fetch(`${proxyUrl}/_health`, {
52
- signal: AbortSignal.timeout(3000),
53
- });
54
- if (resp.ok) {
55
- const data = (await resp.json()) as { version?: string };
56
- return data.version || null;
57
- }
58
- } catch {
59
- // Proxy not reachable
61
+ export async function getProxyVersion(socketPath: string): Promise<string | null> {
62
+ const result = await udsRequest(socketPath, '/_health');
63
+ if (result.ok && result.data?.version) {
64
+ return result.data.version;
60
65
  }
61
66
  return null;
62
67
  }