app-manager-edge-worker 1.3.2 → 1.3.3

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.3
205
+ - **Captcha Session Preservation & Asynchronous Image Resynchronization**:
206
+ - **Deterministic Error Differentiation**: Disentangled captcha validation rejections from business errors (e.g., invalid GSTIN). When captcha validation fails, the worker and backend preserve the browser page and interactive session alive, reset the session TTL timer, and never abort prematurely.
207
+ - **Expanded Captcha Failure Signatures**: Added broad pattern matching (`letters?`, `characters?`, `image below`, `shown below`, `security/verification code`, `invalid_captcha`) and DOM attribute inspection (`input[name='cap'].ng-invalid`, `[data-ng-bind*='ERR_CAPTCHA']`) to immediately classify captcha errors even when portals omit the word "captcha".
208
+ - **Asynchronous Captcha Settle Logic**: Introduced post-rejection settle delays (`waitForFunction` monitoring `img.src` change and AJAX completion) before taking the replacement captcha screenshot, preventing stale/expired captcha image captures.
209
+ - **Session TTL Auto-Refresh**: Automatically resets the session expiration timer on both edge worker and central backend whenever a captcha is retried or refreshed, granting users the full interaction window.
210
+ - **Clean Session Termination on Business Errors**: Genuine business rejections (such as invalid GSTIN or record not found) immediately purge browser contexts and trigger clean state cleanup across worker, backend, and UI test playground.
211
+
204
212
  ### v1.3.2
205
213
  - **Sub-Second Inline Error Resolution & Strict Outcome Race Protocol**:
206
214
  - **Dynamic In-Page Error Detection**: Expanded `defaultBroadErrorSelector` to capture generic inline portal validation elements (`.err`, `span.err`, `p.err`, `div.err`, `[class*='err-']`, `[data-ng-bind*='ERR']`, `[data-ng-show*='err']`, `[data-ng-show*='w_gst']`, `[role='alert']`), eliminating 15-second hangs when portal rejections insert error lines without page reload.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-manager-edge-worker",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
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.2";
4
+ let workerVersion = "1.3.3";
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.2";
23
+ let workerPkgVersion = "1.3.3";
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;
@@ -529,6 +529,13 @@ export async function handleInteractiveSessionRefresh(payload) {
529
529
  });
530
530
  const pageScreenshotBase64 = `data:image/jpeg;base64,${Buffer.from(pageScreenshotBuffer).toString("base64")}`;
531
531
 
532
+ // Reset auto-purge TTL timer to grant user full window to solve refreshed captcha
533
+ if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
534
+ const sessionTtlSeconds = cfg.sessionTimeoutSeconds || 180;
535
+ session.timeoutTimer = setTimeout(() => {
536
+ purgeSession(sessionId).catch(() => { });
537
+ }, sessionTtlSeconds * 1000);
538
+
532
539
  return {
533
540
  sessionId,
534
541
  imageBase64: `data:image/png;base64,${imageBase64}`,
@@ -587,7 +594,19 @@ export async function handleInteractiveSessionRefresh(payload) {
587
594
  .filter(Boolean)
588
595
  .join(", ");
589
596
 
590
- const invalidPattern = cfg.errorDetection?.invalidCaptchaPattern || "captcha.*(incorrect|invalid|wrong|mismatch|enter text)";
597
+ const defaultCaptchaPattern =
598
+ "captcha.*(incorrect|invalid|wrong|mismatch|enter|letters?|text|try again)|" +
599
+ "(invalid|incorrect|wrong|mismatch).*captcha|" +
600
+ "enter\\s*valid\\s*(letters?|characters?|code)|" +
601
+ "letters?\\s*shown\\s*(below|in\\s*the\\s*image)|" +
602
+ "characters?\\s*shown|" +
603
+ "invalid_captcha|" +
604
+ "verification\\s*code.*(invalid|wrong|incorrect)|" +
605
+ "security\\s*code.*(invalid|wrong|incorrect)";
606
+
607
+ const invalidPattern = cfg.errorDetection?.invalidCaptchaPattern
608
+ ? `${cfg.errorDetection.invalidCaptchaPattern}|${defaultCaptchaPattern}`
609
+ : defaultCaptchaPattern;
591
610
 
592
611
  // 1. Type captcha solution with robust candidate fallbacks & frame search
593
612
  const solveTimeout = Math.max(cfg.inputTimeoutMs || 0, 15000);
@@ -880,9 +899,38 @@ export async function handleInteractiveSessionRefresh(payload) {
880
899
  }
881
900
  }
882
901
 
902
+ // 5. DOM-level captcha error detection: check if captcha input or form has invalid captcha state
903
+ let hasDomCaptchaError = false;
904
+ try {
905
+ const capInput = document.querySelector("input[name='cap'], input[id*='captcha' i], input[name*='captcha' i], #fo-captcha");
906
+ if (capInput) {
907
+ const cls = capInput.className || "";
908
+ if (cls.includes("invalid_captcha") || cls.includes("invalid-captcha") || cls.includes("ng-invalid-invalid_captcha")) {
909
+ hasDomCaptchaError = true;
910
+ }
911
+ }
912
+ const form = document.querySelector("form");
913
+ if (form && !hasDomCaptchaError) {
914
+ const formCls = form.className || "";
915
+ if (formCls.includes("invalid_captcha") || formCls.includes("invalid-captcha") || formCls.includes("ng-invalid-invalid_captcha")) {
916
+ hasDomCaptchaError = true;
917
+ }
918
+ }
919
+ if (!hasDomCaptchaError) {
920
+ const capErrSpan = document.querySelector("[data-ng-bind*='ERR_CAPTCHA'], [ng-bind*='ERR_CAPTCHA'], [data-ng-if*='invalid_captcha'], [ng-if*='invalid_captcha']");
921
+ if (capErrSpan) {
922
+ const s = window.getComputedStyle(capErrSpan);
923
+ if (s.display !== "none" && s.visibility !== "hidden") {
924
+ hasDomCaptchaError = true;
925
+ }
926
+ }
927
+ }
928
+ } catch (_) { }
929
+
883
930
  return {
884
931
  matchedRuleId: fallbackRuleId,
885
932
  text: foundText,
933
+ hasDomCaptchaError,
886
934
  };
887
935
  }, customRules, combinedErrorSelectors);
