@vulcn/driver-browser 0.1.2 → 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/dist/index.cjs CHANGED
@@ -25,11 +25,14 @@ __export(index_exports, {
25
25
  BrowserRunner: () => BrowserRunner,
26
26
  BrowserStepSchema: () => BrowserStepSchema,
27
27
  checkBrowsers: () => checkBrowsers,
28
+ checkSessionAlive: () => checkSessionAlive,
28
29
  configSchema: () => configSchema,
29
30
  crawlAndBuildSessions: () => crawlAndBuildSessions,
30
31
  default: () => index_default,
32
+ detectLoginForm: () => detectLoginForm,
31
33
  installBrowsers: () => installBrowsers,
32
- launchBrowser: () => launchBrowser
34
+ launchBrowser: () => launchBrowser,
35
+ performLogin: () => performLogin
33
36
  });
34
37
  module.exports = __toCommonJS(index_exports);
35
38
  var import_zod = require("zod");
@@ -401,9 +404,12 @@ var BrowserRecorder = class _BrowserRecorder {
401
404
  };
402
405
 
403
406
  // src/runner.ts
404
- var BrowserRunner = class _BrowserRunner {
407
+ var BrowserRunner = class {
405
408
  /**
406
- * Execute a session with security payloads
409
+ * Execute a session with security payloads.
410
+ *
411
+ * If ctx.options.browser is provided, reuses that browser (persistent mode).
412
+ * Otherwise, launches and closes its own browser (standalone mode).
407
413
  */
408
414
  static async execute(session, ctx) {
409
415
  const config = session.driverConfig;
@@ -429,90 +435,79 @@ var BrowserRunner = class _BrowserRunner {
429
435
  ]
430
436
  };
431
437
  }
432
- const { browser } = await launchBrowser({
433
- browser: browserType,
434
- headless
435
- });
436
- const context = await browser.newContext({ viewport });
438
+ const sharedBrowser = ctx.options.browser;
439
+ const ownBrowser = sharedBrowser ? null : (await launchBrowser({ browser: browserType, headless })).browser;
440
+ const browser = sharedBrowser ?? ownBrowser;
441
+ const storageState = ctx.options.storageState;
442
+ const contextOptions = { viewport };
443
+ if (storageState) {
444
+ contextOptions.storageState = JSON.parse(storageState);
445
+ }
446
+ const context = await browser.newContext(contextOptions);
437
447
  const page = await context.newPage();
448
+ await ctx.options.onPageReady?.(page);
438
449
  const eventFindings = [];
439
450
  let currentPayloadInfo = null;
440
- const dialogHandler = async (dialog) => {
441
- if (currentPayloadInfo) {
442
- const message = dialog.message();
443
- const dialogType = dialog.type();
444
- if (dialogType !== "beforeunload") {
445
- eventFindings.push({
446
- type: "xss",
447
- severity: "high",
448
- title: `XSS Confirmed - ${dialogType}() triggered`,
449
- description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
450
- stepId: currentPayloadInfo.stepId,
451
- payload: currentPayloadInfo.payloadValue,
452
- url: page.url(),
453
- evidence: `Dialog type: ${dialogType}, Message: ${message}`,
454
- metadata: {
455
- dialogType,
456
- dialogMessage: message,
457
- detectionMethod: "dialog"
458
- }
459
- });
460
- }
461
- }
462
- try {
463
- await dialog.dismiss();
464
- } catch {
465
- }
466
- };
467
- const consoleHandler = async (msg) => {
468
- if (currentPayloadInfo && msg.type() === "log") {
469
- const text = msg.text();
470
- if (text.includes("vulcn") || text.includes(currentPayloadInfo.payloadValue)) {
471
- eventFindings.push({
472
- type: "xss",
473
- severity: "high",
474
- title: "XSS Confirmed - Console Output",
475
- description: `JavaScript console.log was triggered by payload injection`,
476
- stepId: currentPayloadInfo.stepId,
477
- payload: currentPayloadInfo.payloadValue,
478
- url: page.url(),
479
- evidence: `Console output: ${text}`,
480
- metadata: {
481
- consoleType: msg.type(),
482
- detectionMethod: "console"
483
- }
484
- });
485
- }
486
- }
487
- };
451
+ const dialogHandler = createDialogHandler(
452
+ page,
453
+ eventFindings,
454
+ () => currentPayloadInfo
455
+ );
456
+ const consoleHandler = createConsoleHandler(
457
+ eventFindings,
458
+ () => currentPayloadInfo
459
+ );
488
460
  page.on("dialog", dialogHandler);
489
461
  page.on("console", consoleHandler);
490
462
  try {
491
463
  const injectableSteps = session.steps.filter(
492
464
  (step) => step.type === "browser.input" && step.injectable !== false
493
465
  );
494
- const allPayloads = [];
495
- for (const payloadSet of payloads) {
496
- for (const value of payloadSet.payloads) {
497
- allPayloads.push({ payloadSet, value });
498
- }
499
- }
466
+ const allPayloads = interleavePayloads(payloads);
467
+ const confirmedTypes = /* @__PURE__ */ new Set();
500
468
  for (const injectableStep of injectableSteps) {
469
+ let isFirstPayload = true;
470
+ let formPageUrl = null;
501
471
  for (const { payloadSet, value } of allPayloads) {
472
+ const stepTypeKey = `${injectableStep.id}::${payloadSet.category}`;
473
+ if (confirmedTypes.has(stepTypeKey)) {
474
+ continue;
475
+ }
502
476
  try {
503
477
  currentPayloadInfo = {
504
478
  stepId: injectableStep.id,
505
479
  payloadSet,
506
480
  payloadValue: value
507
481
  };
508
- await _BrowserRunner.replayWithPayload(
509
- page,
510
- session,
511
- injectableStep,
512
- value,
513
- startUrl
514
- );
515
- const reflectionFinding = await _BrowserRunner.checkReflection(
482
+ if (isFirstPayload) {
483
+ await replayWithPayload(
484
+ page,
485
+ session,
486
+ injectableStep,
487
+ value,
488
+ startUrl
489
+ );
490
+ isFirstPayload = false;
491
+ formPageUrl = startUrl;
492
+ } else {
493
+ const cycled = await cyclePayload(
494
+ page,
495
+ session,
496
+ injectableStep,
497
+ value,
498
+ formPageUrl ?? startUrl
499
+ );
500
+ if (!cycled) {
501
+ await replayWithPayload(
502
+ page,
503
+ session,
504
+ injectableStep,
505
+ value,
506
+ startUrl
507
+ );
508
+ }
509
+ }
510
+ const reflectionFinding = await checkReflection(
516
511
  page,
517
512
  injectableStep,
518
513
  payloadSet,
@@ -522,14 +517,23 @@ var BrowserRunner = class _BrowserRunner {
522
517
  if (reflectionFinding) {
523
518
  allFindings.push(reflectionFinding);
524
519
  }
520
+ const seenKeys = /* @__PURE__ */ new Set();
525
521
  for (const finding of allFindings) {
526
- ctx.addFinding(finding);
522
+ const dedupKey = `${finding.type}::${finding.stepId}::${finding.title}`;
523
+ if (!seenKeys.has(dedupKey)) {
524
+ seenKeys.add(dedupKey);
525
+ ctx.addFinding(finding);
526
+ }
527
+ }
528
+ if (allFindings.length > 0) {
529
+ confirmedTypes.add(stepTypeKey);
527
530
  }
528
531
  eventFindings.length = 0;
529
532
  payloadsTested++;
530
533
  ctx.options.onStepComplete?.(injectableStep.id, payloadsTested);
531
534
  } catch (err) {
532
535
  errors.push(`${injectableStep.id}: ${String(err)}`);
536
+ isFirstPayload = true;
533
537
  }
534
538
  }
535
539
  }
@@ -537,7 +541,11 @@ var BrowserRunner = class _BrowserRunner {
537
541
  page.off("dialog", dialogHandler);
538
542
  page.off("console", consoleHandler);
539
543
  currentPayloadInfo = null;
540
- await browser.close();
544
+ await ctx.options.onBeforeClose?.(page);
545
+ await context.close();
546
+ if (ownBrowser) {
547
+ await ownBrowser.close();
548
+ }
541
549
  }
542
550
  return {
543
551
  findings: ctx.findings,
@@ -547,136 +555,264 @@ var BrowserRunner = class _BrowserRunner {
547
555
  errors
548
556
  };
549
557
  }
550
- /**
551
- * Replay session steps with payload injected at target step
552
- *
553
- * IMPORTANT: We replay ALL steps, not just up to the injectable step.
554
- * The injection replaces the input value, but subsequent steps (like
555
- * clicking submit) must still execute so the payload reaches the server
556
- * and gets reflected back in the response.
557
- */
558
- static async replayWithPayload(page, session, targetStep, payloadValue, startUrl) {
559
- await page.goto(startUrl, { waitUntil: "domcontentloaded" });
560
- let injected = false;
561
- for (const step of session.steps) {
562
- const browserStep = step;
563
- try {
564
- switch (browserStep.type) {
565
- case "browser.navigate":
566
- if (injected && browserStep.url.includes("sid=")) {
567
- continue;
568
- }
569
- await page.goto(browserStep.url, { waitUntil: "domcontentloaded" });
570
- break;
571
- case "browser.click":
572
- if (injected) {
573
- await Promise.all([
574
- page.waitForNavigation({
575
- waitUntil: "domcontentloaded",
576
- timeout: 5e3
577
- }).catch(() => {
578
- }),
579
- page.click(browserStep.selector, { timeout: 5e3 })
580
- ]);
581
- } else {
582
- await page.click(browserStep.selector, { timeout: 5e3 });
583
- }
584
- break;
585
- case "browser.input": {
586
- const value = step.id === targetStep.id ? payloadValue : browserStep.value;
587
- await page.fill(browserStep.selector, value, { timeout: 5e3 });
588
- if (step.id === targetStep.id) {
589
- injected = true;
590
- }
591
- break;
558
+ };
559
+ function createDialogHandler(page, eventFindings, getPayloadInfo) {
560
+ return async (dialog) => {
561
+ const info = getPayloadInfo();
562
+ if (info) {
563
+ const message = dialog.message();
564
+ const dialogType = dialog.type();
565
+ if (dialogType !== "beforeunload") {
566
+ eventFindings.push({
567
+ type: "xss",
568
+ severity: "high",
569
+ title: `XSS Confirmed - ${dialogType}() triggered`,
570
+ description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
571
+ stepId: info.stepId,
572
+ payload: info.payloadValue,
573
+ url: page.url(),
574
+ evidence: `Dialog type: ${dialogType}, Message: ${message}`,
575
+ metadata: {
576
+ dialogType,
577
+ dialogMessage: message,
578
+ detectionMethod: "dialog"
592
579
  }
593
- case "browser.keypress": {
594
- const modifiers = browserStep.modifiers ?? [];
595
- for (const mod of modifiers) {
596
- await page.keyboard.down(
597
- mod
598
- );
599
- }
600
- await page.keyboard.press(browserStep.key);
601
- for (const mod of modifiers.reverse()) {
602
- await page.keyboard.up(
603
- mod
604
- );
605
- }
606
- break;
580
+ });
581
+ }
582
+ }
583
+ try {
584
+ await dialog.dismiss();
585
+ } catch {
586
+ }
587
+ };
588
+ }
589
+ function createConsoleHandler(eventFindings, getPayloadInfo) {
590
+ return async (msg) => {
591
+ const info = getPayloadInfo();
592
+ if (info && msg.type() === "log") {
593
+ const text = msg.text();
594
+ if (text.includes("vulcn") || text.includes(info.payloadValue)) {
595
+ eventFindings.push({
596
+ type: "xss",
597
+ severity: "high",
598
+ title: "XSS Confirmed - Console Output",
599
+ description: `JavaScript console.log was triggered by payload injection`,
600
+ stepId: info.stepId,
601
+ payload: info.payloadValue,
602
+ url: "",
603
+ evidence: `Console output: ${text}`,
604
+ metadata: {
605
+ consoleType: msg.type(),
606
+ detectionMethod: "console"
607
607
  }
608
- case "browser.scroll":
609
- if (browserStep.selector) {
610
- await page.locator(browserStep.selector).evaluate((el, pos) => {
611
- el.scrollTo(pos.x, pos.y);
612
- }, browserStep.position);
613
- } else {
614
- await page.evaluate((pos) => {
615
- window.scrollTo(pos.x, pos.y);
616
- }, browserStep.position);
617
- }
618
- break;
619
- case "browser.wait":
620
- await page.waitForTimeout(browserStep.duration);
621
- break;
622
- }
623
- } catch {
608
+ });
609
+ }
610
+ }
611
+ };
612
+ }
613
+ async function cyclePayload(page, session, targetStep, payloadValue, formPageUrl) {
614
+ try {
615
+ await page.goBack({ waitUntil: "domcontentloaded", timeout: 5e3 });
616
+ const targetSelector = targetStep.selector;
617
+ const formPresent = await page.waitForSelector(targetSelector, { timeout: 3e3 }).then(() => true).catch(() => false);
618
+ if (!formPresent) {
619
+ await page.goto(formPageUrl, {
620
+ waitUntil: "domcontentloaded",
621
+ timeout: 5e3
622
+ });
623
+ const formPresentAfterNav = await page.waitForSelector(targetSelector, { timeout: 3e3 }).then(() => true).catch(() => false);
624
+ if (!formPresentAfterNav) {
625
+ return false;
624
626
  }
625
627
  }
628
+ await page.fill(targetSelector, payloadValue, { timeout: 5e3 });
629
+ await replayStepsAfter(page, session, targetStep);
626
630
  await page.waitForTimeout(500);
631
+ return true;
632
+ } catch {
633
+ return false;
627
634
  }
628
- /**
629
- * Check for payload reflection in page content
630
- */
631
- static async checkReflection(page, step, payloadSet, payloadValue) {
632
- const content = await page.content();
633
- for (const pattern of payloadSet.detectPatterns) {
634
- if (pattern.test(content)) {
635
- return {
636
- type: payloadSet.category,
637
- severity: _BrowserRunner.getSeverity(payloadSet.category),
638
- title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
639
- description: `Payload pattern was reflected in page content`,
640
- stepId: step.id,
641
- payload: payloadValue,
642
- url: page.url(),
643
- evidence: content.match(pattern)?.[0]?.slice(0, 200)
644
- };
635
+ }
636
+ async function replayWithPayload(page, session, targetStep, payloadValue, startUrl) {
637
+ await page.goto(startUrl, { waitUntil: "domcontentloaded" });
638
+ let injected = false;
639
+ for (const step of session.steps) {
640
+ const browserStep = step;
641
+ try {
642
+ switch (browserStep.type) {
643
+ case "browser.navigate":
644
+ if (injected && browserStep.url.includes("sid=")) {
645
+ continue;
646
+ }
647
+ await page.goto(browserStep.url, { waitUntil: "domcontentloaded" });
648
+ break;
649
+ case "browser.click":
650
+ if (injected) {
651
+ await Promise.all([
652
+ page.waitForNavigation({
653
+ waitUntil: "domcontentloaded",
654
+ timeout: 5e3
655
+ }).catch(() => {
656
+ }),
657
+ page.click(browserStep.selector, { timeout: 5e3 })
658
+ ]);
659
+ } else {
660
+ await page.click(browserStep.selector, { timeout: 5e3 });
661
+ }
662
+ break;
663
+ case "browser.input": {
664
+ const value = step.id === targetStep.id ? payloadValue : browserStep.value;
665
+ await page.fill(browserStep.selector, value, { timeout: 5e3 });
666
+ if (step.id === targetStep.id) {
667
+ injected = true;
668
+ }
669
+ break;
670
+ }
671
+ case "browser.keypress": {
672
+ const modifiers = browserStep.modifiers ?? [];
673
+ for (const mod of modifiers) {
674
+ await page.keyboard.down(
675
+ mod
676
+ );
677
+ }
678
+ await page.keyboard.press(browserStep.key);
679
+ for (const mod of modifiers.reverse()) {
680
+ await page.keyboard.up(mod);
681
+ }
682
+ break;
683
+ }
684
+ case "browser.scroll":
685
+ if (browserStep.selector) {
686
+ await page.locator(browserStep.selector).evaluate((el, pos) => {
687
+ el.scrollTo(pos.x, pos.y);
688
+ }, browserStep.position);
689
+ } else {
690
+ await page.evaluate((pos) => {
691
+ window.scrollTo(pos.x, pos.y);
692
+ }, browserStep.position);
693
+ }
694
+ break;
695
+ case "browser.wait":
696
+ await page.waitForTimeout(browserStep.duration);
697
+ break;
645
698
  }
699
+ } catch {
646
700
  }
647
- if (content.includes(payloadValue)) {
701
+ }
702
+ await page.waitForTimeout(500);
703
+ }
704
+ async function replayStepsAfter(page, session, targetStep) {
705
+ let pastTarget = false;
706
+ for (const step of session.steps) {
707
+ if (step.id === targetStep.id) {
708
+ pastTarget = true;
709
+ continue;
710
+ }
711
+ if (!pastTarget) continue;
712
+ const browserStep = step;
713
+ try {
714
+ switch (browserStep.type) {
715
+ case "browser.navigate":
716
+ break;
717
+ case "browser.click":
718
+ await Promise.all([
719
+ page.waitForNavigation({
720
+ waitUntil: "domcontentloaded",
721
+ timeout: 5e3
722
+ }).catch(() => {
723
+ }),
724
+ page.click(browserStep.selector, { timeout: 5e3 })
725
+ ]);
726
+ break;
727
+ case "browser.input":
728
+ await page.fill(browserStep.selector, browserStep.value, {
729
+ timeout: 5e3
730
+ });
731
+ break;
732
+ case "browser.keypress": {
733
+ const modifiers = browserStep.modifiers ?? [];
734
+ for (const mod of modifiers) {
735
+ await page.keyboard.down(
736
+ mod
737
+ );
738
+ }
739
+ await page.keyboard.press(browserStep.key);
740
+ for (const mod of modifiers.reverse()) {
741
+ await page.keyboard.up(mod);
742
+ }
743
+ break;
744
+ }
745
+ case "browser.scroll":
746
+ break;
747
+ // skip scrolls in fast path
748
+ case "browser.wait":
749
+ break;
750
+ }
751
+ } catch {
752
+ }
753
+ }
754
+ await page.waitForTimeout(500);
755
+ }
756
+ async function checkReflection(page, step, payloadSet, payloadValue) {
757
+ const content = await page.content();
758
+ for (const pattern of payloadSet.detectPatterns) {
759
+ if (pattern.test(content)) {
648
760
  return {
649
761
  type: payloadSet.category,
650
- severity: "medium",
651
- title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
652
- description: `Payload was reflected in page without encoding`,
762
+ severity: getSeverity(payloadSet.category),
763
+ title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
764
+ description: `Payload pattern was reflected in page content`,
653
765
  stepId: step.id,
654
766
  payload: payloadValue,
655
- url: page.url()
767
+ url: page.url(),
768
+ evidence: content.match(pattern)?.[0]?.slice(0, 200)
656
769
  };
657
770
  }
658
- return void 0;
659
771
  }
660
- /**
661
- * Determine severity based on vulnerability category
662
- */
663
- static getSeverity(category) {
664
- switch (category) {
665
- case "sqli":
666
- case "command-injection":
667
- case "xxe":
668
- return "critical";
669
- case "xss":
670
- case "ssrf":
671
- case "path-traversal":
672
- return "high";
673
- case "open-redirect":
674
- return "medium";
675
- default:
676
- return "medium";
772
+ if (content.includes(payloadValue)) {
773
+ return {
774
+ type: payloadSet.category,
775
+ severity: "medium",
776
+ title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
777
+ description: `Payload was reflected in page without encoding`,
778
+ stepId: step.id,
779
+ payload: payloadValue,
780
+ url: page.url()
781
+ };
782
+ }
783
+ return void 0;
784
+ }
785
+ function getSeverity(category) {
786
+ switch (category) {
787
+ case "sqli":
788
+ case "command-injection":
789
+ case "xxe":
790
+ return "critical";
791
+ case "xss":
792
+ case "ssrf":
793
+ case "path-traversal":
794
+ return "high";
795
+ case "open-redirect":
796
+ return "medium";
797
+ default:
798
+ return "medium";
799
+ }
800
+ }
801
+ function interleavePayloads(payloads) {
802
+ const result = [];
803
+ const payloadsByCategory = payloads.map(
804
+ (ps) => ps.payloads.map((value) => ({ payloadSet: ps, value }))
805
+ );
806
+ const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));
807
+ for (let i = 0; i < maxLen; i++) {
808
+ for (const category of payloadsByCategory) {
809
+ if (i < category.length) {
810
+ result.push(category[i]);
811
+ }
677
812
  }
678
813
  }
679
- };
814
+ return result;
815
+ }
680
816
 
681
817
  // src/crawler.ts
682
818
  var INJECTABLE_INPUT_TYPES = /* @__PURE__ */ new Set([
@@ -713,7 +849,8 @@ async function crawlAndBuildSessions(config, options = {}) {
713
849
  headless: config.headless ?? true
714
850
  });
715
851
  const context = await browser.newContext({
716
- viewport: config.viewport ?? { width: 1280, height: 720 }
852
+ viewport: config.viewport ?? { width: 1280, height: 720 },
853
+ ...options.storageState ? { storageState: JSON.parse(options.storageState) } : {}
717
854
  });
718
855
  try {
719
856
  while (queue.length > 0 && visited.size < opts.maxPages) {
@@ -896,20 +1033,58 @@ async function discoverForms(page, pageUrl) {
896
1033
  }
897
1034
  return forms;
898
1035
  }
1036
+ var REDIRECT_PARAMS = /* @__PURE__ */ new Set([
1037
+ "to",
1038
+ "url",
1039
+ "redirect",
1040
+ "redirect_uri",
1041
+ "redirect_url",
1042
+ "return",
1043
+ "return_url",
1044
+ "returnto",
1045
+ "next",
1046
+ "goto",
1047
+ "dest",
1048
+ "destination",
1049
+ "continue",
1050
+ "target",
1051
+ "rurl",
1052
+ "out",
1053
+ "link",
1054
+ "forward"
1055
+ ]);
1056
+ function isExternalRedirectLink(link, origin) {
1057
+ try {
1058
+ const parsed = new URL(link);
1059
+ if (parsed.origin !== origin) return false;
1060
+ for (const [key, value] of parsed.searchParams) {
1061
+ if (REDIRECT_PARAMS.has(key.toLowerCase())) {
1062
+ try {
1063
+ const targetUrl = new URL(value);
1064
+ if (targetUrl.origin !== origin) return true;
1065
+ } catch {
1066
+ }
1067
+ }
1068
+ }
1069
+ return false;
1070
+ } catch {
1071
+ return false;
1072
+ }
1073
+ }
899
1074
  async function discoverLinks(page, origin, sameOrigin) {
900
1075
  const links = await page.evaluate(() => {
901
1076
  return Array.from(document.querySelectorAll("a[href]")).map((a) => a.href).filter((href) => href.startsWith("http"));
902
1077
  });
903
- if (sameOrigin) {
904
- return links.filter((link) => {
905
- try {
906
- return new URL(link).origin === origin;
907
- } catch {
908
- return false;
909
- }
910
- });
911
- }
912
- return links;
1078
+ return links.filter((link) => {
1079
+ try {
1080
+ const linkOrigin = new URL(link).origin;
1081
+ if (sameOrigin && linkOrigin !== origin) return false;
1082
+ if (isExternalRedirectLink(link, origin)) return false;
1083
+ return true;
1084
+ } catch {
1085
+ return false;
1086
+ }
1087
+ });
913
1088
  }
914
1089
  function buildSessions(forms) {
915
1090
  const targetForms = forms.filter((f) => f.inputs.some((i) => i.injectable));
@@ -990,6 +1165,182 @@ function normalizeUrl(url) {
990
1165
  }
991
1166
  }
992
1167
 
1168
+ // src/auth.ts
1169
+ async function detectLoginForm(page) {
1170
+ return page.evaluate(() => {
1171
+ function findUsernameInput(container) {
1172
+ const selectors = [
1173
+ 'input[autocomplete="username"]',
1174
+ 'input[autocomplete="email"]',
1175
+ 'input[type="email"]',
1176
+ 'input[name*="user" i]',
1177
+ 'input[name*="login" i]',
1178
+ 'input[name*="email" i]',
1179
+ 'input[id*="user" i]',
1180
+ 'input[id*="login" i]',
1181
+ 'input[id*="email" i]',
1182
+ 'input[name*="name" i]',
1183
+ 'input[type="text"]'
1184
+ ];
1185
+ for (const sel of selectors) {
1186
+ const el = container.querySelector(sel);
1187
+ if (el && el.type !== "password" && el.type !== "hidden") {
1188
+ return el;
1189
+ }
1190
+ }
1191
+ return null;
1192
+ }
1193
+ function findSubmitButton(container) {
1194
+ const selectors = [
1195
+ 'button[type="submit"]',
1196
+ 'input[type="submit"]',
1197
+ "button:not([type])",
1198
+ 'button[type="button"]'
1199
+ ];
1200
+ for (const sel of selectors) {
1201
+ const el = container.querySelector(sel);
1202
+ if (el) return el;
1203
+ }
1204
+ return null;
1205
+ }
1206
+ function getSelector(el) {
1207
+ if (el.id) return `#${CSS.escape(el.id)}`;
1208
+ if (el.getAttribute("name"))
1209
+ return `${el.tagName.toLowerCase()}[name="${CSS.escape(el.getAttribute("name"))}"]`;
1210
+ if (el.getAttribute("type") && el.tagName === "INPUT")
1211
+ return `input[type="${el.getAttribute("type")}"]`;
1212
+ const parent = el.parentElement;
1213
+ if (!parent) return el.tagName.toLowerCase();
1214
+ const siblings = Array.from(parent.children);
1215
+ const index = siblings.indexOf(el) + 1;
1216
+ return `${parent.tagName.toLowerCase()} > ${el.tagName.toLowerCase()}:nth-child(${index})`;
1217
+ }
1218
+ const forms = document.querySelectorAll("form");
1219
+ for (const form of forms) {
1220
+ const passwordInput2 = form.querySelector(
1221
+ 'input[type="password"]'
1222
+ );
1223
+ if (!passwordInput2) continue;
1224
+ const usernameInput = findUsernameInput(form);
1225
+ const submitButton = findSubmitButton(form);
1226
+ return {
1227
+ usernameSelector: usernameInput ? getSelector(usernameInput) : 'input[type="text"]',
1228
+ passwordSelector: getSelector(passwordInput2),
1229
+ submitSelector: submitButton ? getSelector(submitButton) : null,
1230
+ autoDetected: true
1231
+ };
1232
+ }
1233
+ const passwordInput = document.querySelector(
1234
+ 'input[type="password"]'
1235
+ );
1236
+ if (passwordInput) {
1237
+ const usernameInput = findUsernameInput(document.body);
1238
+ const submitButton = findSubmitButton(document.body);
1239
+ return {
1240
+ usernameSelector: usernameInput ? getSelector(usernameInput) : 'input[type="text"]',
1241
+ passwordSelector: getSelector(passwordInput),
1242
+ submitSelector: submitButton ? getSelector(submitButton) : null,
1243
+ autoDetected: true
1244
+ };
1245
+ }
1246
+ return null;
1247
+ });
1248
+ }
1249
+ async function performLogin(page, context, credentials, options) {
1250
+ const loginUrl = credentials.loginUrl ?? options.targetUrl;
1251
+ await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 15e3 });
1252
+ let usernameSelector;
1253
+ let passwordSelector;
1254
+ let submitSelector;
1255
+ if (credentials.userSelector && credentials.passSelector) {
1256
+ usernameSelector = credentials.userSelector;
1257
+ passwordSelector = credentials.passSelector;
1258
+ submitSelector = null;
1259
+ } else {
1260
+ const form = await detectLoginForm(page);
1261
+ if (!form) {
1262
+ return {
1263
+ success: false,
1264
+ message: `No login form detected on ${loginUrl}. Use --user-field and --pass-field to specify selectors.`
1265
+ };
1266
+ }
1267
+ usernameSelector = form.usernameSelector;
1268
+ passwordSelector = form.passwordSelector;
1269
+ submitSelector = form.submitSelector;
1270
+ }
1271
+ try {
1272
+ await page.fill(usernameSelector, credentials.username, { timeout: 5e3 });
1273
+ } catch {
1274
+ return {
1275
+ success: false,
1276
+ message: `Could not find username field: ${usernameSelector}`
1277
+ };
1278
+ }
1279
+ try {
1280
+ await page.fill(passwordSelector, credentials.password, { timeout: 5e3 });
1281
+ } catch {
1282
+ return {
1283
+ success: false,
1284
+ message: `Could not find password field: ${passwordSelector}`
1285
+ };
1286
+ }
1287
+ try {
1288
+ if (submitSelector) {
1289
+ await Promise.all([
1290
+ page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
1291
+ }),
1292
+ page.click(submitSelector, { timeout: 5e3 })
1293
+ ]);
1294
+ } else {
1295
+ await Promise.all([
1296
+ page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
1297
+ }),
1298
+ page.press(passwordSelector, "Enter")
1299
+ ]);
1300
+ }
1301
+ } catch {
1302
+ return {
1303
+ success: false,
1304
+ message: "Failed to submit login form"
1305
+ };
1306
+ }
1307
+ await page.waitForTimeout(1e3);
1308
+ const bodyText = await page.textContent("body").catch(() => "");
1309
+ if (options.loggedOutIndicator && bodyText?.includes(options.loggedOutIndicator)) {
1310
+ return {
1311
+ success: false,
1312
+ message: `Login failed \u2014 "${options.loggedOutIndicator}" still visible on page`
1313
+ };
1314
+ }
1315
+ if (options.loggedInIndicator && !bodyText?.includes(options.loggedInIndicator)) {
1316
+ return {
1317
+ success: false,
1318
+ message: `Login uncertain \u2014 "${options.loggedInIndicator}" not found on page`
1319
+ };
1320
+ }
1321
+ const storageState = JSON.stringify(await context.storageState());
1322
+ return {
1323
+ success: true,
1324
+ message: "Login successful",
1325
+ storageState
1326
+ };
1327
+ }
1328
+ async function checkSessionAlive(page, config) {
1329
+ try {
1330
+ const bodyText = await page.textContent("body");
1331
+ if (!bodyText) return true;
1332
+ if (config.loggedOutIndicator && bodyText.includes(config.loggedOutIndicator)) {
1333
+ return false;
1334
+ }
1335
+ if (config.loggedInIndicator && !bodyText.includes(config.loggedInIndicator)) {
1336
+ return false;
1337
+ }
1338
+ return true;
1339
+ } catch {
1340
+ return false;
1341
+ }
1342
+ }
1343
+
993
1344
  // src/index.ts
994
1345
  var configSchema = import_zod.z.object({
995
1346
  /** Starting URL for recording */
@@ -1096,9 +1447,12 @@ var index_default = browserDriver;
1096
1447
  BrowserRunner,
1097
1448
  BrowserStepSchema,
1098
1449
  checkBrowsers,
1450
+ checkSessionAlive,
1099
1451
  configSchema,
1100
1452
  crawlAndBuildSessions,
1453
+ detectLoginForm,
1101
1454
  installBrowsers,
1102
- launchBrowser
1455
+ launchBrowser,
1456
+ performLogin
1103
1457
  });
1104
1458
  //# sourceMappingURL=index.cjs.map