@vulcn/engine 0.2.0 → 0.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/dist/index.js CHANGED
@@ -1,218 +1,263 @@
1
- // session.ts
2
- import { z } from "zod";
3
- import { parse, stringify } from "yaml";
4
- var StepSchema = z.discriminatedUnion("type", [
5
- z.object({
6
- id: z.string(),
7
- type: z.literal("navigate"),
8
- url: z.string(),
9
- timestamp: z.number()
10
- }),
11
- z.object({
12
- id: z.string(),
13
- type: z.literal("click"),
14
- selector: z.string(),
15
- position: z.object({ x: z.number(), y: z.number() }).optional(),
16
- timestamp: z.number()
17
- }),
18
- z.object({
19
- id: z.string(),
20
- type: z.literal("input"),
21
- selector: z.string(),
22
- value: z.string(),
23
- injectable: z.boolean().optional().default(true),
24
- timestamp: z.number()
25
- }),
26
- z.object({
27
- id: z.string(),
28
- type: z.literal("keypress"),
29
- key: z.string(),
30
- modifiers: z.array(z.string()).optional(),
31
- timestamp: z.number()
32
- }),
33
- z.object({
34
- id: z.string(),
35
- type: z.literal("scroll"),
36
- selector: z.string().optional(),
37
- position: z.object({ x: z.number(), y: z.number() }),
38
- timestamp: z.number()
39
- }),
40
- z.object({
41
- id: z.string(),
42
- type: z.literal("wait"),
43
- duration: z.number(),
44
- timestamp: z.number()
45
- })
46
- ]);
47
- var SessionSchema = z.object({
48
- version: z.string().default("1"),
49
- name: z.string(),
50
- recordedAt: z.string(),
51
- browser: z.enum(["chromium", "firefox", "webkit"]).default("chromium"),
52
- viewport: z.object({
53
- width: z.number(),
54
- height: z.number()
55
- }),
56
- startUrl: z.string(),
57
- steps: z.array(StepSchema)
58
- });
59
- function createSession(options) {
60
- return {
61
- version: "1",
62
- name: options.name,
63
- recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
64
- browser: options.browser ?? "chromium",
65
- viewport: options.viewport ?? { width: 1280, height: 720 },
66
- startUrl: options.startUrl,
67
- steps: []
68
- };
69
- }
70
- function parseSession(yaml) {
71
- const data = parse(yaml);
72
- return SessionSchema.parse(data);
73
- }
74
- function serializeSession(session) {
75
- return stringify(session, { lineWidth: 0 });
76
- }
77
-
78
- // browser.ts
79
- import { chromium, firefox, webkit } from "playwright";
80
- import { exec } from "child_process";
81
- import { promisify } from "util";
82
- var execAsync = promisify(exec);
83
- var BrowserNotFoundError = class extends Error {
84
- constructor(message) {
85
- super(message);
86
- this.name = "BrowserNotFoundError";
87
- }
88
- };
89
- async function launchBrowser(options = {}) {
90
- const browserType = options.browser ?? "chromium";
91
- const headless = options.headless ?? false;
92
- if (browserType === "chromium") {
93
- try {
94
- const browser = await chromium.launch({
95
- channel: "chrome",
96
- headless
97
- });
98
- return { browser, channel: "chrome" };
99
- } catch {
100
- }
101
- try {
102
- const browser = await chromium.launch({
103
- channel: "msedge",
104
- headless
105
- });
106
- return { browser, channel: "msedge" };
107
- } catch {
1
+ // src/driver-manager.ts
2
+ import { isAbsolute, resolve } from "path";
3
+ import { parse } from "yaml";
4
+ var DriverManager = class {
5
+ drivers = /* @__PURE__ */ new Map();
6
+ defaultDriver = null;
7
+ /**
8
+ * Register a driver
9
+ */
10
+ register(driver, source = "builtin") {
11
+ this.validateDriver(driver);
12
+ this.drivers.set(driver.name, { driver, source });
13
+ if (this.drivers.size === 1) {
14
+ this.defaultDriver = driver.name;
108
15
  }
109
- try {
110
- const browser = await chromium.launch({ headless });
111
- return { browser, channel: "chromium" };
112
- } catch {
113
- throw new BrowserNotFoundError(
114
- "No Chromium browser found. Install Chrome or run: vulcn install chromium"
115
- );
16
+ }
17
+ /**
18
+ * Load a driver from npm or local path
19
+ */
20
+ async load(nameOrPath) {
21
+ let driver;
22
+ let source;
23
+ if (nameOrPath.startsWith("./") || nameOrPath.startsWith("../") || isAbsolute(nameOrPath)) {
24
+ const resolved = isAbsolute(nameOrPath) ? nameOrPath : resolve(process.cwd(), nameOrPath);
25
+ const module = await import(resolved);
26
+ driver = module.default || module;
27
+ source = "local";
28
+ } else {
29
+ const module = await import(nameOrPath);
30
+ driver = module.default || module;
31
+ source = "npm";
116
32
  }
33
+ this.register(driver, source);
117
34
  }
118
- if (browserType === "firefox") {
119
- try {
120
- const browser = await firefox.launch({ headless });
121
- return { browser, channel: "firefox" };
122
- } catch {
123
- throw new BrowserNotFoundError(
124
- "Firefox not found. Run: vulcn install firefox"
125
- );
35
+ /**
36
+ * Get a loaded driver by name
37
+ */
38
+ get(name) {
39
+ return this.drivers.get(name)?.driver;
40
+ }
41
+ /**
42
+ * Get the default driver
43
+ */
44
+ getDefault() {
45
+ if (!this.defaultDriver) return void 0;
46
+ return this.get(this.defaultDriver);
47
+ }
48
+ /**
49
+ * Set the default driver
50
+ */
51
+ setDefault(name) {
52
+ if (!this.drivers.has(name)) {
53
+ throw new Error(`Driver "${name}" is not registered`);
126
54
  }
55
+ this.defaultDriver = name;
56
+ }
57
+ /**
58
+ * Check if a driver is registered
59
+ */
60
+ has(name) {
61
+ return this.drivers.has(name);
127
62
  }
128
- if (browserType === "webkit") {
129
- try {
130
- const browser = await webkit.launch({ headless });
131
- return { browser, channel: "webkit" };
132
- } catch {
133
- throw new BrowserNotFoundError(
134
- "WebKit not found. Run: vulcn install webkit"
63
+ /**
64
+ * Get all registered drivers
65
+ */
66
+ list() {
67
+ return Array.from(this.drivers.values());
68
+ }
69
+ /**
70
+ * Get driver for a session
71
+ */
72
+ getForSession(session) {
73
+ const driverName = session.driver;
74
+ const driver = this.get(driverName);
75
+ if (!driver) {
76
+ throw new Error(
77
+ `Driver "${driverName}" not found. Install @vulcn/driver-${driverName} or load it manually.`
135
78
  );
136
79
  }
80
+ return driver;
137
81
  }
138
- throw new BrowserNotFoundError(`Unknown browser type: ${browserType}`);
139
- }
140
- async function installBrowsers(browsers = ["chromium"]) {
141
- const browserArg = browsers.join(" ");
142
- await execAsync(`npx playwright install ${browserArg}`);
143
- }
144
- async function checkBrowsers() {
145
- const results = {
146
- systemChrome: false,
147
- systemEdge: false,
148
- playwrightChromium: false,
149
- playwrightFirefox: false,
150
- playwrightWebkit: false
151
- };
152
- try {
153
- const browser = await chromium.launch({
154
- channel: "chrome",
155
- headless: true
82
+ /**
83
+ * Parse a YAML session string into a Session object.
84
+ *
85
+ * Handles both new driver-format sessions and legacy v1 sessions.
86
+ * Legacy sessions (those with non-namespaced step types like "click",
87
+ * "input", "navigate") are automatically converted to the driver format
88
+ * (e.g., "browser.click", "browser.input", "browser.navigate").
89
+ *
90
+ * @param yaml - Raw YAML string
91
+ * @param defaultDriver - Driver to assign for legacy sessions (default: "browser")
92
+ */
93
+ parseSession(yaml, defaultDriver = "browser") {
94
+ const data = parse(yaml);
95
+ if (data.driver && typeof data.driver === "string") {
96
+ return data;
97
+ }
98
+ const steps = data.steps ?? [];
99
+ const convertedSteps = steps.map((step) => {
100
+ const type = step.type;
101
+ if (type.includes(".")) {
102
+ return step;
103
+ }
104
+ return {
105
+ ...step,
106
+ type: `${defaultDriver}.${type}`
107
+ };
156
108
  });
157
- await browser.close();
158
- results.systemChrome = true;
159
- } catch {
109
+ return {
110
+ name: data.name ?? "Untitled Session",
111
+ driver: defaultDriver,
112
+ driverConfig: {
113
+ browser: data.browser ?? "chromium",
114
+ viewport: data.viewport ?? { width: 1280, height: 720 },
115
+ startUrl: data.startUrl
116
+ },
117
+ steps: convertedSteps,
118
+ metadata: {
119
+ recordedAt: data.recordedAt,
120
+ version: data.version ?? "1"
121
+ }
122
+ };
160
123
  }
161
- try {
162
- const browser = await chromium.launch({
163
- channel: "msedge",
164
- headless: true
165
- });
166
- await browser.close();
167
- results.systemEdge = true;
168
- } catch {
124
+ /**
125
+ * Start recording with a driver
126
+ */
127
+ async startRecording(driverName, config, options = {}) {
128
+ const driver = this.get(driverName);
129
+ if (!driver) {
130
+ throw new Error(`Driver "${driverName}" not found`);
131
+ }
132
+ return driver.recorder.start(config, options);
169
133
  }
170
- try {
171
- const browser = await chromium.launch({ headless: true });
172
- await browser.close();
173
- results.playwrightChromium = true;
174
- } catch {
134
+ /**
135
+ * Execute a session
136
+ * Invokes plugin hooks (onRunStart, onRunEnd) around the driver runner.
137
+ */
138
+ async execute(session, pluginManager2, options = {}) {
139
+ const driver = this.getForSession(session);
140
+ const findings = [];
141
+ const logger = this.createLogger(driver.name);
142
+ const ctx = {
143
+ session,
144
+ pluginManager: pluginManager2,
145
+ payloads: pluginManager2.getPayloads(),
146
+ findings,
147
+ addFinding: (finding) => {
148
+ findings.push(finding);
149
+ pluginManager2.addFinding(finding);
150
+ options.onFinding?.(finding);
151
+ },
152
+ logger,
153
+ options
154
+ };
155
+ const pluginCtx = {
156
+ session,
157
+ page: null,
158
+ headless: !!options.headless,
159
+ config: {},
160
+ engine: { version: "0.3.0", pluginApiVersion: 1 },
161
+ payloads: pluginManager2.getPayloads(),
162
+ findings,
163
+ logger,
164
+ fetch: globalThis.fetch
165
+ };
166
+ for (const loaded of pluginManager2.getPlugins()) {
167
+ if (loaded.enabled && loaded.plugin.hooks?.onRunStart) {
168
+ try {
169
+ await loaded.plugin.hooks.onRunStart({
170
+ ...pluginCtx,
171
+ config: loaded.config
172
+ });
173
+ } catch (err) {
174
+ logger.warn(`Plugin ${loaded.plugin.name} onRunStart failed: ${err}`);
175
+ }
176
+ }
177
+ }
178
+ let result = await driver.runner.execute(session, ctx);
179
+ for (const loaded of pluginManager2.getPlugins()) {
180
+ if (loaded.enabled && loaded.plugin.hooks?.onRunEnd) {
181
+ try {
182
+ result = await loaded.plugin.hooks.onRunEnd(result, {
183
+ ...pluginCtx,
184
+ config: loaded.config,
185
+ findings: result.findings
186
+ });
187
+ } catch (err) {
188
+ logger.warn(`Plugin ${loaded.plugin.name} onRunEnd failed: ${err}`);
189
+ }
190
+ }
191
+ }
192
+ return result;
175
193
  }
176
- try {
177
- const browser = await firefox.launch({ headless: true });
178
- await browser.close();
179
- results.playwrightFirefox = true;
180
- } catch {
194
+ /**
195
+ * Validate driver structure
196
+ */
197
+ validateDriver(driver) {
198
+ if (!driver || typeof driver !== "object") {
199
+ throw new Error("Driver must be an object");
200
+ }
201
+ const d = driver;
202
+ if (typeof d.name !== "string" || !d.name) {
203
+ throw new Error("Driver must have a name");
204
+ }
205
+ if (typeof d.version !== "string" || !d.version) {
206
+ throw new Error("Driver must have a version");
207
+ }
208
+ if (!Array.isArray(d.stepTypes) || d.stepTypes.length === 0) {
209
+ throw new Error("Driver must define stepTypes");
210
+ }
211
+ if (!d.recorder || typeof d.recorder !== "object") {
212
+ throw new Error("Driver must have a recorder");
213
+ }
214
+ if (!d.runner || typeof d.runner !== "object") {
215
+ throw new Error("Driver must have a runner");
216
+ }
181
217
  }
182
- try {
183
- const browser = await webkit.launch({ headless: true });
184
- await browser.close();
185
- results.playwrightWebkit = true;
186
- } catch {
218
+ /**
219
+ * Create a scoped logger for a driver
220
+ */
221
+ createLogger(name) {
222
+ const prefix = `[driver:${name}]`;
223
+ return {
224
+ debug: (msg, ...args) => console.debug(prefix, msg, ...args),
225
+ info: (msg, ...args) => console.info(prefix, msg, ...args),
226
+ warn: (msg, ...args) => console.warn(prefix, msg, ...args),
227
+ error: (msg, ...args) => console.error(prefix, msg, ...args)
228
+ };
187
229
  }
188
- return results;
189
- }
230
+ };
231
+ var driverManager = new DriverManager();
190
232
 
191
- // plugin-manager.ts
233
+ // src/driver-types.ts
234
+ var DRIVER_API_VERSION = 1;
235
+
236
+ // src/plugin-manager.ts
192
237
  import { readFile } from "fs/promises";
193
238
  import { existsSync } from "fs";
194
- import { resolve, isAbsolute } from "path";
239
+ import { resolve as resolve2, isAbsolute as isAbsolute2 } from "path";
195
240
  import YAML from "yaml";
196
- import { z as z2 } from "zod";
241
+ import { z } from "zod";
197
242
 
198
- // plugin-types.ts
243
+ // src/plugin-types.ts
199
244
  var PLUGIN_API_VERSION = 1;
200
245
 
201
- // plugin-manager.ts
246
+ // src/plugin-manager.ts
202
247
  var ENGINE_VERSION = "0.2.0";
203
- var VulcnConfigSchema = z2.object({
204
- version: z2.string().default("1"),
205
- plugins: z2.array(
206
- z2.object({
207
- name: z2.string(),
208
- config: z2.record(z2.unknown()).optional(),
209
- enabled: z2.boolean().default(true)
248
+ var VulcnConfigSchema = z.object({
249
+ version: z.string().default("1"),
250
+ plugins: z.array(
251
+ z.object({
252
+ name: z.string(),
253
+ config: z.record(z.unknown()).optional(),
254
+ enabled: z.boolean().default(true)
210
255
  })
211
256
  ).optional(),
212
- settings: z2.object({
213
- browser: z2.enum(["chromium", "firefox", "webkit"]).optional(),
214
- headless: z2.boolean().optional(),
215
- timeout: z2.number().optional()
257
+ settings: z.object({
258
+ browser: z.enum(["chromium", "firefox", "webkit"]).optional(),
259
+ headless: z.boolean().optional(),
260
+ timeout: z.number().optional()
216
261
  }).optional()
217
262
  });
218
263
  var PluginManager = class {
@@ -237,7 +282,7 @@ var PluginManager = class {
237
282
  ".vulcnrc.json"
238
283
  ];
239
284
  for (const path of paths) {
240
- const resolved = isAbsolute(path) ? path : resolve(process.cwd(), path);
285
+ const resolved = isAbsolute2(path) ? path : resolve2(process.cwd(), path);
241
286
  if (existsSync(resolved)) {
242
287
  const content = await readFile(resolved, "utf-8");
243
288
  const parsed = path.endsWith(".json") ? JSON.parse(content) : YAML.parse(content);
@@ -276,8 +321,8 @@ var PluginManager = class {
276
321
  const { name, config: pluginConfig = {} } = config;
277
322
  let plugin;
278
323
  let source;
279
- if (name.startsWith("./") || name.startsWith("../") || isAbsolute(name)) {
280
- const resolved = isAbsolute(name) ? name : resolve(process.cwd(), name);
324
+ if (name.startsWith("./") || name.startsWith("../") || isAbsolute2(name)) {
325
+ const resolved = isAbsolute2(name) ? name : resolve2(process.cwd(), name);
281
326
  const module = await import(resolved);
282
327
  plugin = module.default || module;
283
328
  source = "local";
@@ -516,600 +561,12 @@ var PluginManager = class {
516
561
  }
517
562
  };
518
563
  var pluginManager = new PluginManager();
519
-
520
- // recorder.ts
521
- var Recorder = class _Recorder {
522
- /**
523
- * Start a new recording session
524
- * Opens a browser window for the user to interact with
525
- */
526
- static async start(startUrl, options = {}, config = {}) {
527
- const manager = config.pluginManager ?? pluginManager;
528
- const browserType = options.browser ?? "chromium";
529
- const viewport = options.viewport ?? { width: 1280, height: 720 };
530
- const headless = options.headless ?? false;
531
- await manager.initialize();
532
- const { browser } = await launchBrowser({
533
- browser: browserType,
534
- headless
535
- });
536
- const context = await browser.newContext({ viewport });
537
- const page = await context.newPage();
538
- await page.goto(startUrl);
539
- const session = createSession({
540
- name: `Recording ${(/* @__PURE__ */ new Date()).toISOString()}`,
541
- startUrl,
542
- browser: browserType,
543
- viewport
544
- });
545
- const startTime = Date.now();
546
- const steps = [];
547
- let stepCounter = 0;
548
- const generateStepId = () => {
549
- stepCounter++;
550
- return `step_${String(stepCounter).padStart(3, "0")}`;
551
- };
552
- const baseRecordContext = {
553
- startUrl,
554
- browser: browserType,
555
- page,
556
- engine: { version: "0.2.0", pluginApiVersion: 1 },
557
- payloads: manager.getPayloads(),
558
- findings: manager.getFindings(),
559
- logger: {
560
- debug: console.debug.bind(console),
561
- info: console.info.bind(console),
562
- warn: console.warn.bind(console),
563
- error: console.error.bind(console)
564
- },
565
- fetch: globalThis.fetch
566
- };
567
- await manager.callHook("onRecordStart", async (hook, ctx) => {
568
- const recordCtx = { ...baseRecordContext, ...ctx };
569
- await hook(recordCtx);
570
- });
571
- const initialStep = {
572
- id: generateStepId(),
573
- type: "navigate",
574
- url: startUrl,
575
- timestamp: 0
576
- };
577
- const transformedInitialStep = await _Recorder.transformStep(
578
- initialStep,
579
- manager,
580
- baseRecordContext
581
- );
582
- if (transformedInitialStep) {
583
- steps.push(transformedInitialStep);
584
- }
585
- _Recorder.attachListeners(
586
- page,
587
- steps,
588
- startTime,
589
- generateStepId,
590
- manager,
591
- baseRecordContext
592
- );
593
- return {
594
- async stop() {
595
- session.steps = steps;
596
- let finalSession = session;
597
- for (const loaded of manager.getPlugins()) {
598
- const hook = loaded.plugin.hooks?.onRecordEnd;
599
- if (hook) {
600
- const ctx = manager.createContext(loaded.config);
601
- const recordCtx = { ...baseRecordContext, ...ctx };
602
- finalSession = await hook(finalSession, recordCtx);
603
- }
604
- }
605
- await browser.close();
606
- return finalSession;
607
- },
608
- getSteps() {
609
- return [...steps];
610
- },
611
- getPage() {
612
- return page;
613
- }
614
- };
615
- }
616
- /**
617
- * Transform a step through plugin hooks
618
- * Returns null if the step should be filtered out
619
- */
620
- static async transformStep(step, manager, baseContext) {
621
- let transformedStep = step;
622
- for (const loaded of manager.getPlugins()) {
623
- const hook = loaded.plugin.hooks?.onRecordStep;
624
- if (hook) {
625
- const ctx = manager.createContext(loaded.config);
626
- const recordCtx = { ...baseContext, ...ctx };
627
- transformedStep = await hook(transformedStep, recordCtx);
628
- }
629
- }
630
- return transformedStep;
631
- }
632
- static attachListeners(page, steps, startTime, generateStepId, manager, baseContext) {
633
- const getTimestamp = () => Date.now() - startTime;
634
- const addStep = async (step) => {
635
- const transformed = await _Recorder.transformStep(
636
- step,
637
- manager,
638
- baseContext
639
- );
640
- if (transformed) {
641
- steps.push(transformed);
642
- }
643
- };
644
- page.on("framenavigated", (frame) => {
645
- if (frame === page.mainFrame()) {
646
- const url = frame.url();
647
- const lastStep = steps[steps.length - 1];
648
- if (steps.length > 0 && lastStep.type === "navigate" && lastStep.url === url) {
649
- return;
650
- }
651
- addStep({
652
- id: generateStepId(),
653
- type: "navigate",
654
- url,
655
- timestamp: getTimestamp()
656
- });
657
- }
658
- });
659
- page.exposeFunction(
660
- "__vulcn_record",
661
- async (event) => {
662
- const timestamp = getTimestamp();
663
- switch (event.type) {
664
- case "click": {
665
- const data = event.data;
666
- await addStep({
667
- id: generateStepId(),
668
- type: "click",
669
- selector: data.selector,
670
- position: { x: data.x, y: data.y },
671
- timestamp
672
- });
673
- break;
674
- }
675
- case "input": {
676
- const data = event.data;
677
- await addStep({
678
- id: generateStepId(),
679
- type: "input",
680
- selector: data.selector,
681
- value: data.value,
682
- injectable: data.injectable,
683
- timestamp
684
- });
685
- break;
686
- }
687
- case "keypress": {
688
- const data = event.data;
689
- await addStep({
690
- id: generateStepId(),
691
- type: "keypress",
692
- key: data.key,
693
- modifiers: data.modifiers,
694
- timestamp
695
- });
696
- break;
697
- }
698
- }
699
- }
700
- );
701
- page.on("load", async () => {
702
- await _Recorder.injectRecordingScript(page);
703
- });
704
- _Recorder.injectRecordingScript(page);
705
- }
706
- static async injectRecordingScript(page) {
707
- await page.evaluate(`
708
- (function() {
709
- if (window.__vulcn_injected) return;
710
- window.__vulcn_injected = true;
711
-
712
- var textInputTypes = ['text', 'password', 'email', 'search', 'url', 'tel', 'number'];
713
-
714
- function getSelector(el) {
715
- if (el.id) {
716
- return '#' + CSS.escape(el.id);
717
- }
718
- if (el.name) {
719
- var tag = el.tagName.toLowerCase();
720
- var nameSelector = tag + '[name="' + el.name + '"]';
721
- if (document.querySelectorAll(nameSelector).length === 1) {
722
- return nameSelector;
723
- }
724
- }
725
- if (el.dataset && el.dataset.testid) {
726
- return '[data-testid="' + el.dataset.testid + '"]';
727
- }
728
- if (el.tagName === 'INPUT' && el.type && el.name) {
729
- var inputSelector = 'input[type="' + el.type + '"][name="' + el.name + '"]';
730
- if (document.querySelectorAll(inputSelector).length === 1) {
731
- return inputSelector;
732
- }
733
- }
734
- if (el.className && typeof el.className === 'string') {
735
- var classes = el.className.trim().split(/\\s+/).filter(function(c) { return c.length > 0; });
736
- if (classes.length > 0) {
737
- var classSelector = el.tagName.toLowerCase() + '.' + classes.map(function(c) { return CSS.escape(c); }).join('.');
738
- if (document.querySelectorAll(classSelector).length === 1) {
739
- return classSelector;
740
- }
741
- }
742
- }
743
- var path = [];
744
- var current = el;
745
- while (current && current !== document.body) {
746
- var tag = current.tagName.toLowerCase();
747
- var parent = current.parentElement;
748
- if (parent) {
749
- var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === current.tagName; });
750
- if (siblings.length > 1) {
751
- var index = siblings.indexOf(current) + 1;
752
- tag = tag + ':nth-of-type(' + index + ')';
753
- }
754
- }
755
- path.unshift(tag);
756
- current = parent;
757
- }
758
- return path.join(' > ');
759
- }
760
-
761
- function getInputType(el) {
762
- if (el.tagName === 'INPUT') return el.type || 'text';
763
- if (el.tagName === 'TEXTAREA') return 'textarea';
764
- if (el.tagName === 'SELECT') return 'select';
765
- return null;
766
- }
767
-
768
- function isTextInjectable(el) {
769
- var inputType = getInputType(el);
770
- if (!inputType) return false;
771
- if (inputType === 'textarea') return true;
772
- if (inputType === 'select') return false;
773
- return textInputTypes.indexOf(inputType) !== -1;
774
- }
775
-
776
- document.addEventListener('click', function(e) {
777
- var target = e.target;
778
- window.__vulcn_record({
779
- type: 'click',
780
- data: {
781
- selector: getSelector(target),
782
- x: e.clientX,
783
- y: e.clientY
784
- }
785
- });
786
- }, true);
787
-
788
- document.addEventListener('change', function(e) {
789
- var target = e.target;
790
- if ('value' in target) {
791
- var inputType = getInputType(target);
792
- window.__vulcn_record({
793
- type: 'input',
794
- data: {
795
- selector: getSelector(target),
796
- value: target.value,
797
- inputType: inputType,
798
- injectable: isTextInjectable(target)
799
- }
800
- });
801
- }
802
- }, true);
803
-
804
- document.addEventListener('keydown', function(e) {
805
- if (e.ctrlKey || e.metaKey || e.altKey) {
806
- var modifiers = [];
807
- if (e.ctrlKey) modifiers.push('ctrl');
808
- if (e.metaKey) modifiers.push('meta');
809
- if (e.altKey) modifiers.push('alt');
810
- if (e.shiftKey) modifiers.push('shift');
811
-
812
- window.__vulcn_record({
813
- type: 'keypress',
814
- data: {
815
- key: e.key,
816
- modifiers: modifiers
817
- }
818
- });
819
- }
820
- }, true);
821
- })();
822
- `);
823
- }
824
- };
825
-
826
- // runner.ts
827
- var Runner = class _Runner {
828
- /**
829
- * Execute a session with security payloads from plugins
830
- *
831
- * @param session - The recorded session to replay
832
- * @param options - Runner configuration
833
- * @param config - Plugin manager configuration
834
- */
835
- static async execute(session, options = {}, config = {}) {
836
- const manager = config.pluginManager ?? pluginManager;
837
- const browserType = options.browser ?? session.browser ?? "chromium";
838
- const headless = options.headless ?? true;
839
- const startTime = Date.now();
840
- const errors = [];
841
- let payloadsTested = 0;
842
- await manager.initialize();
843
- manager.clearFindings();
844
- const payloads = manager.getPayloads();
845
- if (payloads.length === 0) {
846
- return {
847
- findings: [],
848
- stepsExecuted: session.steps.length,
849
- payloadsTested: 0,
850
- duration: Date.now() - startTime,
851
- errors: [
852
- "No payloads loaded. Add a payload plugin or configure payloads."
853
- ]
854
- };
855
- }
856
- const { browser } = await launchBrowser({
857
- browser: browserType,
858
- headless
859
- });
860
- const context = await browser.newContext({ viewport: session.viewport });
861
- const page = await context.newPage();
862
- const baseRunContext = {
863
- session,
864
- page,
865
- browser: browserType,
866
- headless,
867
- engine: { version: "0.2.0", pluginApiVersion: 1 },
868
- payloads: manager.getPayloads(),
869
- findings: manager.getFindings(),
870
- logger: {
871
- debug: console.debug.bind(console),
872
- info: console.info.bind(console),
873
- warn: console.warn.bind(console),
874
- error: console.error.bind(console)
875
- },
876
- fetch: globalThis.fetch
877
- };
878
- await manager.callHook("onRunStart", async (hook, ctx) => {
879
- const runCtx = { ...baseRunContext, ...ctx };
880
- await hook(runCtx);
881
- });
882
- const eventFindings = [];
883
- let currentDetectContext = null;
884
- const dialogHandler = async (dialog) => {
885
- if (currentDetectContext) {
886
- const findings = await manager.callHookCollect(
887
- "onDialog",
888
- async (hook, ctx) => {
889
- const detectCtx = {
890
- ...currentDetectContext,
891
- ...ctx
892
- };
893
- return hook(dialog, detectCtx);
894
- }
895
- );
896
- eventFindings.push(...findings);
897
- }
898
- try {
899
- await dialog.dismiss();
900
- } catch {
901
- }
902
- };
903
- const consoleHandler = async (msg) => {
904
- if (currentDetectContext) {
905
- const findings = await manager.callHookCollect("onConsoleMessage", async (hook, ctx) => {
906
- const detectCtx = { ...currentDetectContext, ...ctx };
907
- return hook(msg, detectCtx);
908
- });
909
- eventFindings.push(...findings);
910
- }
911
- };
912
- page.on("dialog", dialogHandler);
913
- page.on("console", consoleHandler);
914
- try {
915
- const injectableSteps = session.steps.filter(
916
- (step) => step.type === "input" && step.injectable !== false
917
- );
918
- const allPayloads = [];
919
- for (const payloadSet of payloads) {
920
- for (const value of payloadSet.payloads) {
921
- allPayloads.push({ payloadSet, value });
922
- }
923
- }
924
- for (const injectableStep of injectableSteps) {
925
- for (const { payloadSet, value: originalValue } of allPayloads) {
926
- try {
927
- let transformedPayload = originalValue;
928
- for (const loaded of manager.getPlugins()) {
929
- const hook = loaded.plugin.hooks?.onBeforePayload;
930
- if (hook) {
931
- const ctx = manager.createContext(loaded.config);
932
- const runCtx = { ...baseRunContext, ...ctx };
933
- transformedPayload = await hook(
934
- transformedPayload,
935
- injectableStep,
936
- runCtx
937
- );
938
- }
939
- }
940
- currentDetectContext = {
941
- ...baseRunContext,
942
- config: {},
943
- step: injectableStep,
944
- payloadSet,
945
- payloadValue: transformedPayload,
946
- stepId: injectableStep.id
947
- };
948
- await _Runner.replayWithPayload(
949
- page,
950
- session,
951
- injectableStep,
952
- transformedPayload
953
- );
954
- const afterFindings = await manager.callHookCollect("onAfterPayload", async (hook, ctx) => {
955
- const detectCtx = {
956
- ...currentDetectContext,
957
- ...ctx
958
- };
959
- return hook(detectCtx);
960
- });
961
- const reflectionFinding = await _Runner.checkReflection(
962
- page,
963
- injectableStep,
964
- payloadSet,
965
- transformedPayload
966
- );
967
- const allFindings = [...afterFindings, ...eventFindings];
968
- if (reflectionFinding) {
969
- allFindings.push(reflectionFinding);
970
- }
971
- for (const finding of allFindings) {
972
- manager.addFinding(finding);
973
- options.onFinding?.(finding);
974
- }
975
- eventFindings.length = 0;
976
- payloadsTested++;
977
- } catch (err) {
978
- errors.push(`${injectableStep.id}: ${String(err)}`);
979
- }
980
- }
981
- }
982
- } finally {
983
- page.off("dialog", dialogHandler);
984
- page.off("console", consoleHandler);
985
- currentDetectContext = null;
986
- await browser.close();
987
- }
988
- let result = {
989
- findings: manager.getFindings(),
990
- stepsExecuted: session.steps.length,
991
- payloadsTested,
992
- duration: Date.now() - startTime,
993
- errors
994
- };
995
- for (const loaded of manager.getPlugins()) {
996
- const hook = loaded.plugin.hooks?.onRunEnd;
997
- if (hook) {
998
- const ctx = manager.createContext(loaded.config);
999
- const runCtx = { ...baseRunContext, ...ctx };
1000
- result = await hook(result, runCtx);
1001
- }
1002
- }
1003
- return result;
1004
- }
1005
- /**
1006
- * Execute with explicit payloads (legacy API, for backwards compatibility)
1007
- */
1008
- static async executeWithPayloads(session, payloads, options = {}) {
1009
- const manager = new PluginManager();
1010
- manager.addPayloads(payloads);
1011
- return _Runner.execute(session, options, { pluginManager: manager });
1012
- }
1013
- /**
1014
- * Replay session steps with payload injected at target step
1015
- */
1016
- static async replayWithPayload(page, session, targetStep, payloadValue) {
1017
- await page.goto(session.startUrl, { waitUntil: "domcontentloaded" });
1018
- for (const step of session.steps) {
1019
- try {
1020
- if (step.type === "navigate") {
1021
- await page.goto(step.url, { waitUntil: "domcontentloaded" });
1022
- } else if (step.type === "click") {
1023
- await page.click(step.selector, { timeout: 5e3 });
1024
- } else if (step.type === "input") {
1025
- const value = step.id === targetStep.id ? payloadValue : step.value;
1026
- await page.fill(step.selector, value, { timeout: 5e3 });
1027
- } else if (step.type === "keypress") {
1028
- const modifiers = step.modifiers ?? [];
1029
- for (const mod of modifiers) {
1030
- await page.keyboard.down(
1031
- mod
1032
- );
1033
- }
1034
- await page.keyboard.press(step.key);
1035
- for (const mod of modifiers.reverse()) {
1036
- await page.keyboard.up(mod);
1037
- }
1038
- }
1039
- } catch {
1040
- }
1041
- if (step.id === targetStep.id) {
1042
- await page.waitForTimeout(100);
1043
- break;
1044
- }
1045
- }
1046
- }
1047
- /**
1048
- * Basic reflection check - fallback when no detection plugin is loaded
1049
- */
1050
- static async checkReflection(page, step, payloadSet, payloadValue) {
1051
- const content = await page.content();
1052
- for (const pattern of payloadSet.detectPatterns) {
1053
- if (pattern.test(content)) {
1054
- return {
1055
- type: payloadSet.category,
1056
- severity: _Runner.getSeverity(payloadSet.category),
1057
- title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
1058
- description: `Payload pattern was reflected in page content`,
1059
- stepId: step.id,
1060
- payload: payloadValue,
1061
- url: page.url(),
1062
- evidence: content.match(pattern)?.[0]?.slice(0, 200)
1063
- };
1064
- }
1065
- }
1066
- if (content.includes(payloadValue)) {
1067
- return {
1068
- type: payloadSet.category,
1069
- severity: "medium",
1070
- title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
1071
- description: `Payload was reflected in page without encoding`,
1072
- stepId: step.id,
1073
- payload: payloadValue,
1074
- url: page.url()
1075
- };
1076
- }
1077
- return void 0;
1078
- }
1079
- /**
1080
- * Determine severity based on vulnerability category
1081
- */
1082
- static getSeverity(category) {
1083
- switch (category) {
1084
- case "sqli":
1085
- case "command-injection":
1086
- case "xxe":
1087
- return "critical";
1088
- case "xss":
1089
- case "ssrf":
1090
- case "path-traversal":
1091
- return "high";
1092
- case "open-redirect":
1093
- return "medium";
1094
- default:
1095
- return "medium";
1096
- }
1097
- }
1098
- };
1099
564
  export {
1100
- BrowserNotFoundError,
565
+ DRIVER_API_VERSION,
566
+ DriverManager,
1101
567
  PLUGIN_API_VERSION,
1102
568
  PluginManager,
1103
- Recorder,
1104
- Runner,
1105
- SessionSchema,
1106
- StepSchema,
1107
- checkBrowsers,
1108
- createSession,
1109
- installBrowsers,
1110
- launchBrowser,
1111
- parseSession,
1112
- pluginManager,
1113
- serializeSession
569
+ driverManager,
570
+ pluginManager
1114
571
  };
1115
572
  //# sourceMappingURL=index.js.map