888
936
 
@@ -892,13 +940,32 @@ export async function handleInteractiveSessionRefresh(payload) {
892
940
  errorText = (checkRulesAndText && checkRulesAndText.text) || "";
893
941
 
894
942
  const regex = new RegExp(invalidPattern, "i");
895
- const isCaptchaError = matchedRule
896
- ? matchedRule.action === "RETRY_CAPTCHA"
897
- : (Boolean(errorText) && regex.test(errorText));
943
+ const isCaptchaError =
944
+ Boolean(checkRulesAndText?.hasDomCaptchaError) ||
945
+ (matchedRule ? matchedRule.action === "RETRY_CAPTCHA" : false) ||
946
+ (Boolean(errorText) && regex.test(errorText));
898
947
 
899
- // Check if new captcha image was rendered on page
948
+ // If captcha error occurred, give target portal a brief window to load the refreshed captcha image
900
949
  const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
901
950
  let newImageBase64 = null;
951
+ if (isCaptchaError) {
952
+ try {
953
+ const initialSrc = await page.$eval(imageSelector, (el) => el.src || el.getAttribute("src")).catch(() => null);
954
+ await Promise.race([
955
+ page.waitForFunction(
956
+ (sel, old) => {
957
+ const el = document.querySelector(sel);
958
+ return el && (el.src || el.getAttribute("src")) !== old && el.complete;
959
+ },
960
+ { timeout: 1600 },
961
+ imageSelector,
962
+ initialSrc
963
+ ).catch(() => { }),
964
+ new Promise((r) => setTimeout(r, 650)),
965
+ ]);
966
+ } catch (_) { }
967
+ }
968
+
902
969
  try {
903
970
  const newCaptchaEl = await page.$(imageSelector);
904
971
  if (newCaptchaEl) {
@@ -936,8 +1003,9 @@ export async function handleInteractiveSessionRefresh(payload) {
936
1003
 
937
1004
  // Runtime non-inspection mode:
938
1005
  if (isCaptchaError) {
1006
+ // For captcha errors: NEVER abort session! Keep the browser session alive and reset TTL timer
939
1007
  if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
940
- const retryTtlSeconds = cfg.sessionTimeoutSeconds || 90;
1008
+ const retryTtlSeconds = cfg.sessionTimeoutSeconds || 180;
941
1009
  session.timeoutTimer = setTimeout(() => {
942
1010
  purgeSession(sessionId).catch(() => { });
943
1011
  }, retryTtlSeconds * 1000);
@@ -945,14 +1013,16 @@ export async function handleInteractiveSessionRefresh(payload) {
945
1013
  return {
946
1014
  status: "INVALID_CAPTCHA",
947
1015
  success: false,
948
- message: (matchedRule && matchedRule.customMessage) || errorText || "The captcha entered was incorrect.",
1016
+ message: (matchedRule && matchedRule.customMessage) || errorText || "The captcha entered was incorrect. Please retry with the updated image.",
949
1017
  canRetry: true,
950
1018
  newImageBase64,
951
1019
  errorCode: (matchedRule && matchedRule.id) || "INVALID_CAPTCHA",
1020
+ matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
952
1021
  };
953
1022
  }
954
1023
 
955
1024
  if (errorText || matchedRule) {
1025
+ // For business errors (e.g. invalid GSTIN, not found): abort and purge session immediately
956
1026
  await purgeSession(sessionId);
957
1027
  return {
958
1028
  status: "BUSINESS_ERROR",
@@ -960,6 +1030,7 @@ export async function handleInteractiveSessionRefresh(payload) {
960
1030
  errorCode: (matchedRule && matchedRule.id) || "BUSINESS_ERROR",
961
1031
  message: (matchedRule && matchedRule.customMessage) || errorText || "Target portal returned an error.",
962
1032
  canRetry: false,
1033
+ matchedRule: matchedRule ? { id: matchedRule.id, name: matchedRule.name, action: matchedRule.action } : null,
963
1034
  };
964
1035
  }
965
1036