@vulcn/driver-browser 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/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,83 +435,39 @@ 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();
438
448
  await ctx.options.onPageReady?.(page);
439
449
  const eventFindings = [];
440
450
  let currentPayloadInfo = null;
441
- const dialogHandler = async (dialog) => {
442
- if (currentPayloadInfo) {
443
- const message = dialog.message();
444
- const dialogType = dialog.type();
445
- if (dialogType !== "beforeunload") {
446
- eventFindings.push({
447
- type: "xss",
448
- severity: "high",
449
- title: `XSS Confirmed - ${dialogType}() triggered`,
450
- description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
451
- stepId: currentPayloadInfo.stepId,
452
- payload: currentPayloadInfo.payloadValue,
453
- url: page.url(),
454
- evidence: `Dialog type: ${dialogType}, Message: ${message}`,
455
- metadata: {
456
- dialogType,
457
- dialogMessage: message,
458
- detectionMethod: "dialog"
459
- }
460
- });
461
- }
462
- }
463
- try {
464
- await dialog.dismiss();
465
- } catch {
466
- }
467
- };
468
- const consoleHandler = async (msg) => {
469
- if (currentPayloadInfo && msg.type() === "log") {
470
- const text = msg.text();
471
- if (text.includes("vulcn") || text.includes(currentPayloadInfo.payloadValue)) {
472
- eventFindings.push({
473
- type: "xss",
474
- severity: "high",
475
- title: "XSS Confirmed - Console Output",
476
- description: `JavaScript console.log was triggered by payload injection`,
477
- stepId: currentPayloadInfo.stepId,
478
- payload: currentPayloadInfo.payloadValue,
479
- url: page.url(),
480
- evidence: `Console output: ${text}`,
481
- metadata: {
482
- consoleType: msg.type(),
483
- detectionMethod: "console"
484
- }
485
- });
486
- }
487
- }
488
- };
451
+ const dialogHandler = createDialogHandler(
452
+ page,
453
+ eventFindings,
454
+ () => currentPayloadInfo
455
+ );
456
+ const consoleHandler = createConsoleHandler(
457
+ eventFindings,
458
+ () => currentPayloadInfo
459
+ );
489
460
  page.on("dialog", dialogHandler);
490
461
  page.on("console", consoleHandler);
491
462
  try {
492
463
  const injectableSteps = session.steps.filter(
493
464
  (step) => step.type === "browser.input" && step.injectable !== false
494
465
  );
495
- const allPayloads = [];
496
- const payloadsByCategory = payloads.map(
497
- (ps) => ps.payloads.map((value) => ({ payloadSet: ps, value }))
498
- );
499
- const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));
500
- for (let i = 0; i < maxLen; i++) {
501
- for (const category of payloadsByCategory) {
502
- if (i < category.length) {
503
- allPayloads.push(category[i]);
504
- }
505
- }
506
- }
466
+ const allPayloads = interleavePayloads(payloads);
507
467
  const confirmedTypes = /* @__PURE__ */ new Set();
508
468
  for (const injectableStep of injectableSteps) {
469
+ let isFirstPayload = true;
470
+ let formPageUrl = null;
509
471
  for (const { payloadSet, value } of allPayloads) {
510
472
  const stepTypeKey = `${injectableStep.id}::${payloadSet.category}`;
511
473
  if (confirmedTypes.has(stepTypeKey)) {
@@ -517,14 +479,35 @@ var BrowserRunner = class _BrowserRunner {
517
479
  payloadSet,
518
480
  payloadValue: value
519
481
  };
520
- await _BrowserRunner.replayWithPayload(
521
- page,
522
- session,
523
- injectableStep,
524
- value,
525
- startUrl
526
- );
527
- 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(
528
511
  page,
529
512
  injectableStep,
530
513
  payloadSet,
@@ -550,6 +533,7 @@ var BrowserRunner = class _BrowserRunner {
550
533
  ctx.options.onStepComplete?.(injectableStep.id, payloadsTested);
551
534
  } catch (err) {
552
535
  errors.push(`${injectableStep.id}: ${String(err)}`);
536
+ isFirstPayload = true;
553
537
  }
554
538
  }
555
539
  }
@@ -558,7 +542,10 @@ var BrowserRunner = class _BrowserRunner {
558
542
  page.off("console", consoleHandler);
559
543
  currentPayloadInfo = null;
560
544
  await ctx.options.onBeforeClose?.(page);
561
- await browser.close();
545
+ await context.close();
546
+ if (ownBrowser) {
547
+ await ownBrowser.close();
548
+ }
562
549
  }
563
550
  return {
564
551
  findings: ctx.findings,
@@ -568,136 +555,264 @@ var BrowserRunner = class _BrowserRunner {
568
555
  errors
569
556
  };
570
557
  }
571
- /**
572
- * Replay session steps with payload injected at target step
573
- *
574
- * IMPORTANT: We replay ALL steps, not just up to the injectable step.
575
- * The injection replaces the input value, but subsequent steps (like
576
- * clicking submit) must still execute so the payload reaches the server
577
- * and gets reflected back in the response.
578
- */
579
- static async replayWithPayload(page, session, targetStep, payloadValue, startUrl) {
580
- await page.goto(startUrl, { waitUntil: "domcontentloaded" });
581
- let injected = false;
582
- for (const step of session.steps) {
583
- const browserStep = step;
584
- try {
585
- switch (browserStep.type) {
586
- case "browser.navigate":
587
- if (injected && browserStep.url.includes("sid=")) {
588
- continue;
589
- }
590
- await page.goto(browserStep.url, { waitUntil: "domcontentloaded" });
591
- break;
592
- case "browser.click":
593
- if (injected) {
594
- await Promise.all([
595
- page.waitForNavigation({
596
- waitUntil: "domcontentloaded",
597
- timeout: 5e3
598
- }).catch(() => {
599
- }),
600
- page.click(browserStep.selector, { timeout: 5e3 })
601
- ]);
602
- } else {
603
- await page.click(browserStep.selector, { timeout: 5e3 });
604
- }
605
- break;
606
- case "browser.input": {
607
- const value = step.id === targetStep.id ? payloadValue : browserStep.value;
608
- await page.fill(browserStep.selector, value, { timeout: 5e3 });
609
- if (step.id === targetStep.id) {
610
- injected = true;
611
- }
612
- 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"
613
579
  }
614
- case "browser.keypress": {
615
- const modifiers = browserStep.modifiers ?? [];
616
- for (const mod of modifiers) {
617
- await page.keyboard.down(
618
- mod
619
- );
620
- }
621
- await page.keyboard.press(browserStep.key);
622
- for (const mod of modifiers.reverse()) {
623
- await page.keyboard.up(
624
- mod
625
- );
626
- }
627
- 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"
628
607
  }
629
- case "browser.scroll":
630
- if (browserStep.selector) {
631
- await page.locator(browserStep.selector).evaluate((el, pos) => {
632
- el.scrollTo(pos.x, pos.y);
633
- }, browserStep.position);
634
- } else {
635
- await page.evaluate((pos) => {
636
- window.scrollTo(pos.x, pos.y);
637
- }, browserStep.position);
638
- }
639
- break;
640
- case "browser.wait":
641
- await page.waitForTimeout(browserStep.duration);
642
- break;
643
- }
644
- } catch {
608
+ });
645
609
  }
646
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;
626
+ }
627
+ }
628
+ await page.fill(targetSelector, payloadValue, { timeout: 5e3 });
629
+ await replayStepsAfter(page, session, targetStep);
647
630
  await page.waitForTimeout(500);
