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