aquaman-plugin 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.
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Proxy process lifecycle manager
3
+ *
4
+ * Spawns and manages the aquaman proxy daemon as a separate process
5
+ * for maximum credential isolation.
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 type { PluginConfig } from './config-schema.js';
12
+
13
+ export interface ProxyConnectionInfo {
14
+ ready: boolean;
15
+ port: number;
16
+ protocol: 'http' | 'https';
17
+ baseUrl: string;
18
+ services: string[];
19
+ backend: string;
20
+ }
21
+
22
+ export interface ProxyManagerOptions {
23
+ config: PluginConfig;
24
+ onReady?: (info: ProxyConnectionInfo) => void;
25
+ onError?: (error: Error) => void;
26
+ onExit?: (code: number | null) => void;
27
+ }
28
+
29
+ export class ProxyManager {
30
+ private process: ChildProcess | null = null;
31
+ private options: ProxyManagerOptions;
32
+ private connectionInfo: ProxyConnectionInfo | null = null;
33
+ private starting = false;
34
+ private startPromise: Promise<ProxyConnectionInfo> | null = null;
35
+
36
+ constructor(options: ProxyManagerOptions) {
37
+ this.options = options;
38
+ }
39
+
40
+ /**
41
+ * Start the proxy process
42
+ */
43
+ async start(): Promise<ProxyConnectionInfo> {
44
+ if (this.process && this.connectionInfo) {
45
+ return this.connectionInfo;
46
+ }
47
+
48
+ if (this.starting && this.startPromise) {
49
+ return this.startPromise;
50
+ }
51
+
52
+ this.starting = true;
53
+ this.startPromise = this.doStart();
54
+
55
+ try {
56
+ const result = await this.startPromise;
57
+ return result;
58
+ } finally {
59
+ this.starting = false;
60
+ this.startPromise = null;
61
+ }
62
+ }
63
+
64
+ private async doStart(): Promise<ProxyConnectionInfo> {
65
+ return new Promise((resolve, reject) => {
66
+ const config = this.options.config;
67
+
68
+ // Find aquaman binary
69
+ const binaryPath = this.findAquamanBinary();
70
+
71
+ if (!binaryPath) {
72
+ const error = new Error(
73
+ 'aquaman proxy binary not found. Install with: npm install -g aquaman-proxy'
74
+ );
75
+ this.options.onError?.(error);
76
+ reject(error);
77
+ return;
78
+ }
79
+
80
+ // Build arguments
81
+ const args = [
82
+ 'plugin-mode',
83
+ '--port', String(config.proxyPort || 8081)
84
+ ];
85
+
86
+ // Spawn proxy process
87
+ this.process = spawn(binaryPath, args, {
88
+ stdio: ['ignore', 'pipe', 'pipe'],
89
+ env: {
90
+ ...process.env,
91
+ // Pass config through environment
92
+ AQUAMAN_BACKEND: config.backend,
93
+ AQUAMAN_VAULT_ADDRESS: config.vaultAddress,
94
+ AQUAMAN_VAULT_TOKEN: config.vaultToken,
95
+ AQUAMAN_VAULT_NAMESPACE: config.vaultNamespace,
96
+ AQUAMAN_VAULT_MOUNT_PATH: config.vaultMountPath,
97
+ AQUAMAN_1PASSWORD_VAULT: config.onePasswordVault,
98
+ AQUAMAN_1PASSWORD_ACCOUNT: config.onePasswordAccount
99
+ }
100
+ });
101
+
102
+ let stdout = '';
103
+ let stderr = '';
104
+
105
+ this.process.stdout?.on('data', (data) => {
106
+ stdout += data.toString();
107
+
108
+ // Try to parse connection info from first line
109
+ const firstLine = stdout.split('\n')[0];
110
+ if (firstLine && !this.connectionInfo) {
111
+ try {
112
+ const info = JSON.parse(firstLine) as ProxyConnectionInfo;
113
+ if (info.ready) {
114
+ this.connectionInfo = info;
115
+ this.options.onReady?.(info);
116
+ resolve(info);
117
+ }
118
+ } catch {
119
+ // Not JSON yet, keep buffering
120
+ }
121
+ }
122
+ });
123
+
124
+ this.process.stderr?.on('data', (data) => {
125
+ stderr += data.toString();
126
+ console.error('[aquaman-proxy]', data.toString().trim());
127
+ });
128
+
129
+ this.process.on('error', (error) => {
130
+ this.options.onError?.(error);
131
+ reject(error);
132
+ });
133
+
134
+ this.process.on('exit', (code) => {
135
+ this.process = null;
136
+ this.connectionInfo = null;
137
+ this.options.onExit?.(code);
138
+
139
+ if (!this.connectionInfo) {
140
+ const error = new Error(`Proxy exited with code ${code}: ${stderr || stdout}`);
141
+ reject(error);
142
+ }
143
+ });
144
+
145
+ // Timeout after 10 seconds
146
+ setTimeout(() => {
147
+ if (!this.connectionInfo) {
148
+ const error = new Error('Proxy startup timeout');
149
+ this.stop();
150
+ reject(error);
151
+ }
152
+ }, 10000);
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Stop the proxy process
158
+ */
159
+ async stop(): Promise<void> {
160
+ if (!this.process) {
161
+ return;
162
+ }
163
+
164
+ return new Promise((resolve) => {
165
+ const proc = this.process!;
166
+
167
+ const timeout = setTimeout(() => {
168
+ proc.kill('SIGKILL');
169
+ }, 5000);
170
+
171
+ proc.on('exit', () => {
172
+ clearTimeout(timeout);
173
+ this.process = null;
174
+ this.connectionInfo = null;
175
+ resolve();
176
+ });
177
+
178
+ proc.kill('SIGTERM');
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Check if proxy is running
184
+ */
185
+ isRunning(): boolean {
186
+ return this.process !== null && this.connectionInfo !== null;
187
+ }
188
+
189
+ /**
190
+ * Get connection info
191
+ */
192
+ getConnectionInfo(): ProxyConnectionInfo | null {
193
+ return this.connectionInfo;
194
+ }
195
+
196
+ /**
197
+ * Get base URL for a service
198
+ */
199
+ getServiceUrl(service: string): string | null {
200
+ if (!this.connectionInfo) {
201
+ return null;
202
+ }
203
+ return `${this.connectionInfo.baseUrl}/${service}`;
204
+ }
205
+
206
+ /**
207
+ * Health check
208
+ */
209
+ async healthCheck(): Promise<boolean> {
210
+ if (!this.connectionInfo) {
211
+ return false;
212
+ }
213
+
214
+ try {
215
+ const response = await fetch(`${this.connectionInfo.baseUrl}/health`, {
216
+ method: 'GET',
217
+ signal: AbortSignal.timeout(5000)
218
+ });
219
+ return response.ok;
220
+ } catch {
221
+ return false;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Find the aquaman binary
227
+ */
228
+ private findAquamanBinary(): string | null {
229
+ // Check common locations
230
+ const locations = [
231
+ // In node_modules
232
+ path.join(process.cwd(), 'node_modules', '.bin', 'aquaman'),
233
+ path.join(process.cwd(), 'node_modules', '@aquaman', 'proxy', 'dist', 'cli', 'index.js'),
234
+
235
+ // Global install
236
+ '/usr/local/bin/aquaman',
237
+
238
+ // In PATH (will use which in spawn)
239
+ 'aquaman'
240
+ ];
241
+
242
+ for (const loc of locations) {
243
+ if (loc === 'aquaman') {
244
+ // Check if in PATH
245
+ try {
246
+ const { execSync } = require('child_process');
247
+ execSync('which aquaman', { stdio: 'ignore' });
248
+ return 'aquaman';
249
+ } catch {
250
+ continue;
251
+ }
252
+ }
253
+
254
+ if (fs.existsSync(loc)) {
255
+ return loc;
256
+ }
257
+ }
258
+
259
+ return null;
260
+ }
261
+ }
262
+
263
+ export function createProxyManager(options: ProxyManagerOptions): ProxyManager {
264
+ return new ProxyManager(options);
265
+ }