@adcp/client 2.4.0 โ†’ 2.4.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,273 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Async webhook handler for AdCP CLI
5
+ *
6
+ * This module handles async/webhook responses by:
7
+ * 1. Starting a temporary HTTP server for webhooks
8
+ * 2. Using ngrok to expose the server publicly (if available)
9
+ * 3. Waiting for the async response
10
+ */
11
+
12
+ const http = require('http');
13
+ const { spawn } = require('child_process');
14
+ const { randomUUID } = require('crypto');
15
+
16
+ class AsyncWebhookHandler {
17
+ constructor(options = {}) {
18
+ this.port = options.port || 0; // 0 = random available port
19
+ this.timeout = options.timeout || 300000; // 5 minutes default
20
+ this.debug = options.debug || false;
21
+ this.server = null;
22
+ this.ngrokProcess = null;
23
+ this.webhookUrl = null;
24
+ this.operationId = randomUUID();
25
+ this.responsePromise = null;
26
+ this.responseResolver = null;
27
+ }
28
+
29
+ /**
30
+ * Check if ngrok is installed
31
+ */
32
+ static isNgrokAvailable() {
33
+ return new Promise((resolve) => {
34
+ const check = spawn('which', ['ngrok']);
35
+ check.on('close', (code) => resolve(code === 0));
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Start the webhook server and optionally ngrok tunnel
41
+ * @param {boolean} useNgrok - Whether to use ngrok (default: true)
42
+ */
43
+ async start(useNgrok = true) {
44
+ // Create the promise that will resolve when we get the webhook
45
+ this.responsePromise = new Promise((resolve, reject) => {
46
+ this.responseResolver = resolve;
47
+ this.responseRejector = reject;
48
+
49
+ // Set timeout
50
+ setTimeout(() => {
51
+ reject(new Error(`Webhook timeout after ${this.timeout}ms`));
52
+ }, this.timeout);
53
+ });
54
+
55
+ // Start HTTP server
56
+ await this.startServer();
57
+
58
+ if (useNgrok) {
59
+ // Start ngrok tunnel
60
+ const ngrokAvailable = await AsyncWebhookHandler.isNgrokAvailable();
61
+ if (ngrokAvailable) {
62
+ await this.startNgrok();
63
+ } else {
64
+ throw new Error('ngrok is not installed. Install it with: brew install ngrok (Mac) or download from https://ngrok.com');
65
+ }
66
+ } else {
67
+ // Use local URL (for local agents)
68
+ this.webhookUrl = `http://localhost:${this.port}`;
69
+
70
+ if (this.debug) {
71
+ console.error(`โœ… Local webhook server ready: ${this.webhookUrl}`);
72
+ }
73
+ }
74
+
75
+ return this.webhookUrl;
76
+ }
77
+
78
+ /**
79
+ * Start the HTTP server
80
+ */
81
+ startServer() {
82
+ return new Promise((resolve, reject) => {
83
+ this.server = http.createServer((req, res) => {
84
+ if (req.method === 'POST') {
85
+ let body = '';
86
+
87
+ req.on('data', chunk => {
88
+ body += chunk.toString();
89
+ });
90
+
91
+ req.on('end', () => {
92
+ try {
93
+ const payload = JSON.parse(body);
94
+
95
+ if (this.debug) {
96
+ console.error('\n๐ŸŽฃ Webhook received:');
97
+ console.error(JSON.stringify(payload, null, 2));
98
+ }
99
+
100
+ // Send 202 Accepted response
101
+ res.writeHead(202, { 'Content-Type': 'application/json' });
102
+ res.end(JSON.stringify({ status: 'accepted' }));
103
+
104
+ // Resolve the promise with the webhook payload
105
+ if (this.responseResolver) {
106
+ this.responseResolver(payload);
107
+ }
108
+ } catch (error) {
109
+ if (this.debug) {
110
+ console.error('Error parsing webhook:', error);
111
+ }
112
+ res.writeHead(400, { 'Content-Type': 'application/json' });
113
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
114
+ }
115
+ });
116
+ } else {
117
+ // Health check endpoint
118
+ res.writeHead(200, { 'Content-Type': 'application/json' });
119
+ res.end(JSON.stringify({ status: 'ready', operation_id: this.operationId }));
120
+ }
121
+ });
122
+
123
+ this.server.listen(this.port, () => {
124
+ const address = this.server.address();
125
+ this.port = address.port;
126
+
127
+ if (this.debug) {
128
+ console.error(`โœ… Webhook server listening on port ${this.port}`);
129
+ }
130
+
131
+ resolve();
132
+ });
133
+
134
+ this.server.on('error', reject);
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Start ngrok tunnel
140
+ */
141
+ startNgrok() {
142
+ return new Promise((resolve, reject) => {
143
+ if (this.debug) {
144
+ console.error(`๐Ÿš‡ Starting ngrok tunnel for port ${this.port}...`);
145
+ }
146
+
147
+ // Start ngrok with JSON output for easier parsing
148
+ this.ngrokProcess = spawn('ngrok', ['http', String(this.port), '--log=stdout', '--log-format=json']);
149
+
150
+ let ngrokStarted = false;
151
+ let buffer = '';
152
+
153
+ this.ngrokProcess.stdout.on('data', (data) => {
154
+ buffer += data.toString();
155
+ const lines = buffer.split('\n');
156
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
157
+
158
+ for (const line of lines) {
159
+ if (!line.trim()) continue;
160
+
161
+ try {
162
+ const parsed = JSON.parse(line);
163
+
164
+ // Look for the tunnel URL in ngrok's JSON output
165
+ if (parsed.url && parsed.url.startsWith('https://')) {
166
+ this.webhookUrl = parsed.url;
167
+ ngrokStarted = true;
168
+
169
+ if (this.debug) {
170
+ console.error(`โœ… ngrok tunnel ready: ${this.webhookUrl}`);
171
+ }
172
+
173
+ resolve();
174
+ }
175
+ } catch (e) {
176
+ // Not JSON, might be plain text output
177
+ // Try to extract URL from plain text
178
+ const urlMatch = line.match(/https:\/\/[a-z0-9-]+\.ngrok(?:-free)?\.(?:app|io)/);
179
+ if (urlMatch && !ngrokStarted) {
180
+ this.webhookUrl = urlMatch[0];
181
+ ngrokStarted = true;
182
+
183
+ if (this.debug) {
184
+ console.error(`โœ… ngrok tunnel ready: ${this.webhookUrl}`);
185
+ }
186
+
187
+ resolve();
188
+ }
189
+ }
190
+ }
191
+ });
192
+
193
+ this.ngrokProcess.stderr.on('data', (data) => {
194
+ if (this.debug) {
195
+ console.error('ngrok stderr:', data.toString());
196
+ }
197
+ });
198
+
199
+ this.ngrokProcess.on('error', (error) => {
200
+ reject(new Error(`Failed to start ngrok: ${error.message}`));
201
+ });
202
+
203
+ this.ngrokProcess.on('close', (code) => {
204
+ if (!ngrokStarted && code !== 0) {
205
+ reject(new Error(`ngrok exited with code ${code}`));
206
+ }
207
+ });
208
+
209
+ // Timeout for ngrok startup
210
+ setTimeout(() => {
211
+ if (!ngrokStarted) {
212
+ reject(new Error('ngrok failed to start within 10 seconds'));
213
+ }
214
+ }, 10000);
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Wait for the webhook response
220
+ */
221
+ async waitForResponse() {
222
+ if (this.debug) {
223
+ console.error('\nโณ Waiting for async response...');
224
+ }
225
+
226
+ const startTime = Date.now();
227
+ const result = await this.responsePromise;
228
+ const duration = Date.now() - startTime;
229
+
230
+ if (this.debug) {
231
+ console.error(`โœ… Response received after ${(duration / 1000).toFixed(1)}s\n`);
232
+ }
233
+
234
+ return result;
235
+ }
236
+
237
+ /**
238
+ * Clean up resources
239
+ */
240
+ async cleanup() {
241
+ if (this.debug) {
242
+ console.error('๐Ÿงน Cleaning up...');
243
+ }
244
+
245
+ // Close HTTP server
246
+ if (this.server) {
247
+ await new Promise((resolve) => {
248
+ this.server.close(() => resolve());
249
+ });
250
+ }
251
+
252
+ // Kill ngrok process
253
+ if (this.ngrokProcess) {
254
+ this.ngrokProcess.kill();
255
+ }
256
+
257
+ if (this.debug) {
258
+ console.error('โœ… Cleanup complete');
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Get the webhook URL with operation ID
264
+ */
265
+ getWebhookUrl() {
266
+ if (!this.webhookUrl) {
267
+ throw new Error('Webhook server not started');
268
+ }
269
+ return `${this.webhookUrl}?operation_id=${this.operationId}`;
270
+ }
271
+ }
272
+
273
+ module.exports = { AsyncWebhookHandler };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcp/client",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "AdCP client library with protocol support for MCP and A2A, includes testing framework",
5
5
  "main": "dist/lib/index.js",
6
6
  "types": "dist/lib/index.d.ts",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "files": [
22
22
  "dist/lib/**/*",
23
- "bin/adcp.js",
23
+ "bin/**/*.js",
24
24
  "README.md",
25
25
  "LICENSE"
26
26
  ],