@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.js CHANGED
@@ -368,9 +368,12 @@ var BrowserRecorder = class _BrowserRecorder {
368
368
  };
369
369
 
370
370
  // src/runner.ts
371
- var BrowserRunner = class _BrowserRunner {
371
+ var BrowserRunner = class {
372
372
  /**
373
- * Execute a session with security payloads
373
+ * Execute a session with security payloads.
374
+ *
375
+ * If ctx.options.browser is provided, reuses that browser (persistent mode).
376
+ * Otherwise, launches and closes its own browser (standalone mode).
374
377
  */
375
378
  static async execute(session, ctx) {
376
379
  const config = session.driverConfig;
@@ -396,83 +399,39 @@ var BrowserRunner = class _BrowserRunner {
396
399
  ]
397
400
  };
398
401
  }
399
- const { browser } = await launchBrowser({
400
- browser: browserType,
401
- headless
402
- });
403
- const context = await browser.newContext({ viewport });
402
+ const sharedBrowser = ctx.options.browser;
403
+ const ownBrowser = sharedBrowser ? null : (await launchBrowser({ browser: browserType, headless })).browser;
404
+ const browser = sharedBrowser ?? ownBrowser;
405
+ const storageState = ctx.options.storageState;
406
+ const contextOptions = { viewport };
407
+ if (storageState) {
408
+ contextOptions.storageState = JSON.parse(storageState);
409
+ }
410
+ const context = await browser.newContext(contextOptions);
404
411
  const page = await context.newPage();
405
412
  await ctx.options.onPageReady?.(page);
406
413
  const eventFindings = [];
407
414
  let currentPayloadInfo = null;
408
- const dialogHandler = async (dialog) => {
409
- if (currentPayloadInfo) {
410
- const message = dialog.message();
411
- const dialogType = dialog.type();
412
- if (dialogType !== "beforeunload") {
413
- eventFindings.push({
414
- type: "xss",
415
- severity: "high",
416
- title: `XSS Confirmed - ${dialogType}() triggered`,
417
- description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
418
- stepId: currentPayloadInfo.stepId,
419
- payload: currentPayloadInfo.payloadValue,
420
- url: page.url(),
421
- evidence: `Dialog type: ${dialogType}, Message: ${message}`,
422
- metadata: {
423
- dialogType,
424
- dialogMessage: message,
425
- detectionMethod: "dialog"
426
- }
427
- });
428
- }
429
- }
430
- try {
431
- await dialog.dismiss();
432
- } catch {
433
- }
434
- };
435
- const consoleHandler = async (msg) => {
436
- if (currentPayloadInfo && msg.type() === "log") {
437
- const text = msg.text();
438
- if (text.includes("vulcn") || text.includes(currentPayloadInfo.payloadValue)) {
439
- eventFindings.push({
440
- type: "xss",
441
- severity: "high",
442
- title: "XSS Confirmed - Console Output",
443
- description: `JavaScript console.log was triggered by payload injection`,
444
- stepId: currentPayloadInfo.stepId,
445
- payload: currentPayloadInfo.payloadValue,
446
- url: page.url(),
447
- evidence: `Console output: ${text}`,
448
- metadata: {
449
- consoleType: msg.type(),
450
- detectionMethod: "console"
451
- }
452
- });
453
- }
454
- }
455
- };
415
+ const dialogHandler = createDialogHandler(
416
+ page,
417
+ eventFindings,
418
+ () => currentPayloadInfo
419
+ );
420
+ const consoleHandler = createConsoleHandler(
421
+ eventFindings,
422
+ () => currentPayloadInfo
423
+ );
456
424
  page.on("dialog", dialogHandler);
457
425
  page.on("console", consoleHandler);
458
426
  try {
459
427
  const injectableSteps = session.steps.filter(
460
428
  (step) => step.type === "browser.input" && step.injectable !== false
461
429
  );
462
- const allPayloads = [];
463
- const payloadsByCategory = payloads.map(
464
- (ps) => ps.payloads.map((value) => ({ payloadSet: ps, value }))
465
- );
466
- const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));
467
- for (let i = 0; i < maxLen; i++) {
468
- for (const category of payloadsByCategory) {
469
- if (i < category.length) {
470
- allPayloads.push(category[i]);
471
- }
472
- }
473
- }
430
+ const allPayloads = interleavePayloads(payloads);
474
431
  const confirmedTypes = /* @__PURE__ */ new Set();
