agentgate-mcp 0.2.0 → 0.3.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.
- package/ARCHITECTURE.md +18 -34
- package/MCP_TOOLS.md +50 -26
- package/README.md +54 -75
- package/package.json +1 -3
- package/src/browser-session.js +230 -0
- package/src/cli.js +14 -45
- package/src/config.js +1 -9
- package/src/mcp-server.js +136 -67
- package/src/orchestrator.js +54 -67
- package/services/_template.service.json +0 -34
- package/src/browser-runtime.js +0 -411
- package/src/integrations/captcha-solver.js +0 -128
- package/src/integrations/gmail-watcher.js +0 -129
- package/src/playwright-engine.js +0 -391
- package/src/registry.js +0 -47
- package/src/scaffold.js +0 -103
- package/src/setup.js +0 -109
- package/src/signup-engine.js +0 -24
- package/src/vault.js +0 -105
package/src/browser-runtime.js
DELETED
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
import { createLogger } from './logger.js';
|
|
2
|
-
|
|
3
|
-
const log = createLogger('browser-runtime');
|
|
4
|
-
|
|
5
|
-
function getByPath(obj, path) {
|
|
6
|
-
const parts = path.split('.');
|
|
7
|
-
let cursor = obj;
|
|
8
|
-
for (const part of parts) {
|
|
9
|
-
if (cursor == null) {
|
|
10
|
-
return undefined;
|
|
11
|
-
}
|
|
12
|
-
cursor = cursor[part];
|
|
13
|
-
}
|
|
14
|
-
return cursor;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function setByPath(obj, path, value) {
|
|
18
|
-
const parts = path.split('.');
|
|
19
|
-
let cursor = obj;
|
|
20
|
-
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
21
|
-
const key = parts[i];
|
|
22
|
-
if (typeof cursor[key] !== 'object' || cursor[key] === null) {
|
|
23
|
-
cursor[key] = {};
|
|
24
|
-
}
|
|
25
|
-
cursor = cursor[key];
|
|
26
|
-
}
|
|
27
|
-
cursor[parts[parts.length - 1]] = value;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function interpolate(template, state) {
|
|
31
|
-
if (typeof template !== 'string') {
|
|
32
|
-
return template;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_match, token) => {
|
|
36
|
-
const value = getByPath(state, token.trim());
|
|
37
|
-
return value == null ? '' : String(value);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function coerceTimeout(value, fallback = 15_000) {
|
|
42
|
-
const n = Number(value);
|
|
43
|
-
return Number.isFinite(n) ? n : fallback;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function withRetry(fn, { retries = 0, delayMs = 1_000, label = '' } = {}) {
|
|
47
|
-
let lastError;
|
|
48
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
49
|
-
try {
|
|
50
|
-
return await fn();
|
|
51
|
-
} catch (error) {
|
|
52
|
-
lastError = error;
|
|
53
|
-
if (attempt < retries) {
|
|
54
|
-
log.warn(`Retry ${attempt + 1}/${retries} for ${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
throw lastError;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export class BrowserRuntime {
|
|
63
|
-
constructor({ page, state, db, gmailWatcher, captchaSolver }) {
|
|
64
|
-
this.page = page;
|
|
65
|
-
this.state = state;
|
|
66
|
-
this.db = db;
|
|
67
|
-
this.gmailWatcher = gmailWatcher;
|
|
68
|
-
this.captchaSolver = captchaSolver;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async run(actions) {
|
|
72
|
-
for (const [index, action] of actions.entries()) {
|
|
73
|
-
if (!action || typeof action !== 'object') {
|
|
74
|
-
throw new Error(`Invalid action at index ${index}`);
|
|
75
|
-
}
|
|
76
|
-
log.debug(`Action ${index}: ${action.type}`, { selector: action.selector, url: action.url });
|
|
77
|
-
|
|
78
|
-
const retries = Number(action.retries) || 0;
|
|
79
|
-
const retryDelay = Number(action.retryDelayMs) || 1_000;
|
|
80
|
-
|
|
81
|
-
await withRetry(() => this.execute(action, index), {
|
|
82
|
-
retries,
|
|
83
|
-
delayMs: retryDelay,
|
|
84
|
-
label: `${action.type}@${index}`
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async execute(action, index) {
|
|
90
|
-
const type = action.type;
|
|
91
|
-
|
|
92
|
-
switch (type) {
|
|
93
|
-
case 'goto':
|
|
94
|
-
await this.page.goto(interpolate(action.url, this.state), {
|
|
95
|
-
waitUntil: action.waitUntil || 'domcontentloaded',
|
|
96
|
-
timeout: coerceTimeout(action.timeoutMs)
|
|
97
|
-
});
|
|
98
|
-
break;
|
|
99
|
-
|
|
100
|
-
case 'click':
|
|
101
|
-
await this.page.click(interpolate(action.selector, this.state), {
|
|
102
|
-
timeout: coerceTimeout(action.timeoutMs)
|
|
103
|
-
});
|
|
104
|
-
break;
|
|
105
|
-
|
|
106
|
-
case 'fill':
|
|
107
|
-
await this.page.fill(
|
|
108
|
-
interpolate(action.selector, this.state),
|
|
109
|
-
interpolate(action.value, this.state),
|
|
110
|
-
{ timeout: coerceTimeout(action.timeoutMs) }
|
|
111
|
-
);
|
|
112
|
-
break;
|
|
113
|
-
|
|
114
|
-
case 'select':
|
|
115
|
-
await this.page.selectOption(
|
|
116
|
-
interpolate(action.selector, this.state),
|
|
117
|
-
interpolate(action.value, this.state),
|
|
118
|
-
{ timeout: coerceTimeout(action.timeoutMs) }
|
|
119
|
-
);
|
|
120
|
-
break;
|
|
121
|
-
|
|
122
|
-
case 'press':
|
|
123
|
-
await this.page.press(
|
|
124
|
-
interpolate(action.selector, this.state),
|
|
125
|
-
interpolate(action.key, this.state),
|
|
126
|
-
{ timeout: coerceTimeout(action.timeoutMs) }
|
|
127
|
-
);
|
|
128
|
-
break;
|
|
129
|
-
|
|
130
|
-
case 'wait_for':
|
|
131
|
-
await this.page.waitForSelector(interpolate(action.selector, this.state), {
|
|
132
|
-
timeout: coerceTimeout(action.timeoutMs),
|
|
133
|
-
state: action.state || 'visible'
|
|
134
|
-
});
|
|
135
|
-
break;
|
|
136
|
-
|
|
137
|
-
case 'wait_for_navigation':
|
|
138
|
-
await this.page.waitForNavigation({
|
|
139
|
-
waitUntil: action.waitUntil || 'domcontentloaded',
|
|
140
|
-
timeout: coerceTimeout(action.timeoutMs)
|
|
141
|
-
});
|
|
142
|
-
break;
|
|
143
|
-
|
|
144
|
-
case 'sleep':
|
|
145
|
-
await this.page.waitForTimeout(coerceTimeout(action.ms, 500));
|
|
146
|
-
break;
|
|
147
|
-
|
|
148
|
-
case 'screenshot': {
|
|
149
|
-
const screenshotPath = interpolate(action.path, this.state) || '/tmp/agentgate-screenshot.png';
|
|
150
|
-
await this.page.screenshot({ path: screenshotPath, fullPage: Boolean(action.fullPage) });
|
|
151
|
-
log.info(`Screenshot saved to ${screenshotPath}`);
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
case 'extract_text': {
|
|
156
|
-
const target = this.resolveTarget(action);
|
|
157
|
-
const text = await target.textContent(
|
|
158
|
-
interpolate(action.selector, this.state),
|
|
159
|
-
{ timeout: coerceTimeout(action.timeoutMs) }
|
|
160
|
-
);
|
|
161
|
-
setByPath(this.state, action.target, (text || '').trim());
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
case 'extract_value': {
|
|
166
|
-
const target = this.resolveTarget(action);
|
|
167
|
-
const value = await target.inputValue(
|
|
168
|
-
interpolate(action.selector, this.state),
|
|
169
|
-
{ timeout: coerceTimeout(action.timeoutMs) }
|
|
170
|
-
);
|
|
171
|
-
setByPath(this.state, action.target, value);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
case 'extract_attribute': {
|
|
176
|
-
const target = this.resolveTarget(action);
|
|
177
|
-
const value = await target.getAttribute(
|
|
178
|
-
interpolate(action.selector, this.state),
|
|
179
|
-
interpolate(action.attribute, this.state),
|
|
180
|
-
{ timeout: coerceTimeout(action.timeoutMs) }
|
|
181
|
-
);
|
|
182
|
-
setByPath(this.state, action.target, value || '');
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
case 'regex_extract': {
|
|
187
|
-
const source = getByPath(this.state, action.source);
|
|
188
|
-
const pattern = new RegExp(action.pattern, action.flags || '');
|
|
189
|
-
const match = String(source || '').match(pattern);
|
|
190
|
-
if (!match) {
|
|
191
|
-
throw new Error(`regex_extract failed at action ${index}: pattern did not match`);
|
|
192
|
-
}
|
|
193
|
-
const group = Number.isInteger(action.group) ? action.group : 1;
|
|
194
|
-
setByPath(this.state, action.target, match[group] || '');
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
case 'set_var':
|
|
199
|
-
setByPath(this.state, action.target, interpolate(action.value, this.state));
|
|
200
|
-
break;
|
|
201
|
-
|
|
202
|
-
case 'assert_present': {
|
|
203
|
-
const value = getByPath(this.state, action.path);
|
|
204
|
-
if (value == null || value === '') {
|
|
205
|
-
throw new Error(action.message || `Missing required state value: ${action.path}`);
|
|
206
|
-
}
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
case 'store_alias': {
|
|
211
|
-
const emailAlias = getByPath(this.state, action.emailPath || 'generated.emailAlias');
|
|
212
|
-
const password = getByPath(this.state, action.passwordPath || 'generated.password');
|
|
213
|
-
if (!emailAlias) {
|
|
214
|
-
throw new Error('store_alias action missing email alias in state');
|
|
215
|
-
}
|
|
216
|
-
this.db.saveAlias({
|
|
217
|
-
service: this.state.service.name,
|
|
218
|
-
emailAlias,
|
|
219
|
-
passwordHint: password ? `${String(password).slice(0, 2)}...${String(password).slice(-2)}` : null
|
|
220
|
-
});
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
case 'wait_for_email_code': {
|
|
225
|
-
if (!this.gmailWatcher) {
|
|
226
|
-
throw new Error('wait_for_email_code requested but Gmail watcher is not configured');
|
|
227
|
-
}
|
|
228
|
-
const result = await this.gmailWatcher.waitForCode({
|
|
229
|
-
serviceName: this.state.service.name,
|
|
230
|
-
timeoutMs: coerceTimeout(action.timeoutMs, 120_000),
|
|
231
|
-
pollMs: coerceTimeout(action.pollMs, 5_000),
|
|
232
|
-
regex: action.regex || '(\\d{6})'
|
|
233
|
-
});
|
|
234
|
-
setByPath(this.state, action.target || 'scratch.emailCode', result.code);
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
case 'solve_captcha': {
|
|
239
|
-
if (!this.captchaSolver) {
|
|
240
|
-
throw new Error('solve_captcha requested but CAPTCHA solver is not configured');
|
|
241
|
-
}
|
|
242
|
-
const solved = await this.captchaSolver.solve({
|
|
243
|
-
serviceName: this.state.service.name,
|
|
244
|
-
provider: action.provider || 'capsolver',
|
|
245
|
-
siteKey: action.siteKey,
|
|
246
|
-
siteUrl: action.siteUrl || this.page.url(),
|
|
247
|
-
captchaType: action.captchaType,
|
|
248
|
-
timeoutMs: coerceTimeout(action.timeoutMs, 120_000)
|
|
249
|
-
});
|
|
250
|
-
setByPath(this.state, action.target || 'scratch.captchaToken', solved.token);
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
case 'fill_card':
|
|
255
|
-
await this.fillCard(action);
|
|
256
|
-
break;
|
|
257
|
-
|
|
258
|
-
case 'switch_to_iframe': {
|
|
259
|
-
const frameSelector = interpolate(action.selector, this.state);
|
|
260
|
-
const frameElement = await this.page.waitForSelector(frameSelector, {
|
|
261
|
-
timeout: coerceTimeout(action.timeoutMs),
|
|
262
|
-
state: 'attached'
|
|
263
|
-
});
|
|
264
|
-
const frame = await frameElement.contentFrame();
|
|
265
|
-
if (!frame) {
|
|
266
|
-
throw new Error(`No iframe found at selector: ${frameSelector}`);
|
|
267
|
-
}
|
|
268
|
-
this.state._iframeStack = this.state._iframeStack || [];
|
|
269
|
-
this.state._iframeStack.push(this.page);
|
|
270
|
-
this.page = frame;
|
|
271
|
-
log.debug(`Switched to iframe: ${frameSelector}`);
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
case 'switch_to_parent': {
|
|
276
|
-
const stack = this.state._iframeStack;
|
|
277
|
-
if (!stack || stack.length === 0) {
|
|
278
|
-
throw new Error('switch_to_parent: no parent frame to switch to');
|
|
279
|
-
}
|
|
280
|
-
this.page = stack.pop();
|
|
281
|
-
log.debug('Switched to parent frame');
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
default:
|
|
286
|
-
throw new Error(`Unsupported workflow action type: ${type}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Returns the page or iframe to operate on.
|
|
292
|
-
* If the action specifies an `iframe` selector, resolves into that frame first.
|
|
293
|
-
*/
|
|
294
|
-
resolveTarget(action) {
|
|
295
|
-
return this.page;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async fillCard(action) {
|
|
299
|
-
const billing = this.state.vault?.billing || {};
|
|
300
|
-
const required = ['cardNumber', 'cardExpMonth', 'cardExpYear', 'cardCvc'];
|
|
301
|
-
for (const field of required) {
|
|
302
|
-
if (!billing[field]) {
|
|
303
|
-
throw new Error(`fill_card requires billing.${field} in vault`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Detect Stripe iframe and switch context if needed
|
|
308
|
-
let target = this.page;
|
|
309
|
-
let switchedToIframe = false;
|
|
310
|
-
|
|
311
|
-
const stripeIframeSelectors = [
|
|
312
|
-
"iframe[name*='__privateStripeFrame']",
|
|
313
|
-
"iframe[src*='stripe.com']",
|
|
314
|
-
"iframe[title*='card']",
|
|
315
|
-
"iframe[name*='__stripe']"
|
|
316
|
-
];
|
|
317
|
-
|
|
318
|
-
const paddleIframeSelectors = [
|
|
319
|
-
"iframe[src*='paddle.com']",
|
|
320
|
-
"iframe[class*='paddle']"
|
|
321
|
-
];
|
|
322
|
-
|
|
323
|
-
const allIframeSelectors = [...stripeIframeSelectors, ...paddleIframeSelectors];
|
|
324
|
-
|
|
325
|
-
for (const iframeSelector of allIframeSelectors) {
|
|
326
|
-
try {
|
|
327
|
-
const frameElement = await this.page.waitForSelector(iframeSelector, {
|
|
328
|
-
timeout: 2_000,
|
|
329
|
-
state: 'attached'
|
|
330
|
-
});
|
|
331
|
-
const frame = await frameElement.contentFrame();
|
|
332
|
-
if (frame) {
|
|
333
|
-
target = frame;
|
|
334
|
-
switchedToIframe = true;
|
|
335
|
-
log.info(`Detected payment iframe: ${iframeSelector}`);
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
} catch {
|
|
339
|
-
// No such iframe, try next
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const selectors = {
|
|
344
|
-
cardNumber: [
|
|
345
|
-
"input[name='cardnumber']",
|
|
346
|
-
"input[autocomplete='cc-number']",
|
|
347
|
-
"input[id*='card'][id*='number']",
|
|
348
|
-
"input[data-elements-stable-field-name='cardNumber']",
|
|
349
|
-
"input[name='number']"
|
|
350
|
-
],
|
|
351
|
-
cardExp: [
|
|
352
|
-
"input[name='exp-date']",
|
|
353
|
-
"input[autocomplete='cc-exp']",
|
|
354
|
-
"input[id*='exp']",
|
|
355
|
-
"input[data-elements-stable-field-name='cardExpiry']",
|
|
356
|
-
"input[name='expiry']"
|
|
357
|
-
],
|
|
358
|
-
cardCvc: [
|
|
359
|
-
"input[name='cvc']",
|
|
360
|
-
"input[autocomplete='cc-csc']",
|
|
361
|
-
"input[id*='cvc']",
|
|
362
|
-
"input[data-elements-stable-field-name='cardCvc']",
|
|
363
|
-
"input[name='verification_value']"
|
|
364
|
-
],
|
|
365
|
-
cardName: [
|
|
366
|
-
"input[name='name']",
|
|
367
|
-
"input[autocomplete='cc-name']",
|
|
368
|
-
"input[id*='cardholder']"
|
|
369
|
-
],
|
|
370
|
-
cardZip: [
|
|
371
|
-
"input[name='postal']",
|
|
372
|
-
"input[autocomplete='postal-code']",
|
|
373
|
-
"input[name='postalCode']"
|
|
374
|
-
]
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
await this.fillFirstAvailable(target, selectors.cardNumber, billing.cardNumber, action.timeoutMs);
|
|
378
|
-
await this.fillFirstAvailable(
|
|
379
|
-
target,
|
|
380
|
-
selectors.cardExp,
|
|
381
|
-
`${billing.cardExpMonth}/${String(billing.cardExpYear).slice(-2)}`,
|
|
382
|
-
action.timeoutMs
|
|
383
|
-
);
|
|
384
|
-
await this.fillFirstAvailable(target, selectors.cardCvc, billing.cardCvc, action.timeoutMs);
|
|
385
|
-
|
|
386
|
-
if (billing.cardHolder) {
|
|
387
|
-
await this.fillFirstAvailable(target, selectors.cardName, billing.cardHolder, action.timeoutMs, true);
|
|
388
|
-
}
|
|
389
|
-
if (billing.cardZip) {
|
|
390
|
-
await this.fillFirstAvailable(target, selectors.cardZip, billing.cardZip, action.timeoutMs, true);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
async fillFirstAvailable(target, selectors, value, timeoutMs, optional = false) {
|
|
395
|
-
for (const selector of selectors) {
|
|
396
|
-
try {
|
|
397
|
-
await target.waitForSelector(selector, {
|
|
398
|
-
timeout: coerceTimeout(timeoutMs, 1_000),
|
|
399
|
-
state: 'visible'
|
|
400
|
-
});
|
|
401
|
-
await target.fill(selector, String(value));
|
|
402
|
-
return;
|
|
403
|
-
} catch {
|
|
404
|
-
// Try next selector.
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (!optional) {
|
|
408
|
-
throw new Error(`Could not find any card field selector: ${selectors.join(', ')}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import https from 'node:https';
|
|
2
|
-
import { createLogger } from '../logger.js';
|
|
3
|
-
|
|
4
|
-
const log = createLogger('captcha-solver');
|
|
5
|
-
|
|
6
|
-
const CAPSOLVER_API = 'https://api.capsolver.com';
|
|
7
|
-
|
|
8
|
-
function sleep(ms) {
|
|
9
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function postJson(url, body) {
|
|
13
|
-
return new Promise((resolve, reject) => {
|
|
14
|
-
const data = JSON.stringify(body);
|
|
15
|
-
const parsed = new URL(url);
|
|
16
|
-
const req = https.request(
|
|
17
|
-
{
|
|
18
|
-
hostname: parsed.hostname,
|
|
19
|
-
path: parsed.pathname,
|
|
20
|
-
method: 'POST',
|
|
21
|
-
headers: {
|
|
22
|
-
'Content-Type': 'application/json',
|
|
23
|
-
'Content-Length': Buffer.byteLength(data)
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
(res) => {
|
|
27
|
-
const chunks = [];
|
|
28
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
29
|
-
res.on('end', () => {
|
|
30
|
-
const text = Buffer.concat(chunks).toString('utf8');
|
|
31
|
-
try {
|
|
32
|
-
resolve(JSON.parse(text));
|
|
33
|
-
} catch {
|
|
34
|
-
reject(new Error(`Invalid JSON from CapSolver: ${text.slice(0, 200)}`));
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
req.on('error', reject);
|
|
40
|
-
req.write(data);
|
|
41
|
-
req.end();
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class CaptchaSolver {
|
|
46
|
-
constructor({ vault }) {
|
|
47
|
-
this.vault = vault;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async solve({ serviceName, provider = 'capsolver', siteKey, siteUrl, captchaType = 'ReCaptchaV2TaskProxyLess', timeoutMs = 120_000 }) {
|
|
51
|
-
// Mock mode for testing
|
|
52
|
-
const mockToken = process.env.AGENTGATE_CAPTCHA_MOCK_TOKEN;
|
|
53
|
-
if (mockToken) {
|
|
54
|
-
log.info(`Returning mock CAPTCHA token for ${serviceName}`);
|
|
55
|
-
return { provider, service: serviceName, token: mockToken, source: 'mock' };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const apiKey = process.env.CAPSOLVER_API_KEY;
|
|
59
|
-
if (!apiKey) {
|
|
60
|
-
throw new Error(
|
|
61
|
-
'CAPTCHA requested but no solver configured. Set CAPSOLVER_API_KEY or AGENTGATE_CAPTCHA_MOCK_TOKEN.'
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (provider !== 'capsolver') {
|
|
66
|
-
throw new Error(`Unsupported CAPTCHA provider: ${provider}. Only "capsolver" is supported.`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
log.info(`Creating CAPTCHA task for ${serviceName}`, { captchaType });
|
|
70
|
-
|
|
71
|
-
// Create task
|
|
72
|
-
const createResult = await postJson(`${CAPSOLVER_API}/createTask`, {
|
|
73
|
-
clientKey: apiKey,
|
|
74
|
-
task: {
|
|
75
|
-
type: captchaType,
|
|
76
|
-
websiteURL: siteUrl || `https://${serviceName}.com`,
|
|
77
|
-
websiteKey: siteKey || ''
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (createResult.errorId && createResult.errorId !== 0) {
|
|
82
|
-
throw new Error(`CapSolver createTask error: ${createResult.errorDescription || createResult.errorCode}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const taskId = createResult.taskId;
|
|
86
|
-
if (!taskId) {
|
|
87
|
-
throw new Error('CapSolver did not return a taskId');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
log.info(`CapSolver task created: ${taskId}`);
|
|
91
|
-
|
|
92
|
-
// Poll for result
|
|
93
|
-
const started = Date.now();
|
|
94
|
-
const pollMs = 3_000;
|
|
95
|
-
|
|
96
|
-
while (Date.now() - started < timeoutMs) {
|
|
97
|
-
await sleep(pollMs);
|
|
98
|
-
|
|
99
|
-
const result = await postJson(`${CAPSOLVER_API}/getTaskResult`, {
|
|
100
|
-
clientKey: apiKey,
|
|
101
|
-
taskId
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (result.errorId && result.errorId !== 0) {
|
|
105
|
-
throw new Error(`CapSolver error: ${result.errorDescription || result.errorCode}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (result.status === 'ready') {
|
|
109
|
-
const token =
|
|
110
|
-
result.solution?.gRecaptchaResponse ||
|
|
111
|
-
result.solution?.token ||
|
|
112
|
-
result.solution?.text ||
|
|
113
|
-
'';
|
|
114
|
-
|
|
115
|
-
if (!token) {
|
|
116
|
-
throw new Error('CapSolver returned ready status but no token in solution');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
log.info(`CAPTCHA solved for ${serviceName}`);
|
|
120
|
-
return { provider, service: serviceName, token, source: 'capsolver' };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
log.debug(`CapSolver task ${taskId} still processing...`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
throw new Error(`CAPTCHA solving timed out after ${timeoutMs}ms for ${serviceName}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import https from 'node:https';
|
|
2
|
-
import { createLogger } from '../logger.js';
|
|
3
|
-
|
|
4
|
-
const log = createLogger('gmail-watcher');
|
|
5
|
-
|
|
6
|
-
function sleep(ms) {
|
|
7
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function httpsRequest(url, options = {}) {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const req = https.request(url, options, (res) => {
|
|
13
|
-
const chunks = [];
|
|
14
|
-
res.on('data', (chunk) => chunks.push(chunk));
|
|
15
|
-
res.on('end', () => {
|
|
16
|
-
const body = Buffer.concat(chunks).toString('utf8');
|
|
17
|
-
if (res.statusCode >= 400) {
|
|
18
|
-
reject(new Error(`Gmail API ${res.statusCode}: ${body}`));
|
|
19
|
-
} else {
|
|
20
|
-
resolve(JSON.parse(body));
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
req.on('error', reject);
|
|
25
|
-
req.end();
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class GmailWatcher {
|
|
30
|
-
constructor({ vault }) {
|
|
31
|
-
this.vault = vault;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async waitForCode({ serviceName, timeoutMs = 120_000, pollMs = 5_000, regex = '(\\d{6})' }) {
|
|
35
|
-
// Mock mode: environment variable override for testing
|
|
36
|
-
const mockCode = process.env.AGENTGATE_GMAIL_MOCK_CODE;
|
|
37
|
-
if (mockCode) {
|
|
38
|
-
const compiled = new RegExp(regex);
|
|
39
|
-
const match = mockCode.match(compiled);
|
|
40
|
-
if (match && match[1]) {
|
|
41
|
-
log.info(`Returning mock email code for ${serviceName}`);
|
|
42
|
-
return { code: match[1], source: 'mock', service: serviceName };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const payload = this.vault.getPayload();
|
|
47
|
-
const token = payload.google?.gmailOAuthToken;
|
|
48
|
-
if (!token) {
|
|
49
|
-
throw new Error('Gmail OAuth token missing. Run `agentgate setup`.');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
log.info(`Polling Gmail for verification code from ${serviceName}`);
|
|
53
|
-
const started = Date.now();
|
|
54
|
-
const compiled = new RegExp(regex);
|
|
55
|
-
const searchAfter = Math.floor(started / 1000) - 60; // messages from last minute
|
|
56
|
-
|
|
57
|
-
while (Date.now() - started < timeoutMs) {
|
|
58
|
-
try {
|
|
59
|
-
const code = await this.pollOnce({ token, serviceName, compiled, searchAfter });
|
|
60
|
-
if (code) {
|
|
61
|
-
log.info(`Found verification code for ${serviceName}`);
|
|
62
|
-
return { code, source: 'gmail_api', service: serviceName };
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
log.warn(`Gmail poll error: ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
await sleep(pollMs);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
throw new Error(`Timed out waiting for verification email for ${serviceName}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async pollOnce({ token, serviceName, compiled, searchAfter }) {
|
|
75
|
-
const query = encodeURIComponent(`from:${serviceName} after:${searchAfter} is:unread`);
|
|
76
|
-
const listUrl = `https://gmail.googleapis.com/gmail/v1/users/me/messages?q=${query}&maxResults=5`;
|
|
77
|
-
|
|
78
|
-
const listResult = await httpsRequest(listUrl, {
|
|
79
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
if (!listResult.messages || listResult.messages.length === 0) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
for (const msg of listResult.messages) {
|
|
87
|
-
const msgUrl = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${msg.id}?format=full`;
|
|
88
|
-
const detail = await httpsRequest(msgUrl, {
|
|
89
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const body = this.extractBody(detail);
|
|
93
|
-
if (!body) continue;
|
|
94
|
-
|
|
95
|
-
const match = body.match(compiled);
|
|
96
|
-
if (match && match[1]) {
|
|
97
|
-
return match[1];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
extractBody(message) {
|
|
105
|
-
if (!message.payload) return null;
|
|
106
|
-
|
|
107
|
-
// Check snippet first (often contains the code)
|
|
108
|
-
if (message.snippet) {
|
|
109
|
-
return message.snippet;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Decode body parts
|
|
113
|
-
const parts = message.payload.parts || [message.payload];
|
|
114
|
-
for (const part of parts) {
|
|
115
|
-
if (part.mimeType === 'text/plain' && part.body?.data) {
|
|
116
|
-
return Buffer.from(part.body.data, 'base64url').toString('utf8');
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Fallback to HTML part
|
|
121
|
-
for (const part of parts) {
|
|
122
|
-
if (part.mimeType === 'text/html' && part.body?.data) {
|
|
123
|
-
return Buffer.from(part.body.data, 'base64url').toString('utf8');
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
}
|