631
+ return true;
632
+ } catch {
633
+ return false;
648
634
  }
649
- /**
650
- * Check for payload reflection in page content
651
- */
652
- static async checkReflection(page, step, payloadSet, payloadValue) {
653
- const content = await page.content();
654
- for (const pattern of payloadSet.detectPatterns) {
655
- if (pattern.test(content)) {
656
- return {
657
- type: payloadSet.category,
658
- severity: _BrowserRunner.getSeverity(payloadSet.category),
659
- title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
660
- description: `Payload pattern was reflected in page content`,
661
- stepId: step.id,
662
- payload: payloadValue,
663
- url: page.url(),
664
- evidence: content.match(pattern)?.[0]?.slice(0, 200)
665
- };
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;
666
698
  }
699
+ } catch {
667
700
  }
668
- 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)) {
669
760
  return {
670
761
  type: payloadSet.category,
671
- severity: "medium",
672
- title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
673
- 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`,
674
765
  stepId: step.id,
675
766
  payload: payloadValue,
676
- url: page.url()
767
+ url: page.url(),
768
+ evidence: content.match(pattern)?.[0]?.slice(0, 200)
677
769
  };
678
770
  }
679
- return void 0;
680
771
  }
681
- /**
682
- * Determine severity based on vulnerability category
683
- */
684
- static getSeverity(category) {
685
- switch (category) {
686
- case "sqli":
687
- case "command-injection":
688
- case "xxe":
689
- return "critical";
690
- case "xss":
691
- case "ssrf":
692
- case "path-traversal":
693
- return "high";
694
- case "open-redirect":
695
- return "medium";
696
- default:
697
- 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
+ }
698
812
  }
699
813
  }
700
- };
814
+ return result;
815
+ }
701
816
 
702
817
  // src/crawler.ts
703
818
  var INJECTABLE_INPUT_TYPES = /* @__PURE__ */ new Set([
@@ -734,7 +849,8 @@ async function crawlAndBuildSessions(config, options = {}) {
734
849
  headless: config.headless ?? true
735
850
  });
736
851
  const context = await browser.newContext({
737
- viewport: config.viewport ?? { width: 1280, height: 720 }
852
+ viewport: config.viewport ?? { width: 1280, height: 720 },
853
+ ...options.storageState ? { storageState: JSON.parse(options.storageState) } : {}
738
854
  });
739
855
  try {
740
856
  while (queue.length > 0 && visited.size < opts.maxPages) {
@@ -1049,6 +1165,182 @@ function normalizeUrl(url) {
1049
1165
  }
1050
1166
  }
1051
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
+
1052
1344
  // src/index.ts
1053
1345
  var configSchema = import_zod.z.object({
1054
1346
  /** Starting URL for recording */
@@ -1155,9 +1447,12 @@ var index_default = browserDriver;
1155
1447
  BrowserRunner,
1156
1448
  BrowserStepSchema,
1157
1449
  checkBrowsers,
1450
+ checkSessionAlive,
1158
1451
  configSchema,
1159
1452
  crawlAndBuildSessions,
1453
+ detectLoginForm,
1160
1454
  installBrowsers,
1161
- launchBrowser
1455
+ launchBrowser,
1456
+ performLogin
1162
1457
  });
1163
1458
  //# sourceMappingURL=index.cjs.map