aquaman-plugin 0.11.2 → 0.11.4
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/README.md +38 -10
- package/dist/index.d.ts +81 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +502 -0
- package/dist/index.js.map +1 -0
- package/dist/src/config-schema.d.ts +37 -0
- package/dist/src/config-schema.d.ts.map +1 -0
- package/dist/src/config-schema.js +58 -0
- package/dist/src/config-schema.js.map +1 -0
- package/dist/src/http-interceptor.d.ts +48 -0
- package/dist/src/http-interceptor.d.ts.map +1 -0
- package/dist/src/http-interceptor.js +143 -0
- package/dist/src/http-interceptor.js.map +1 -0
- package/dist/src/proxy-health.d.ts +23 -0
- package/dist/src/proxy-health.d.ts.map +1 -0
- package/dist/src/proxy-health.js +61 -0
- package/dist/src/proxy-health.js.map +1 -0
- package/dist/src/proxy-manager.d.ts +79 -0
- package/dist/src/proxy-manager.d.ts.map +1 -0
- package/dist/src/proxy-manager.js +246 -0
- package/dist/src/proxy-manager.js.map +1 -0
- package/openclaw.plugin.json +6 -1
- package/package.json +12 -11
- package/index.ts +0 -592
- package/src/commands.ts +0 -306
- package/src/config-schema.ts +0 -68
- package/src/http-interceptor.ts +0 -176
- package/src/index.ts +0 -62
- package/src/plugin.ts +0 -281
- package/src/proxy-health.ts +0 -67
- package/src/proxy-manager.ts +0 -309
package/src/plugin.ts
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenClaw Plugin Entry Point
|
|
3
|
-
*
|
|
4
|
-
* This is the main plugin that implements the OpenClaw plugin interface.
|
|
5
|
-
* It provides credential isolation through proxy mode:
|
|
6
|
-
* credentials are held in a separate process and never enter the Gateway.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { type PluginConfig, mergeConfig, defaultConfig } from './config-schema.js';
|
|
10
|
-
import { createProxyManager, type ProxyManager, type ProxyConnectionInfo } from './proxy-manager.js';
|
|
11
|
-
import { executeCommand, type CommandContext, type CommandResult, getAvailableCommands, type PluginCommand } from './commands.js';
|
|
12
|
-
import { HttpInterceptor, createHttpInterceptor } from './http-interceptor.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* OpenClaw Plugin Interface (simplified for standalone use)
|
|
16
|
-
*
|
|
17
|
-
* When used with actual OpenClaw, this would implement their ToolPlugin interface.
|
|
18
|
-
*/
|
|
19
|
-
export interface AquamanPluginOptions {
|
|
20
|
-
config?: Partial<PluginConfig>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class AquamanPlugin {
|
|
24
|
-
/**
|
|
25
|
-
* Plugin name - required by OpenClaw ToolPlugin interface
|
|
26
|
-
*/
|
|
27
|
-
readonly name = 'aquaman-plugin';
|
|
28
|
-
|
|
29
|
-
private config: PluginConfig;
|
|
30
|
-
private proxyManager: ProxyManager | null = null;
|
|
31
|
-
private httpInterceptor: HttpInterceptor | null = null;
|
|
32
|
-
private initialized = false;
|
|
33
|
-
private environmentVariables: Record<string, string> = {};
|
|
34
|
-
|
|
35
|
-
constructor(options: AquamanPluginOptions = {}) {
|
|
36
|
-
this.config = mergeConfig(options.config || {});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Plugin lifecycle: onLoad
|
|
41
|
-
* Called when the plugin is loaded by OpenClaw
|
|
42
|
-
*
|
|
43
|
-
* @param config - Configuration passed from openclaw.json
|
|
44
|
-
*/
|
|
45
|
-
async onLoad(config?: Partial<PluginConfig>): Promise<void> {
|
|
46
|
-
if (this.initialized) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Merge any runtime config with defaults
|
|
51
|
-
if (config) {
|
|
52
|
-
this.config = mergeConfig({ ...this.config, ...config });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Validate config
|
|
56
|
-
this.validateConfig();
|
|
57
|
-
|
|
58
|
-
console.log('[aquaman] Initializing plugin...');
|
|
59
|
-
|
|
60
|
-
await this.initProxyMode();
|
|
61
|
-
|
|
62
|
-
this.initialized = true;
|
|
63
|
-
console.log('[aquaman] Plugin initialized in proxy mode');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Validate configuration
|
|
68
|
-
*/
|
|
69
|
-
private validateConfig(): void {
|
|
70
|
-
// Vault backend requires address
|
|
71
|
-
if (this.config.backend === 'vault' && !this.config.vaultAddress) {
|
|
72
|
-
throw new Error('Vault backend requires vaultAddress configuration');
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Plugin lifecycle: onUnload
|
|
78
|
-
* Called when the plugin is unloaded
|
|
79
|
-
*/
|
|
80
|
-
async onUnload(): Promise<void> {
|
|
81
|
-
console.log('[aquaman] Unloading plugin...');
|
|
82
|
-
|
|
83
|
-
if (this.httpInterceptor) {
|
|
84
|
-
this.httpInterceptor.deactivate();
|
|
85
|
-
this.httpInterceptor = null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (this.proxyManager) {
|
|
89
|
-
await this.proxyManager.stop();
|
|
90
|
-
this.proxyManager = null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
this.initialized = false;
|
|
94
|
-
|
|
95
|
-
console.log('[aquaman] Plugin unloaded');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Initialize proxy mode
|
|
100
|
-
*/
|
|
101
|
-
private async initProxyMode(): Promise<void> {
|
|
102
|
-
this.proxyManager = createProxyManager({
|
|
103
|
-
config: this.config,
|
|
104
|
-
onReady: (info) => {
|
|
105
|
-
console.log(`[aquaman] Proxy ready on ${info.socketPath}`);
|
|
106
|
-
this.configureEnvironmentForProxy();
|
|
107
|
-
},
|
|
108
|
-
onError: (error) => {
|
|
109
|
-
console.error('[aquaman] Proxy error:', error);
|
|
110
|
-
},
|
|
111
|
-
onExit: (code) => {
|
|
112
|
-
console.log(`[aquaman] Proxy exited with code ${code}`);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Start proxy
|
|
117
|
-
try {
|
|
118
|
-
const info = await this.proxyManager.start();
|
|
119
|
-
this.configureEnvironmentForProxy();
|
|
120
|
-
this.activateHttpInterceptor(info.socketPath);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
console.error('[aquaman] Failed to start proxy:', error);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Activate HTTP interceptor for channel credential isolation.
|
|
128
|
-
*/
|
|
129
|
-
private activateHttpInterceptor(proxySocketPath: string): void {
|
|
130
|
-
// Build host map from the service registry's host patterns
|
|
131
|
-
const hostMap = new Map<string, string>([
|
|
132
|
-
['api.anthropic.com', 'anthropic'],
|
|
133
|
-
['api.openai.com', 'openai'],
|
|
134
|
-
['api.github.com', 'github'],
|
|
135
|
-
['slack.com', 'slack'],
|
|
136
|
-
['*.slack.com', 'slack'],
|
|
137
|
-
['discord.com', 'discord'],
|
|
138
|
-
['*.discord.com', 'discord'],
|
|
139
|
-
['api.telegram.org', 'telegram'],
|
|
140
|
-
['matrix.org', 'matrix'],
|
|
141
|
-
['*.matrix.org', 'matrix'],
|
|
142
|
-
['api.line.me', 'line'],
|
|
143
|
-
['api-data.line.me', 'line'],
|
|
144
|
-
['api.twitch.tv', 'twitch'],
|
|
145
|
-
['id.twitch.tv', 'twitch'],
|
|
146
|
-
['api.twilio.com', 'twilio'],
|
|
147
|
-
['*.twilio.com', 'twilio'],
|
|
148
|
-
['api.telnyx.com', 'telnyx'],
|
|
149
|
-
['api.elevenlabs.io', 'elevenlabs'],
|
|
150
|
-
['openapi.zalo.me', 'zalo'],
|
|
151
|
-
['graph.microsoft.com', 'ms-teams'],
|
|
152
|
-
['open.feishu.cn', 'feishu'],
|
|
153
|
-
['open.larksuite.com', 'feishu'],
|
|
154
|
-
['chat.googleapis.com', 'google-chat'],
|
|
155
|
-
]);
|
|
156
|
-
|
|
157
|
-
this.httpInterceptor = createHttpInterceptor({
|
|
158
|
-
socketPath: proxySocketPath,
|
|
159
|
-
hostMap,
|
|
160
|
-
log: (msg) => console.log(msg),
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
this.httpInterceptor.activate();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Configure environment variables using sentinel hostname
|
|
168
|
-
*/
|
|
169
|
-
private configureEnvironmentForProxy(): void {
|
|
170
|
-
const services = this.config.services || defaultConfig.services;
|
|
171
|
-
for (const service of services!) {
|
|
172
|
-
const serviceUrl = `http://aquaman.local/${service}`;
|
|
173
|
-
|
|
174
|
-
switch (service) {
|
|
175
|
-
case 'anthropic':
|
|
176
|
-
this.setEnvVar('ANTHROPIC_BASE_URL', serviceUrl);
|
|
177
|
-
break;
|
|
178
|
-
case 'openai':
|
|
179
|
-
this.setEnvVar('OPENAI_BASE_URL', serviceUrl);
|
|
180
|
-
break;
|
|
181
|
-
case 'github':
|
|
182
|
-
this.setEnvVar('GITHUB_API_URL', serviceUrl);
|
|
183
|
-
break;
|
|
184
|
-
default: {
|
|
185
|
-
const envKey = `${service.toUpperCase().replace(/-/g, '_')}_BASE_URL`;
|
|
186
|
-
this.setEnvVar(envKey, serviceUrl);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Set an environment variable and track it
|
|
194
|
-
*/
|
|
195
|
-
private setEnvVar(key: string, value: string): void {
|
|
196
|
-
process.env[key] = value;
|
|
197
|
-
this.environmentVariables[key] = value;
|
|
198
|
-
console.log(`[aquaman] Set ${key}=${value}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Execute a slash command
|
|
203
|
-
*/
|
|
204
|
-
async executeCommand(command: string, args: string[] = []): Promise<CommandResult> {
|
|
205
|
-
const ctx: CommandContext = {
|
|
206
|
-
config: this.config,
|
|
207
|
-
proxyManager: this.proxyManager || undefined
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
return executeCommand(ctx, command, args);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Get plugin status
|
|
215
|
-
*/
|
|
216
|
-
getStatus(): {
|
|
217
|
-
initialized: boolean;
|
|
218
|
-
backend: string;
|
|
219
|
-
proxyRunning: boolean;
|
|
220
|
-
services: string[];
|
|
221
|
-
} {
|
|
222
|
-
return {
|
|
223
|
-
initialized: this.initialized,
|
|
224
|
-
backend: this.config.backend || 'keychain',
|
|
225
|
-
proxyRunning: this.proxyManager?.isRunning() || false,
|
|
226
|
-
services: this.config.services || []
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Get configured backend
|
|
232
|
-
*/
|
|
233
|
-
getBackend(): string {
|
|
234
|
-
return this.config.backend || 'keychain';
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Check if plugin is ready
|
|
239
|
-
*/
|
|
240
|
-
isReady(): boolean {
|
|
241
|
-
return this.initialized;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Get environment variables set by the plugin
|
|
246
|
-
*/
|
|
247
|
-
getEnvironmentVariables(): Record<string, string> {
|
|
248
|
-
return { ...this.environmentVariables };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Get available slash commands
|
|
253
|
-
*/
|
|
254
|
-
getCommands(): PluginCommand[] {
|
|
255
|
-
const ctx: CommandContext = {
|
|
256
|
-
config: this.config,
|
|
257
|
-
proxyManager: this.proxyManager || undefined
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
return getAvailableCommands(ctx);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Check if proxy is healthy
|
|
265
|
-
*/
|
|
266
|
-
async isProxyHealthy(): Promise<boolean> {
|
|
267
|
-
return this.proxyManager?.isRunning() || false;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Create plugin instance
|
|
273
|
-
*/
|
|
274
|
-
export function createAquamanPlugin(options?: AquamanPluginOptions): AquamanPlugin {
|
|
275
|
-
return new AquamanPlugin(options);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Default export for OpenClaw plugin loading
|
|
280
|
-
*/
|
|
281
|
-
export default AquamanPlugin;
|
package/src/proxy-health.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Proxy health and discovery utilities.
|
|
3
|
-
*
|
|
4
|
-
* Separated from index.ts to avoid co-locating network calls with env reads
|
|
5
|
-
* (triggers OpenClaw code safety scanner env-harvesting false positive).
|
|
6
|
-
*
|
|
7
|
-
* Uses http.request with socketPath for UDS communication.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import * as http from 'node:http';
|
|
11
|
-
|
|
12
|
-
/**
|
|
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.
|
|
39
|
-
* Returns an empty map if the endpoint is unavailable (caller handles fallback).
|
|
40
|
-
*/
|
|
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>));
|
|
45
|
-
}
|
|
46
|
-
return new Map();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Check if a proxy is running on the given socket path.
|
|
51
|
-
*/
|
|
52
|
-
export async function isProxyRunning(socketPath: string): Promise<boolean> {
|
|
53
|
-
const result = await udsRequest(socketPath, '/_health');
|
|
54
|
-
return result.ok;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Get the version of a running proxy from its /_health endpoint via UDS.
|
|
59
|
-
* Returns null if the proxy is not running or doesn't report version.
|
|
60
|
-
*/
|
|
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;
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
package/src/proxy-manager.ts
DELETED
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Proxy process lifecycle manager
|
|
3
|
-
*
|
|
4
|
-
* Spawns and manages the aquaman proxy daemon as a separate process
|
|
5
|
-
* for maximum credential isolation. Communicates via Unix domain socket.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { spawn, type ChildProcess } from 'node:child_process';
|
|
9
|
-
import * as path from 'node:path';
|
|
10
|
-
import * as fs from 'node:fs';
|
|
11
|
-
import * as os from 'node:os';
|
|
12
|
-
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import type { PluginConfig } from './config-schema.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Find the aquaman proxy binary.
|
|
17
|
-
*
|
|
18
|
-
* Search order:
|
|
19
|
-
* 1. Plugin's own node_modules/.bin/aquaman (bundled dep — version-matched)
|
|
20
|
-
* 2. PATH (global install via npm install -g aquaman-proxy)
|
|
21
|
-
*/
|
|
22
|
-
export function findAquamanProxyBinary(): string | null {
|
|
23
|
-
// 1. Resolve from this file's location → plugin package root → node_modules/.bin/
|
|
24
|
-
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
const pluginRoot = path.resolve(thisDir, '..');
|
|
26
|
-
const localBin = path.join(pluginRoot, 'node_modules', '.bin', 'aquaman');
|
|
27
|
-
if (fs.existsSync(localBin)) {
|
|
28
|
-
return localBin;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// 2. Search PATH
|
|
32
|
-
const pathEnv = process.env.PATH || '';
|
|
33
|
-
const dirs = pathEnv.split(path.delimiter);
|
|
34
|
-
for (const dir of dirs) {
|
|
35
|
-
const candidate = path.join(dir, 'aquaman');
|
|
36
|
-
try {
|
|
37
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
38
|
-
return candidate;
|
|
39
|
-
} catch {
|
|
40
|
-
// Not found in this dir
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Execute an aquaman proxy CLI command (non-interactive).
|
|
49
|
-
* Captures stdout/stderr and returns them.
|
|
50
|
-
*/
|
|
51
|
-
export function execAquamanProxyCli(
|
|
52
|
-
args: string[],
|
|
53
|
-
options?: { timeoutMs?: number },
|
|
54
|
-
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
55
|
-
return new Promise((resolve, reject) => {
|
|
56
|
-
const binary = findAquamanProxyBinary();
|
|
57
|
-
if (!binary) {
|
|
58
|
-
reject(new Error('aquaman proxy binary not found. Install with: npm install -g aquaman-proxy'));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const proc = spawn(binary, args, {
|
|
63
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
64
|
-
env: process.env,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
let stdout = '';
|
|
68
|
-
let stderr = '';
|
|
69
|
-
|
|
70
|
-
proc.stdout?.on('data', (d: Buffer) => { stdout += d.toString(); });
|
|
71
|
-
proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString(); });
|
|
72
|
-
|
|
73
|
-
proc.on('error', reject);
|
|
74
|
-
proc.on('close', (code) => {
|
|
75
|
-
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const timeout = options?.timeoutMs ?? 30_000;
|
|
79
|
-
const timer = setTimeout(() => {
|
|
80
|
-
proc.kill('SIGTERM');
|
|
81
|
-
reject(new Error(`aquaman CLI timed out after ${timeout}ms`));
|
|
82
|
-
}, timeout);
|
|
83
|
-
|
|
84
|
-
proc.on('close', () => clearTimeout(timer));
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Execute an aquaman proxy CLI command interactively (stdio: inherit).
|
|
90
|
-
* Used for commands that need TTY input (setup, credentials add).
|
|
91
|
-
*/
|
|
92
|
-
export function execAquamanProxyInteractive(
|
|
93
|
-
args: string[],
|
|
94
|
-
): Promise<number> {
|
|
95
|
-
return new Promise((resolve, reject) => {
|
|
96
|
-
const binary = findAquamanProxyBinary();
|
|
97
|
-
if (!binary) {
|
|
98
|
-
reject(new Error('aquaman proxy binary not found. Install with: npm install -g aquaman-proxy'));
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const proc = spawn(binary, args, {
|
|
103
|
-
stdio: 'inherit',
|
|
104
|
-
env: process.env,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
proc.on('error', reject);
|
|
108
|
-
proc.on('close', (code) => resolve(code ?? 1));
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface ProxyConnectionInfo {
|
|
113
|
-
ready: boolean;
|
|
114
|
-
socketPath: string;
|
|
115
|
-
services: string[];
|
|
116
|
-
backend: string;
|
|
117
|
-
hostMap?: Record<string, string>;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export interface ProxyManagerOptions {
|
|
121
|
-
config: PluginConfig;
|
|
122
|
-
onReady?: (info: ProxyConnectionInfo) => void;
|
|
123
|
-
onError?: (error: Error) => void;
|
|
124
|
-
onExit?: (code: number | null) => void;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export class ProxyManager {
|
|
128
|
-
private process: ChildProcess | null = null;
|
|
129
|
-
private options: ProxyManagerOptions;
|
|
130
|
-
private connectionInfo: ProxyConnectionInfo | null = null;
|
|
131
|
-
private starting = false;
|
|
132
|
-
private startPromise: Promise<ProxyConnectionInfo> | null = null;
|
|
133
|
-
|
|
134
|
-
constructor(options: ProxyManagerOptions) {
|
|
135
|
-
this.options = options;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Start the proxy process
|
|
140
|
-
*/
|
|
141
|
-
async start(): Promise<ProxyConnectionInfo> {
|
|
142
|
-
if (this.process && this.connectionInfo) {
|
|
143
|
-
return this.connectionInfo;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (this.starting && this.startPromise) {
|
|
147
|
-
return this.startPromise;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
this.starting = true;
|
|
151
|
-
this.startPromise = this.doStart();
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const result = await this.startPromise;
|
|
155
|
-
return result;
|
|
156
|
-
} finally {
|
|
157
|
-
this.starting = false;
|
|
158
|
-
this.startPromise = null;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
private async doStart(): Promise<ProxyConnectionInfo> {
|
|
163
|
-
return new Promise((resolve, reject) => {
|
|
164
|
-
const config = this.options.config;
|
|
165
|
-
|
|
166
|
-
// Find aquaman binary
|
|
167
|
-
const binaryPath = this.findBinary();
|
|
168
|
-
|
|
169
|
-
if (!binaryPath) {
|
|
170
|
-
const error = new Error(
|
|
171
|
-
'aquaman proxy binary not found. Install with: npm install -g aquaman-proxy'
|
|
172
|
-
);
|
|
173
|
-
this.options.onError?.(error);
|
|
174
|
-
reject(error);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Build arguments — UDS is the default, no --port needed
|
|
179
|
-
const args = ['plugin-mode'];
|
|
180
|
-
|
|
181
|
-
// Spawn proxy process
|
|
182
|
-
this.process = spawn(binaryPath, args, {
|
|
183
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
184
|
-
env: {
|
|
185
|
-
...process.env,
|
|
186
|
-
// Pass config through environment
|
|
187
|
-
AQUAMAN_BACKEND: config.backend,
|
|
188
|
-
AQUAMAN_VAULT_ADDRESS: config.vaultAddress,
|
|
189
|
-
AQUAMAN_VAULT_TOKEN: config.vaultToken,
|
|
190
|
-
AQUAMAN_VAULT_NAMESPACE: config.vaultNamespace,
|
|
191
|
-
AQUAMAN_VAULT_MOUNT_PATH: config.vaultMountPath,
|
|
192
|
-
AQUAMAN_1PASSWORD_VAULT: config.onePasswordVault,
|
|
193
|
-
AQUAMAN_1PASSWORD_ACCOUNT: config.onePasswordAccount
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
let stdout = '';
|
|
198
|
-
let stderr = '';
|
|
199
|
-
|
|
200
|
-
this.process.stdout?.on('data', (data) => {
|
|
201
|
-
stdout += data.toString();
|
|
202
|
-
|
|
203
|
-
// Try to parse connection info from first line
|
|
204
|
-
const firstLine = stdout.split('\n')[0];
|
|
205
|
-
if (firstLine && !this.connectionInfo) {
|
|
206
|
-
try {
|
|
207
|
-
const info = JSON.parse(firstLine) as ProxyConnectionInfo;
|
|
208
|
-
if (info.ready) {
|
|
209
|
-
this.connectionInfo = info;
|
|
210
|
-
this.options.onReady?.(info);
|
|
211
|
-
resolve(info);
|
|
212
|
-
}
|
|
213
|
-
} catch {
|
|
214
|
-
// Not JSON yet, keep buffering
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
this.process.stderr?.on('data', (data) => {
|
|
220
|
-
stderr += data.toString();
|
|
221
|
-
console.error('[aquaman-proxy]', data.toString().trim());
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
this.process.on('error', (error) => {
|
|
225
|
-
this.options.onError?.(error);
|
|
226
|
-
reject(error);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
this.process.on('exit', (code) => {
|
|
230
|
-
this.process = null;
|
|
231
|
-
this.connectionInfo = null;
|
|
232
|
-
this.options.onExit?.(code);
|
|
233
|
-
|
|
234
|
-
if (!this.connectionInfo) {
|
|
235
|
-
const stderrText = stderr || stdout;
|
|
236
|
-
const error = new Error(`Proxy exited with code ${code}: ${stderrText}`);
|
|
237
|
-
reject(error);
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Timeout after 10 seconds
|
|
242
|
-
setTimeout(() => {
|
|
243
|
-
if (!this.connectionInfo) {
|
|
244
|
-
const error = new Error('Proxy startup timeout');
|
|
245
|
-
this.stop();
|
|
246
|
-
reject(error);
|
|
247
|
-
}
|
|
248
|
-
}, 10000);
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Stop the proxy process
|
|
254
|
-
*/
|
|
255
|
-
async stop(): Promise<void> {
|
|
256
|
-
if (!this.process) {
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return new Promise((resolve) => {
|
|
261
|
-
const proc = this.process!;
|
|
262
|
-
|
|
263
|
-
const timeout = setTimeout(() => {
|
|
264
|
-
proc.kill('SIGKILL');
|
|
265
|
-
}, 5000);
|
|
266
|
-
|
|
267
|
-
proc.on('exit', () => {
|
|
268
|
-
clearTimeout(timeout);
|
|
269
|
-
this.process = null;
|
|
270
|
-
this.connectionInfo = null;
|
|
271
|
-
resolve();
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
proc.kill('SIGTERM');
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Check if proxy is running
|
|
280
|
-
*/
|
|
281
|
-
isRunning(): boolean {
|
|
282
|
-
return this.process !== null && this.connectionInfo !== null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Get connection info
|
|
287
|
-
*/
|
|
288
|
-
getConnectionInfo(): ProxyConnectionInfo | null {
|
|
289
|
-
return this.connectionInfo;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Get socket path
|
|
294
|
-
*/
|
|
295
|
-
getSocketPath(): string | null {
|
|
296
|
-
return this.connectionInfo?.socketPath || null;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Find the aquaman proxy binary
|
|
301
|
-
*/
|
|
302
|
-
private findBinary(): string | null {
|
|
303
|
-
return findAquamanProxyBinary();
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function createProxyManager(options: ProxyManagerOptions): ProxyManager {
|
|
308
|
-
return new ProxyManager(options);
|
|
309
|
-
}
|