475
432
  for (const injectableStep of injectableSteps) {
433
+ let isFirstPayload = true;
434
+ let formPageUrl = null;
476
435
  for (const { payloadSet, value } of allPayloads) {
477
436
  const stepTypeKey = `${injectableStep.id}::${payloadSet.category}`;
478
437
  if (confirmedTypes.has(stepTypeKey)) {
@@ -484,14 +443,35 @@ var BrowserRunner = class _BrowserRunner {
484
443
  payloadSet,
485
444
  payloadValue: value
486
445
  };
487
- await _BrowserRunner.replayWithPayload(
488
- page,
489
- session,
490
- injectableStep,
491
- value,
492
- startUrl
493
- );
494
- const reflectionFinding = await _BrowserRunner.checkReflection(
446
+ if (isFirstPayload) {
447
+ await replayWithPayload(
448
+ page,
449
+ session,
450
+ injectableStep,
451
+ value,
452
+ startUrl
453
+ );
454
+ isFirstPayload = false;
455
+ formPageUrl = startUrl;
456
+ } else {
457
+ const cycled = await cyclePayload(
458
+ page,
459
+ session,
460
+ injectableStep,
461
+ value,
462
+ formPageUrl ?? startUrl
463
+ );
464
+ if (!cycled) {
465
+ await replayWithPayload(
466
+ page,
467
+ session,
468
+ injectableStep,
469
+ value,
470
+ startUrl
471
+ );
472
+ }
473
+ }
474
+ const reflectionFinding = await checkReflection(
495
475
  page,
496
476
  injectableStep,
497
477
  payloadSet,
@@ -517,6 +497,7 @@ var BrowserRunner = class _BrowserRunner {
517
497
  ctx.options.onStepComplete?.(injectableStep.id, payloadsTested);
518
498
  } catch (err) {
519
499
  errors.push(`${injectableStep.id}: ${String(err)}`);
500
+ isFirstPayload = true;
520
501
  }
521
502
  }
522
503
  }
@@ -525,7 +506,10 @@ var BrowserRunner = class _BrowserRunner {
525
506
  page.off("console", consoleHandler);
526
507
  currentPayloadInfo = null;
527
508
  await ctx.options.onBeforeClose?.(page);
528
- await browser.close();
509
+ await context.close();
510
+ if (ownBrowser) {
511
+ await ownBrowser.close();
512
+ }
529
513
  }
530
514
  return {
531
515
  findings: ctx.findings,
@@ -535,136 +519,264 @@ var BrowserRunner = class _BrowserRunner {
535
519
  errors
536
520
  };
537
521
  }
538
- /**
539
- * Replay session steps with payload injected at target step
540
- *
541
- * IMPORTANT: We replay ALL steps, not just up to the injectable step.
542
- * The injection replaces the input value, but subsequent steps (like
543
- * clicking submit) must still execute so the payload reaches the server
544
- * and gets reflected back in the response.
545
- */
546
- static async replayWithPayload(page, session, targetStep, payloadValue, startUrl) {
547
- await page.goto(startUrl, { waitUntil: "domcontentloaded" });
548
- let injected = false;
549
- for (const step of session.steps) {
550
- const browserStep = step;
551
- try {
552
- switch (browserStep.type) {
553
- case "browser.navigate":
554
- if (injected && browserStep.url.includes("sid=")) {
555
- continue;
556
- }
557
- await page.goto(browserStep.url, { waitUntil: "domcontentloaded" });
558
- break;
559
- case "browser.click":
560
- if (injected) {
561
- await Promise.all([
562
- page.waitForNavigation({
563
- waitUntil: "domcontentloaded",
564
- timeout: 5e3
565
- }).catch(() => {
566
- }),
567
- page.click(browserStep.selector, { timeout: 5e3 })
568
- ]);
569
- } else {
570
- await page.click(browserStep.selector, { timeout: 5e3 });
571
- }
572
- break;
573
- case "browser.input": {
574
- const value = step.id === targetStep.id ? payloadValue : browserStep.value;
575
- await page.fill(browserStep.selector, value, { timeout: 5e3 });
576
- if (step.id === targetStep.id) {
577
- injected = true;
578
- }
579
- break;
522
+ };
523
+ function createDialogHandler(page, eventFindings, getPayloadInfo) {
524
+ return async (dialog) => {
525
+ const info = getPayloadInfo();
526
+ if (info) {
527
+ const message = dialog.message();
528
+ const dialogType = dialog.type();
529
+ if (dialogType !== "beforeunload") {
530
+ eventFindings.push({
531
+ type: "xss",
532
+ severity: "high",
533
+ title: `XSS Confirmed - ${dialogType}() triggered`,
534
+ description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
535
+ stepId: info.stepId,
536
+ payload: info.payloadValue,
537
+ url: page.url(),
538
+ evidence: `Dialog type: ${dialogType}, Message: ${message}`,
539
+ metadata: {
540
+ dialogType,
541
+ dialogMessage: message,
542
+ detectionMethod: "dialog"
580
543
  }
581
- case "browser.keypress": {
582
- const modifiers = browserStep.modifiers ?? [];
583
- for (const mod of modifiers) {
584
- await page.keyboard.down(
585
- mod
586
- );
587
- }
588
- await page.keyboard.press(browserStep.key);
589
- for (const mod of modifiers.reverse()) {
590
- await page.keyboard.up(
591
- mod
592
- );
593
- }
594
- break;
544
+ });
545
+ }
546
+ }
547
+ try {
548
+ await dialog.dismiss();
549
+ } catch {
550
+ }
551
+ };
552
+ }
553
+ function createConsoleHandler(eventFindings, getPayloadInfo) {
554
+ return async (msg) => {
555
+ const info = getPayloadInfo();
556
+ if (info && msg.type() === "log") {
557
+ const text = msg.text();
558
+ if (text.includes("vulcn") || text.includes(info.payloadValue)) {
559
+ eventFindings.push({
560
+ type: "xss",
561
+ severity: "high",
562
+ title: "XSS Confirmed - Console Output",
563
+ description: `JavaScript console.log was triggered by payload injection`,
564
+ stepId: info.stepId,
565
+ payload: info.payloadValue,
566
+ url: "",
567
+ evidence: `Console output: ${text}`,
568
+ metadata: {
569
+ consoleType: msg.type(),
570
+ detectionMethod: "console"
595
571
  }
596
- case "browser.scroll":
597
- if (browserStep.selector) {
598
- await page.locator(browserStep.selector).evaluate((el, pos) => {
599
- el.scrollTo(pos.x, pos.y);
600
- }, browserStep.position);
601
- } else {
602
- await page.evaluate((pos) => {
603
- window.scrollTo(pos.x, pos.y);
604
- }, browserStep.position);
605
- }
606
- break;
607
- case "browser.wait":
608
- await page.waitForTimeout(browserStep.duration);
609
- break;
610
- }
611
- } catch {
572
+ });
612
573
  }
613
574
  }
575
+ };
576
+ }
577
+ async function cyclePayload(page, session, targetStep, payloadValue, formPageUrl) {
578
+ try {
579
+ await page.goBack({ waitUntil: "domcontentloaded", timeout: 5e3 });
580
+ const targetSelector = targetStep.selector;
581
+ const formPresent = await page.waitForSelector(targetSelector, { timeout: 3e3 }).then(() => true).catch(() => false);
582
+ if (!formPresent) {
583
+ await page.goto(formPageUrl, {
584
+ waitUntil: "domcontentloaded",
585
+ timeout: 5e3
586
+ });
587
+ const formPresentAfterNav = await page.waitForSelector(targetSelector, { timeout: 3e3 }).then(() => true).catch(() => false);
588
+ if (!formPresentAfterNav) {
589
+ return false;
590
+ }
591
+ }
592
+ await page.fill(targetSelector, payloadValue, { timeout: 5e3 });
593
+ await replayStepsAfter(page, session, targetStep);
614
594
  await page.waitForTimeout(500);
595
+ return true;
596
+ } catch {
597
+ return false;
615
598
  }
616
- /**
617
- * Check for payload reflection in page content
618
- */
619
- static async checkReflection(page, step, payloadSet, payloadValue) {
620
- const content = await page.content();
621
- for (const pattern of payloadSet.detectPatterns) {
622
- if (pattern.test(content)) {
623
- return {
624
- type: payloadSet.category,
625
- severity: _BrowserRunner.getSeverity(payloadSet.category),
626
- title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
627
- description: `Payload pattern was reflected in page content`,
628
- stepId: step.id,
629
- payload: payloadValue,
630
- url: page.url(),
631
- evidence: content.match(pattern)?.[0]?.slice(0, 200)
632
- };
599
+ }
600
+ async function replayWithPayload(page, session, targetStep, payloadValue, startUrl) {
601
+ await page.goto(startUrl, { waitUntil: "domcontentloaded" });
602
+ let injected = false;
603
+ for (const step of session.steps) {
604
+ const browserStep = step;
605
+ try {
606
+ switch (browserStep.type) {
607
+ case "browser.navigate":
608
+ if (injected && browserStep.url.includes("sid=")) {
609
+ continue;
610
+ }
611
+ await page.goto(browserStep.url, { waitUntil: "domcontentloaded" });
612
+ break;
613
+ case "browser.click":
614
+ if (injected) {
615
+ await Promise.all([
616
+ page.waitForNavigation({
617
+ waitUntil: "domcontentloaded",
618
+ timeout: 5e3
619
+ }).catch(() => {
620
+ }),
621
+ page.click(browserStep.selector, { timeout: 5e3 })
622
+ ]);
623
+ } else {
624
+ await page.click(browserStep.selector, { timeout: 5e3 });
625
+ }
626
+ break;
627
+ case "browser.input": {
628
+ const value = step.id === targetStep.id ? payloadValue : browserStep.value;
629
+ await page.fill(browserStep.selector, value, { timeout: 5e3 });
630
+ if (step.id === targetStep.id) {
631
+ injected = true;
632
+ }
633
+ break;
634
+ }
635
+ case "browser.keypress": {
636
+ const modifiers = browserStep.modifiers ?? [];
637
+ for (const mod of modifiers) {
638
+ await page.keyboard.down(
639
+ mod
640
+ );
641
+ }
642
+ await page.keyboard.press(browserStep.key);
643
+ for (const mod of modifiers.reverse()) {
644
+ await page.keyboard.up(mod);
645
+ }
646
+ break;
647
+ }
648
+ case "browser.scroll":
649
+ if (browserStep.selector) {
650
+ await page.locator(browserStep.selector).evaluate((el, pos) => {
651
+ el.scrollTo(pos.x, pos.y);
652
+ }, browserStep.position);
653
+ } else {
654
+ await page.evaluate((pos) => {
655
+ window.scrollTo(pos.x, pos.y);
656
+ }, browserStep.position);
657
+ }
658
+ break;
659
+ case "browser.wait":
660
+ await page.waitForTimeout(browserStep.duration);
661
+ break;
633
662
  }
663
+ } catch {
634
664
  }
635
- if (content.includes(payloadValue)) {
665
+ }
666
+ await page.waitForTimeout(500);
667
+ }
668
+ async function replayStepsAfter(page, session, targetStep) {
669
+ let pastTarget = false;
670
+ for (const step of session.steps) {
671
+ if (step.id === targetStep.id) {
672
+ pastTarget = true;
673
+ continue;
674
+ }
675
+ if (!pastTarget) continue;
676
+ const browserStep = step;
677
+ try {
678
+ switch (browserStep.type) {
679
+ case "browser.navigate":
680
+ break;
681
+ case "browser.click":
682
+ await Promise.all([
683
+ page.waitForNavigation({
684
+ waitUntil: "domcontentloaded",
685
+ timeout: 5e3
686
+ }).catch(() => {
687
+ }),
688
+ page.click(browserStep.selector, { timeout: 5e3 })
689
+ ]);
690
+ break;
691
+ case "browser.input":
692
+ await page.fill(browserStep.selector, browserStep.value, {
693
+ timeout: 5e3
694
+ });
695
+ break;
696
+ case "browser.keypress": {
697
+ const modifiers = browserStep.modifiers ?? [];
698
+ for (const mod of modifiers) {
699
+ await page.keyboard.down(
700
+ mod
701
+ );
702
+ }
703
+ await page.keyboard.press(browserStep.key);
704
+ for (const mod of modifiers.reverse()) {
705
+ await page.keyboard.up(mod);
706
+ }
707
+ break;
708
+ }
709
+ case "browser.scroll":
710
+ break;
711
+ // skip scrolls in fast path
712
+ case "browser.wait":
713
+ break;
714
+ }
715
+ } catch {
716
+ }
717
+ }
718
+ await page.waitForTimeout(500);
719
+ }
720
+ async function checkReflection(page, step, payloadSet, payloadValue) {
721
+ const content = await page.content();
722
+ for (const pattern of payloadSet.detectPatterns) {
723
+ if (pattern.test(content)) {
636
724
  return {
637
725
  type: payloadSet.category,
638
- severity: "medium",
639
- title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
640
- description: `Payload was reflected in page without encoding`,
726
+ severity: getSeverity(payloadSet.category),
727
+ title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
728
+ description: `Payload pattern was reflected in page content`,
641
729
  stepId: step.id,
642
730
  payload: payloadValue,
643
- url: page.url()
731
+ url: page.url(),
732
+ evidence: content.match(pattern)?.[0]?.slice(0, 200)
644
733
  };
645
734
  }
646
- return void 0;
647
735
  }
648
- /**
649
- * Determine severity based on vulnerability category
650
- */
651
- static getSeverity(category) {
652
- switch (category) {
653
- case "sqli":
654
- case "command-injection":
655
- case "xxe":
656
- return "critical";
657
- case "xss":
658
- case "ssrf":
659
- case "path-traversal":
660
- return "high";
661
- case "open-redirect":
662
- return "medium";
663
- default:
664
- return "medium";
736
+ if (content.includes(payloadValue)) {
737
+ return {
738
+ type: payloadSet.category,
739
+ severity: "medium",
740
+ title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
741
+ description: `Payload was reflected in page without encoding`,
742
+ stepId: step.id,
743
+ payload: payloadValue,
744
+ url: page.url()
745
+ };
746
+ }
747
+ return void 0;
748
+ }
749
+ function getSeverity(category) {
750
+ switch (category) {
751
+ case "sqli":
752
+ case "command-injection":
753
+ case "xxe":
754
+ return "critical";
755
+ case "xss":
756
+ case "ssrf":
757
+ case "path-traversal":
758
+ return "high";
759
+ case "open-redirect":
760
+ return "medium";
761
+ default:
762
+ return "medium";
763
+ }
764
+ }
765
+ function interleavePayloads(payloads) {
766
+ const result = [];
767
+ const payloadsByCategory = payloads.map(
768
+ (ps) => ps.payloads.map((value) => ({ payloadSet: ps, value }))
769
+ );
770
+ const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));
771
+ for (let i = 0; i < maxLen; i++) {
772
+ for (const category of payloadsByCategory) {
773
+ if (i < category.length) {
774
+ result.push(category[i]);
775
+ }
665
776
  }
666
777
  }
667
- };
778
+ return result;
779
+ }
668
780
 
669
781
  // src/crawler.ts
670
782
  var INJECTABLE_INPUT_TYPES = /* @__PURE__ */ new Set([
@@ -701,7 +813,8 @@ async function crawlAndBuildSessions(config, options = {}) {
701
813
  headless: config.headless ?? true
702
814
  });
703
815
  const context = await browser.newContext({
704
- viewport: config.viewport ?? { width: 1280, height: 720 }
816
+ viewport: config.viewport ?? { width: 1280, height: 720 },
817
+ ...options.storageState ? { storageState: JSON.parse(options.storageState) } : {}
705
818
  });
706
819
  try {
707
820
  while (queue.length > 0 && visited.size < opts.maxPages) {
@@ -1016,6 +1129,182 @@ function normalizeUrl(url) {
1016
1129
  }
1017
1130
  }
1018
1131
 
1132
+ // src/auth.ts
1133
+ async function detectLoginForm(page) {
1134
+ return page.evaluate(() => {
1135
+ function findUsernameInput(container) {
1136
+ const selectors = [
1137
+ 'input[autocomplete="username"]',
1138
+ 'input[autocomplete="email"]',
1139
+ 'input[type="email"]',
1140
+ 'input[name*="user" i]',
1141
+ 'input[name*="login" i]',
1142
+ 'input[name*="email" i]',
1143
+ 'input[id*="user" i]',
1144
+ 'input[id*="login" i]',
1145
+ 'input[id*="email" i]',
1146
+ 'input[name*="name" i]',
1147
+ 'input[type="text"]'
1148
+ ];
1149
+ for (const sel of selectors) {
1150
+ const el = container.querySelector(sel);
1151
+ if (el && el.type !== "password" && el.type !== "hidden") {
1152
+ return el;
1153
+ }
1154
+ }
1155
+ return null;
1156
+ }
1157
+ function findSubmitButton(container) {
1158
+ const selectors = [
1159
+ 'button[type="submit"]',
1160
+ 'input[type="submit"]',
1161
+ "button:not([type])",
1162
+ 'button[type="button"]'
1163
+ ];
1164
+ for (const sel of selectors) {
1165
+ const el = container.querySelector(sel);
1166
+ if (el) return el;
1167
+ }
1168
+ return null;
1169
+ }
1170
+ function getSelector(el) {
1171
+ if (el.id) return `#${CSS.escape(el.id)}`;
1172
+ if (el.getAttribute("name"))
1173
+ return `${el.tagName.toLowerCase()}[name="${CSS.escape(el.getAttribute("name"))}"]`;
1174
+ if (el.getAttribute("type") && el.tagName === "INPUT")
1175
+ return `input[type="${el.getAttribute("type")}"]`;
1176
+ const parent = el.parentElement;
1177
+ if (!parent) return el.tagName.toLowerCase();
1178
+ const siblings = Array.from(parent.children);
1179
+ const index = siblings.indexOf(el) + 1;
1180
+ return `${parent.tagName.toLowerCase()} > ${el.tagName.toLowerCase()}:nth-child(${index})`;
1181
+ }
1182
+ const forms = document.querySelectorAll("form");
1183
+ for (const form of forms) {
1184
+ const passwordInput2 = form.querySelector(
1185
+ 'input[type="password"]'
1186
+ );
1187
+ if (!passwordInput2) continue;
1188
+ const usernameInput = findUsernameInput(form);
1189
+ const submitButton = findSubmitButton(form);
1190
+ return {
1191
+ usernameSelector: usernameInput ? getSelector(usernameInput) : 'input[type="text"]',
1192
+ passwordSelector: getSelector(passwordInput2),
1193
+ submitSelector: submitButton ? getSelector(submitButton) : null,
1194
+ autoDetected: true
1195
+ };
1196
+ }
1197
+ const passwordInput = document.querySelector(
1198
+ 'input[type="password"]'
1199
+ );
1200
+ if (passwordInput) {
1201
+ const usernameInput = findUsernameInput(document.body);
1202
+ const submitButton = findSubmitButton(document.body);
1203
+ return {
1204
+ usernameSelector: usernameInput ? getSelector(usernameInput) : 'input[type="text"]',
1205
+ passwordSelector: getSelector(passwordInput),
1206
+ submitSelector: submitButton ? getSelector(submitButton) : null,
1207
+ autoDetected: true
1208
+ };
1209
+ }
1210
+ return null;
1211
+ });
1212
+ }
1213
+ async function performLogin(page, context, credentials, options) {
1214
+ const loginUrl = credentials.loginUrl ?? options.targetUrl;
1215
+ await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 15e3 });
1216
+ let usernameSelector;
1217
+ let passwordSelector;
1218
+ let submitSelector;
1219
+ if (credentials.userSelector && credentials.passSelector) {
1220
+ usernameSelector = credentials.userSelector;
1221
+ passwordSelector = credentials.passSelector;
1222
+ submitSelector = null;
1223
+ } else {
1224
+ const form = await detectLoginForm(page);
1225
+ if (!form) {
1226
+ return {
1227
+ success: false,
1228
+ message: `No login form detected on ${loginUrl}. Use --user-field and --pass-field to specify selectors.`
1229
+ };
1230
+ }
1231
+ usernameSelector = form.usernameSelector;
1232
+ passwordSelector = form.passwordSelector;
1233
+ submitSelector = form.submitSelector;
1234
+ }
1235
+ try {
1236
+ await page.fill(usernameSelector, credentials.username, { timeout: 5e3 });
1237
+ } catch {
1238
+ return {
1239
+ success: false,
1240
+ message: `Could not find username field: ${usernameSelector}`
1241
+ };
1242
+ }
1243
+ try {
1244
+ await page.fill(passwordSelector, credentials.password, { timeout: 5e3 });
1245
+ } catch {
1246
+ return {
1247
+ success: false,
1248
+ message: `Could not find password field: ${passwordSelector}`
1249
+ };
1250
+ }
1251
+ try {
1252
+ if (submitSelector) {
1253
+ await Promise.all([
1254
+ page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
1255
+ }),
1256
+ page.click(submitSelector, { timeout: 5e3 })
1257
+ ]);
1258
+ } else {
1259
+ await Promise.all([
1260
+ page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
1261
+ }),
1262
+ page.press(passwordSelector, "Enter")
1263
+ ]);
1264
+ }
1265
+ } catch {
1266
+ return {
1267
+ success: false,
1268
+ message: "Failed to submit login form"
1269
+ };
1270
+ }
1271
+ await page.waitForTimeout(1e3);
1272
+ const bodyText = await page.textContent("body").catch(() => "");
1273
+ if (options.loggedOutIndicator && bodyText?.includes(options.loggedOutIndicator)) {
1274
+ return {
1275
+ success: false,
1276
+ message: `Login failed \u2014 "${options.loggedOutIndicator}" still visible on page`
1277
+ };
1278
+ }
1279
+ if (options.loggedInIndicator && !bodyText?.includes(options.loggedInIndicator)) {
1280
+ return {
1281
+ success: false,
1282
+ message: `Login uncertain \u2014 "${options.loggedInIndicator}" not found on page`
1283
+ };
1284
+ }
1285
+ const storageState = JSON.stringify(await context.storageState());
1286
+ return {
1287
+ success: true,
1288
+ message: "Login successful",
1289
+ storageState
1290
+ };
1291
+ }
1292
+ async function checkSessionAlive(page, config) {
1293
+ try {
1294
+ const bodyText = await page.textContent("body");
1295
+ if (!bodyText) return true;
1296
+ if (config.loggedOutIndicator && bodyText.includes(config.loggedOutIndicator)) {
1297
+ return false;
1298
+ }
1299
+ if (config.loggedInIndicator && !bodyText.includes(config.loggedInIndicator)) {
1300
+ return false;
1301
+ }
1302
+ return true;
1303
+ } catch {
1304
+ return false;
1305
+ }
1306
+ }
1307
+
1019
1308
  // src/index.ts
1020
1309
  var configSchema = z.object({
1021
1310
  /** Starting URL for recording */
@@ -1121,10 +1410,13 @@ export {
1121
1410
  BrowserRunner,
1122
1411
  BrowserStepSchema,
1123
1412
  checkBrowsers,
1413
+ checkSessionAlive,
1124
1414
  configSchema,
1125
1415
  crawlAndBuildSessions,
1126
1416
  index_default as default,
1417
+ detectLoginForm,
1127
1418
  installBrowsers,
1128
- launchBrowser
1419
+ launchBrowser,
1420
+ performLogin
1129
1421
  };
1130
1422
  //# sourceMappingURL=index.js.map