app-manager-edge-worker 1.3.0 → 1.3.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.
package/README.md CHANGED
@@ -201,6 +201,14 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
201
201
 
202
202
  ---
203
203
 
204
+ ### v1.3.1
205
+ - **Robust Portal Input Resolution & Dynamic SPA Lifecycle Engine**:
206
+ - **Dynamic Multi-Selector Fallback Generation**: Form input selectors now automatically generate and evaluate candidate fallbacks (e.g. `input[name="for_gstin"]` expands to include `#for_gstin`, `[name="for_gstin"]`, `input#for_gstin`, `input[data-ng-model*="gstin"]`, and placeholder variants), resolving elements regardless of framework attribute formatting.
207
+ - **IFrame-Aware Element Discovery**: Introduced `locateElementInPageOrFrames` across `HITL_SESSION_INIT`, `HITL_SESSION_REFRESH`, and `HITL_SESSION_SOLVE`, querying the main page as well as all nested frames/iframes for inputs, captchas, and submit buttons.
208
+ - **Realistic Timeout Scaling**: Replaced fragile hardcoded timeouts (4s - 6s) with dynamic portal timeout scaling (`Math.max(timeoutMs, 20000)`), granting heavy client-side frameworks (AngularJS, React) sufficient headroom to download scripts and compile views.
209
+ - **AngularJS Scope & Model Synchronization**: Typed input values now dispatch full keyboard cascades (`input`, `change`, `keydown`, `keyup`, `blur`) and directly synchronize AngularJS `ngModel` (`$setViewValue`, `$render`, `$apply`) to trigger dynamic validation and captcha loaders (e.g. `showCap1`).
210
+ - **Actionable Diagnostic Logging**: Failure to locate an element now logs the browser's active URL and document title in terminal output with detailed context.
211
+
204
212
  ### v1.3.0
205
213
  - **Multi-Error Identifier Rules & Inspection Resilience Engine**:
206
214
  - **Custom Error Identifier Rules**: Supported typed error detection rules (`errorDetection.rules`) allowing classification of multiple distinct errors (e.g. `INVALID_GSTIN`, `INVALID_CAPTCHA`, `NOT_FOUND`) with tailored actions (`RETRY_CAPTCHA`, `BUSINESS_ERROR`, `ABORT_SESSION`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-manager-edge-worker",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Distributed Scraper Edge Worker for Apps Manager. Run on any local machine, VPS, or Termux Android device to serve browser and HTTP scraping jobs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/banner.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "module";
2
2
 
3
3
  const require = createRequire(import.meta.url);
4
- let workerVersion = "1.3.0";
4
+ let workerVersion = "1.3.1";
5
5
  try {
6
6
  const pkg = require("../package.json");
7
7
  if (pkg?.version) workerVersion = pkg.version;
package/src/client.js CHANGED
@@ -20,7 +20,7 @@ import fs from "fs";
20
20
  import { getWhatsAppHandler } from "./handlers/whatsappHandler.js";
21
21
  import { printBanner, logInfo, logSuccess, logWarn, logError, logJob } from "./banner.js";
22
22
 
23
- let workerPkgVersion = "1.3.0";
23
+ let workerPkgVersion = "1.3.1";
24
24
  try {
25
25
  const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
26
26
  if (pkg?.version) workerPkgVersion = pkg.version;
@@ -1,6 +1,7 @@
1
1
  import crypto from "crypto";
2
2
  import localBrowserManager from "../browser.js";
3
3
  import { runTransformPipeline, castValueToType } from "./transformUtils.js";
4
+ import { logInfo, logWarn, logError } from "../banner.js";
4
5
 
5
6
  /**
6
7
  * In-memory map of active interactive sessions on this Edge Worker.
@@ -93,6 +94,82 @@ async function purgeSession(sessionId) {
93
94
  activeSessions.delete(sessionId);
94
95
  }
95
96
 
97
+ /**
98
+ * Constructs a robust set of CSS selectors for an input field based on its configured selector and paramKey.
99
+ * Handles IDs, name attributes, ng-model attributes, placeholders, and comma-separated selectors.
100
+ */
101
+ function buildRobustInputSelectors(selector, paramKey) {
102
+ const set = new Set();
103
+ if (selector && typeof selector === "string") {
104
+ selector.split(",").map((s) => s.trim()).filter(Boolean).forEach((s) => set.add(s));
105
+
106
+ // If selector contains [name="xxx"] or [name='xxx'], extract the name
107
+ const nameMatch = selector.match(/\[name=["']?([^"'\]\s]+)["']?\]/i);
108
+ if (nameMatch && nameMatch[1]) {
109
+ const name = nameMatch[1];
110
+ set.add(`#${name}`);
111
+ set.add(`input[name="${name}"]`);
112
+ set.add(`[name="${name}"]`);
113
+ set.add(`input#${name}`);
114
+ }
115
+
116
+ // If selector contains #xxx or [id="xxx"], extract the id
117
+ const idMatch = selector.match(/#([a-zA-Z0-9_\-]+)/) || selector.match(/\[id=["']?([^"'\]\s]+)["']?\]/i);
118
+ if (idMatch && idMatch[1]) {
119
+ const id = idMatch[1];
120
+ set.add(`#${id}`);
121
+ set.add(`input[name="${id}"]`);
122
+ set.add(`[name="${id}"]`);
123
+ set.add(`input#${id}`);
124
+ }
125
+ }
126
+
127
+ if (paramKey && typeof paramKey === "string") {
128
+ const pk = paramKey.trim();
129
+ set.add(`#${pk}`);
130
+ set.add(`input[name="${pk}"]`);
131
+ set.add(`[name="${pk}"]`);
132
+ set.add(`input[data-ng-model*="${pk}"]`);
133
+ set.add(`input[ng-model*="${pk}"]`);
134
+ set.add(`input[placeholder*="${pk}" i]`);
135
+ }
136
+
137
+ return Array.from(set);
138
+ }
139
+
140
+ /**
141
+ * Searches for an element in the main page or any child iframe, waiting up to timeoutMs.
142
+ * Returns { target: page | frame, element, matchedSelector } or null.
143
+ */
144
+ async function locateElementInPageOrFrames(page, selectorList, timeoutMs = 15000) {
145
+ const combinedSelector = Array.isArray(selectorList) ? selectorList.join(", ") : String(selectorList);
146
+
147
+ // 1. Try waiting on the top page first
148
+ try {
149
+ const el = await page.waitForSelector(combinedSelector, { timeout: timeoutMs });
150
+ if (el) {
151
+ return { target: page, element: el, matchedSelector: combinedSelector };
152
+ }
153
+ } catch (_) {
154
+ // Top-level wait timed out or failed; inspect child frames next
155
+ }
156
+
157
+ // 2. Scan child frames/iframes if not found on top-level page
158
+ try {
159
+ const frames = page.frames();
160
+ for (const frame of frames) {
161
+ try {
162
+ const fEl = await frame.$(combinedSelector);
163
+ if (fEl) {
164
+ return { target: frame, element: fEl, matchedSelector: combinedSelector };
165
+ }
166
+ } catch (_) {}
167
+ }
168
+ } catch (_) {}
169
+
170
+ return null;
171
+ }
172
+
96
173
  /**
97
174
  * 1. Handles HITL_SESSION_INIT:
98
175
  * Launches page, pre-fills initial form inputs, captures captcha screenshot,
@@ -106,6 +183,7 @@ export async function handleInteractiveSessionInit(payload) {
106
183
  captchaConfig = {},
107
184
  preScrapeActions = [],
108
185
  authentication = null,
186
+ navigation = {},
109
187
  timeoutMs = 30000,
110
188
  fieldMappings = [],
111
189
  collections = [],
@@ -146,10 +224,13 @@ export async function handleInteractiveSessionInit(payload) {
146
224
  }
147
225
  }
148
226
 
149
- // Navigate to target portal
150
- await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs });
227
+ // Navigate to target portal with flexible waitUntil and realistic timeout
228
+ const navTimeout = navigation.timeoutMs || timeoutMs || 30000;
229
+ const navWaitUntil = navigation.waitUntil || "domcontentloaded";
230
+
231
+ await page.goto(url, { waitUntil: navWaitUntil, timeout: navTimeout });
151
232
  try {
152
- await page.waitForNetworkIdle({ idleTime: 500, timeout: 3500 }).catch(() => {});
233
+ await page.waitForNetworkIdle({ idleTime: 500, timeout: 4000 }).catch(() => {});
153
234
  } catch (_) {}
154
235
 
155
236
  // Execute pre-scrape actions if present
@@ -157,14 +238,15 @@ export async function handleInteractiveSessionInit(payload) {
157
238
  for (const action of preScrapeActions) {
158
239
  try {
159
240
  const actType = (action.type || action.actionType || "").toUpperCase();
241
+ const actionTimeout = Math.max(action.timeoutMs || 0, 10000);
160
242
  if (actType === "CLICK" && action.selector) {
161
- await page.waitForSelector(action.selector, { timeout: action.timeoutMs || 4000 });
243
+ await page.waitForSelector(action.selector, { timeout: actionTimeout });
162
244
  await page.click(action.selector);
163
245
  } else if (actType === "TYPE" && action.selector) {
164
- await page.waitForSelector(action.selector, { timeout: action.timeoutMs || 4000 });
246
+ await page.waitForSelector(action.selector, { timeout: actionTimeout });
165
247
  await page.type(action.selector, action.value || "", { delay: 30 });
166
248
  } else if (actType === "WAIT_FOR" && action.selector) {
167
- await page.waitForSelector(action.selector, { timeout: action.timeoutMs || 8000 });
249
+ await page.waitForSelector(action.selector, { timeout: actionTimeout });
168
250
  }
169
251
  if (action.delayMs > 0) {
170
252
  await new Promise((r) => setTimeout(r, Math.min(action.delayMs, 4000)));
@@ -175,47 +257,98 @@ export async function handleInteractiveSessionInit(payload) {
175
257
  }
176
258
  }
177
259
 
178
- // Fill Form Inputs (e.g. GSTIN, PAN, registration number)
260
+ // Fill Form Inputs (e.g. GSTIN, PAN, registration number) with robust selector fallbacks & frame support
179
261
  if (Array.isArray(formInputs) && formInputs.length > 0) {
180
262
  for (const input of formInputs) {
181
263
  if (!input.selector || input.value === undefined || input.value === null) continue;
182
- await page.waitForSelector(input.selector, { timeout: 6000 });
264
+
265
+ const inputTimeout = Math.max(input.timeoutMs || 0, timeoutMs || 30000, 20000);
266
+ const robustSelectors = buildRobustInputSelectors(input.selector, input.paramKey);
267
+
268
+ const located = await locateElementInPageOrFrames(page, robustSelectors, inputTimeout);
269
+ if (!located || !located.element) {
270
+ const currentUrl = page.url();
271
+ const pageTitle = (await page.title().catch(() => "")) || "Untitled";
272
+ logWarn(
273
+ `[HITL_SESSION_INIT] Input selector '${input.selector}' (or fallbacks: ${robustSelectors.join(", ")}) not found after ${inputTimeout}ms on page "${pageTitle}" (${currentUrl})`
274
+ );
275
+ throw new Error(
276
+ `Waiting for selector '${input.selector}' failed on page "${pageTitle}" (${currentUrl}) after ${inputTimeout}ms. Ensure portal is reachable and selector is correct.`
277
+ );
278
+ }
279
+
280
+ const { target, element, matchedSelector } = located;
183
281
 
184
282
  if (input.clearFirst !== false) {
185
- await page.click(input.selector, { clickCount: 3 }).catch(() => {});
283
+ await element.click({ clickCount: 3 }).catch(() => {});
186
284
  await page.keyboard.press("Backspace").catch(() => {});
187
285
  }
188
286
 
189
287
  if (input.actionType === "SELECT") {
190
- await page.select(input.selector, String(input.value));
288
+ await target.select(matchedSelector, String(input.value));
191
289
  } else {
192
- await page.type(input.selector, String(input.value), { delay: input.delayMs || 30 });
290
+ await element.type(String(input.value), { delay: input.delayMs || 30 });
193
291
  }
194
292
 
195
- // Explicitly dispatch input, change, and blur events to trigger AngularJS/React/portal validation listeners
196
- await page.evaluate((sel) => {
293
+ // Explicitly dispatch input, change, keydown, keyup, and blur events, plus sync AngularJS ngModel if present
294
+ await target.evaluate((sel) => {
197
295
  const el = document.querySelector(sel);
198
296
  if (el) {
199
297
  el.dispatchEvent(new Event("input", { bubbles: true }));
200
298
  el.dispatchEvent(new Event("change", { bubbles: true }));
299
+ el.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
300
+ el.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "Enter" }));
201
301
  el.dispatchEvent(new Event("blur", { bubbles: true }));
302
+
303
+ // AngularJS scope & ngModel synchronization
304
+ if (window.angular) {
305
+ try {
306
+ const ngEl = window.angular.element(el);
307
+ const ngModel = ngEl.controller("ngModel");
308
+ if (ngModel) {
309
+ ngModel.$setViewValue(el.value);
310
+ ngModel.$render();
311
+ ngModel.$commitViewValue();
312
+ }
313
+ const scope = ngEl.scope();
314
+ if (scope && !scope.$$phase) {
315
+ scope.$apply();
316
+ }
317
+ } catch (_) {}
318
+ }
202
319
  }
203
- }, input.selector).catch(() => {});
204
- await page.keyboard.press("Tab").catch(() => {});
320
+ }, matchedSelector).catch(() => {});
205
321
 
206
- await new Promise((r) => setTimeout(r, 250));
322
+ await page.keyboard.press("Tab").catch(() => {});
323
+ await new Promise((r) => setTimeout(r, 300));
207
324
  }
208
325
  }
209
326
 
210
- // Locate and Screenshot Captcha DOM Element
211
- const imageSelector = captchaConfig.imageSelector || "img[src*='captcha'], #imgCaptcha, #captchaImg";
212
- await page.waitForSelector(imageSelector, { timeout: 8000 });
213
-
214
- const captchaElement = await page.$(imageSelector);
215
- if (!captchaElement) {
216
- throw new Error(`Captcha element not found with selector: ${imageSelector}`);
327
+ // Locate and Screenshot Captcha DOM Element with robust candidate fallbacks
328
+ const imageTimeout = Math.max(captchaConfig.timeoutMs || 0, timeoutMs || 30000, 15000);
329
+ const candidateImageSelectors = [
330
+ captchaConfig.imageSelector,
331
+ "#imgCaptcha",
332
+ "#captchaImg",
333
+ "img[src*='captcha' i]",
334
+ "img[id*='captcha' i]",
335
+ "img[alt*='captcha' i]",
336
+ "canvas[id*='captcha' i]",
337
+ ".captcha-image img",
338
+ "#captcha_image",
339
+ ].filter(Boolean);
340
+
341
+ const locatedImage = await locateElementInPageOrFrames(page, candidateImageSelectors, imageTimeout);
342
+ if (!locatedImage || !locatedImage.element) {
343
+ const currentUrl = page.url();
344
+ const pageTitle = (await page.title().catch(() => "")) || "Untitled";
345
+ throw new Error(
346
+ `Captcha element not found with selector '${captchaConfig.imageSelector || candidateImageSelectors[0]}' on page "${pageTitle}" (${currentUrl}) after ${imageTimeout}ms`
347
+ );
217
348
  }
218
349
 
350
+ const captchaElement = locatedImage.element;
351
+
219
352
  // Capture screenshot of the exact visual captcha DOM node
220
353
  const screenshotBuffer = await captchaElement.screenshot({
221
354
  encoding: "binary",
@@ -291,19 +424,29 @@ export async function handleInteractiveSessionRefresh(payload) {
291
424
  const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
292
425
  const refreshSelector = cfg.refreshSelector;
293
426
 
427
+ const candidateImageSelectors = [
428
+ cfg.imageSelector,
429
+ "#imgCaptcha",
430
+ "#captchaImg",
431
+ "img[src*='captcha' i]",
432
+ "img[id*='captcha' i]",
433
+ ].filter(Boolean);
434
+
294
435
  // 1. Snapshot current image hash and src attribute
295
- const oldEl = await page.$(imageSelector);
296
- if (!oldEl) {
436
+ const locatedOldImg = await locateElementInPageOrFrames(page, candidateImageSelectors, 8000);
437
+ if (!locatedOldImg || !locatedOldImg.element) {
297
438
  throw new Error(`Captcha element not found with selector: ${imageSelector}`);
298
439
  }
440
+ const oldEl = locatedOldImg.element;
441
+ const effectiveImageSelector = locatedOldImg.matchedSelector;
299
442
 
300
443
  const oldBuffer = await oldEl.screenshot({ encoding: "binary" });
301
444
  const oldHash = crypto.createHash("md5").update(oldBuffer).digest("hex");
302
445
 
303
- const oldSrc = await page.evaluate((sel) => {
446
+ const oldSrc = await locatedOldImg.target.evaluate((sel) => {
304
447
  const el = document.querySelector(sel);
305
448
  return el ? el.getAttribute("src") : null;
306
- }, imageSelector);
449
+ }, effectiveImageSelector);
307
450
 
308
451
  // 2. Click the reload button with robust candidate fallbacks
309
452
  const candidateSelectors = [
@@ -328,9 +471,9 @@ export async function handleInteractiveSessionRefresh(payload) {
328
471
  let clicked = false;
329
472
  for (const sel of candidateSelectors) {
330
473
  try {
331
- const btn = await page.$(sel);
332
- if (btn) {
333
- await btn.click();
474
+ const locatedBtn = await locateElementInPageOrFrames(page, [sel], 500);
475
+ if (locatedBtn && locatedBtn.element) {
476
+ await locatedBtn.element.click();
334
477
  clicked = true;
335
478
  break;
336
479
  }
@@ -442,15 +585,70 @@ export async function handleInteractiveSessionSolve(payload) {
442
585
 
443
586
  const invalidPattern = cfg.errorDetection?.invalidCaptchaPattern || "captcha.*(incorrect|invalid|wrong|mismatch|enter text)";
444
587
 
445
- // 1. Type captcha solution
446
- await page.waitForSelector(inputSelector, { timeout: 4000 });
447
- await page.click(inputSelector, { clickCount: 3 }).catch(() => {});
588
+ // 1. Type captcha solution with robust candidate fallbacks & frame search
589
+ const solveTimeout = Math.max(cfg.inputTimeoutMs || 0, 15000);
590
+ const candidateInputSelectors = [
591
+ inputSelector,
592
+ "#fo-captcha",
593
+ "#captcha",
594
+ "input[name='cap']",
595
+ "input[name*='captcha' i]",
596
+ "input[id*='captcha' i]",
597
+ "input[placeholder*='captcha' i]",
598
+ ].filter(Boolean);
599
+
600
+ const locatedInput = await locateElementInPageOrFrames(page, candidateInputSelectors, solveTimeout);
601
+ if (!locatedInput || !locatedInput.element) {
602
+ throw new Error(`Captcha input field not found with selector: ${inputSelector}`);
603
+ }
604
+
605
+ await locatedInput.element.click({ clickCount: 3 }).catch(() => {});
448
606
  await page.keyboard.press("Backspace").catch(() => {});
449
- await page.type(inputSelector, String(captchaSolution).trim(), { delay: 35 });
607
+ await locatedInput.element.type(String(captchaSolution).trim(), { delay: 35 });
608
+
609
+ // Sync input events and AngularJS ngModel for captcha input
610
+ await locatedInput.target.evaluate((sel) => {
611
+ const el = document.querySelector(sel);
612
+ if (el) {
613
+ el.dispatchEvent(new Event("input", { bubbles: true }));
614
+ el.dispatchEvent(new Event("change", { bubbles: true }));
615
+ el.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
616
+ el.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "Enter" }));
617
+ el.dispatchEvent(new Event("blur", { bubbles: true }));
618
+
619
+ if (window.angular) {
620
+ try {
621
+ const ngEl = window.angular.element(el);
622
+ const ngModel = ngEl.controller("ngModel");
623
+ if (ngModel) {
624
+ ngModel.$setViewValue(el.value);
625
+ ngModel.$render();
626
+ ngModel.$commitViewValue();
627
+ }
628
+ const scope = ngEl.scope();
629
+ if (scope && !scope.$$phase) scope.$apply();
630
+ } catch (_) {}
631
+ }
632
+ }
633
+ }, locatedInput.matchedSelector).catch(() => {});
634
+
635
+ // 2. Click submit button with candidate fallbacks & frame search
636
+ const candidateSubmitSelectors = [
637
+ submitSelector,
638
+ "#lotsearch",
639
+ "button[type='submit']",
640
+ "input[type='submit']",
641
+ "button[id*='search' i]",
642
+ "button[id*='submit' i]",
643
+ "button.btn-primary",
644
+ ].filter(Boolean);
645
+
646
+ const locatedSubmit = await locateElementInPageOrFrames(page, candidateSubmitSelectors, solveTimeout);
647
+ if (!locatedSubmit || !locatedSubmit.element) {
648
+ throw new Error(`Submit button not found with selector: ${submitSelector}`);
649
+ }
450
650
 
451
- // 2. Click submit button
452
- await page.waitForSelector(submitSelector, { timeout: 4000 });
453
- await page.click(submitSelector);
651
+ await locatedSubmit.element.click();
454
652
 
455
653
  // 3. Post-Submit Race: Wait for Success Selector OR Error Selector
456
654
  // In inspection mode, use a shorter safety window (5-7s) so users aren't stalled waiting on empty selectors