@vulcn/engine 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,86 +17,579 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
- // index.ts
30
+ // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
23
- BUILTIN_PAYLOADS: () => BUILTIN_PAYLOADS,
24
33
  BrowserNotFoundError: () => BrowserNotFoundError,
34
+ DRIVER_API_VERSION: () => DRIVER_API_VERSION,
35
+ DriverManager: () => DriverManager,
36
+ PLUGIN_API_VERSION: () => PLUGIN_API_VERSION,
37
+ PluginManager: () => PluginManager,
25
38
  Recorder: () => Recorder,
26
39
  Runner: () => Runner,
27
40
  SessionSchema: () => SessionSchema,
28
41
  StepSchema: () => StepSchema,
29
42
  checkBrowsers: () => checkBrowsers,
30
43
  createSession: () => createSession,
31
- getPayload: () => getPayload,
32
- getPayloadNames: () => getPayloadNames,
33
- getPayloadsByCategory: () => getPayloadsByCategory,
44
+ driverManager: () => driverManager,
34
45
  installBrowsers: () => installBrowsers,
35
46
  launchBrowser: () => launchBrowser,
36
47
  parseSession: () => parseSession,
48
+ pluginManager: () => pluginManager,
37
49
  serializeSession: () => serializeSession
38
50
  });
39
51
  module.exports = __toCommonJS(index_exports);
40
52
 
41
- // session.ts
53
+ // src/driver-manager.ts
54
+ var import_node_path = require("path");
55
+ var DriverManager = class {
56
+ drivers = /* @__PURE__ */ new Map();
57
+ defaultDriver = null;
58
+ /**
59
+ * Register a driver
60
+ */
61
+ register(driver, source = "builtin") {
62
+ this.validateDriver(driver);
63
+ this.drivers.set(driver.name, { driver, source });
64
+ if (this.drivers.size === 1) {
65
+ this.defaultDriver = driver.name;
66
+ }
67
+ }
68
+ /**
69
+ * Load a driver from npm or local path
70
+ */
71
+ async load(nameOrPath) {
72
+ let driver;
73
+ let source;
74
+ if (nameOrPath.startsWith("./") || nameOrPath.startsWith("../") || (0, import_node_path.isAbsolute)(nameOrPath)) {
75
+ const resolved = (0, import_node_path.isAbsolute)(nameOrPath) ? nameOrPath : (0, import_node_path.resolve)(process.cwd(), nameOrPath);
76
+ const module2 = await import(resolved);
77
+ driver = module2.default || module2;
78
+ source = "local";
79
+ } else {
80
+ const module2 = await import(nameOrPath);
81
+ driver = module2.default || module2;
82
+ source = "npm";
83
+ }
84
+ this.register(driver, source);
85
+ }
86
+ /**
87
+ * Get a loaded driver by name
88
+ */
89
+ get(name) {
90
+ return this.drivers.get(name)?.driver;
91
+ }
92
+ /**
93
+ * Get the default driver
94
+ */
95
+ getDefault() {
96
+ if (!this.defaultDriver) return void 0;
97
+ return this.get(this.defaultDriver);
98
+ }
99
+ /**
100
+ * Set the default driver
101
+ */
102
+ setDefault(name) {
103
+ if (!this.drivers.has(name)) {
104
+ throw new Error(`Driver "${name}" is not registered`);
105
+ }
106
+ this.defaultDriver = name;
107
+ }
108
+ /**
109
+ * Check if a driver is registered
110
+ */
111
+ has(name) {
112
+ return this.drivers.has(name);
113
+ }
114
+ /**
115
+ * Get all registered drivers
116
+ */
117
+ list() {
118
+ return Array.from(this.drivers.values());
119
+ }
120
+ /**
121
+ * Get driver for a session
122
+ */
123
+ getForSession(session) {
124
+ const driverName = session.driver;
125
+ const driver = this.get(driverName);
126
+ if (!driver) {
127
+ throw new Error(
128
+ `Driver "${driverName}" not found. Install @vulcn/driver-${driverName} or load it manually.`
129
+ );
130
+ }
131
+ return driver;
132
+ }
133
+ /**
134
+ * Start recording with a driver
135
+ */
136
+ async startRecording(driverName, config, options = {}) {
137
+ const driver = this.get(driverName);
138
+ if (!driver) {
139
+ throw new Error(`Driver "${driverName}" not found`);
140
+ }
141
+ return driver.recorder.start(config, options);
142
+ }
143
+ /**
144
+ * Execute a session
145
+ */
146
+ async execute(session, pluginManager2, options = {}) {
147
+ const driver = this.getForSession(session);
148
+ const findings = [];
149
+ const logger = this.createLogger(driver.name);
150
+ const ctx = {
151
+ session,
152
+ pluginManager: pluginManager2,
153
+ payloads: pluginManager2.getPayloads(),
154
+ findings,
155
+ addFinding: (finding) => {
156
+ findings.push(finding);
157
+ pluginManager2.addFinding(finding);
158
+ options.onFinding?.(finding);
159
+ },
160
+ logger,
161
+ options
162
+ };
163
+ return driver.runner.execute(session, ctx);
164
+ }
165
+ /**
166
+ * Validate driver structure
167
+ */
168
+ validateDriver(driver) {
169
+ if (!driver || typeof driver !== "object") {
170
+ throw new Error("Driver must be an object");
171
+ }
172
+ const d = driver;
173
+ if (typeof d.name !== "string" || !d.name) {
174
+ throw new Error("Driver must have a name");
175
+ }
176
+ if (typeof d.version !== "string" || !d.version) {
177
+ throw new Error("Driver must have a version");
178
+ }
179
+ if (!Array.isArray(d.stepTypes) || d.stepTypes.length === 0) {
180
+ throw new Error("Driver must define stepTypes");
181
+ }
182
+ if (!d.recorder || typeof d.recorder !== "object") {
183
+ throw new Error("Driver must have a recorder");
184
+ }
185
+ if (!d.runner || typeof d.runner !== "object") {
186
+ throw new Error("Driver must have a runner");
187
+ }
188
+ }
189
+ /**
190
+ * Create a scoped logger for a driver
191
+ */
192
+ createLogger(name) {
193
+ const prefix = `[driver:${name}]`;
194
+ return {
195
+ debug: (msg, ...args) => console.debug(prefix, msg, ...args),
196
+ info: (msg, ...args) => console.info(prefix, msg, ...args),
197
+ warn: (msg, ...args) => console.warn(prefix, msg, ...args),
198
+ error: (msg, ...args) => console.error(prefix, msg, ...args)
199
+ };
200
+ }
201
+ };
202
+ var driverManager = new DriverManager();
203
+
204
+ // src/driver-types.ts
205
+ var DRIVER_API_VERSION = 1;
206
+
207
+ // src/plugin-manager.ts
208
+ var import_promises = require("fs/promises");
209
+ var import_node_fs = require("fs");
210
+ var import_node_path2 = require("path");
211
+ var import_yaml = __toESM(require("yaml"), 1);
42
212
  var import_zod = require("zod");
43
- var import_yaml = require("yaml");
44
- var StepSchema = import_zod.z.discriminatedUnion("type", [
45
- import_zod.z.object({
46
- id: import_zod.z.string(),
47
- type: import_zod.z.literal("navigate"),
48
- url: import_zod.z.string(),
49
- timestamp: import_zod.z.number()
213
+
214
+ // src/plugin-types.ts
215
+ var PLUGIN_API_VERSION = 1;
216
+
217
+ // src/plugin-manager.ts
218
+ var ENGINE_VERSION = "0.2.0";
219
+ var VulcnConfigSchema = import_zod.z.object({
220
+ version: import_zod.z.string().default("1"),
221
+ plugins: import_zod.z.array(
222
+ import_zod.z.object({
223
+ name: import_zod.z.string(),
224
+ config: import_zod.z.record(import_zod.z.unknown()).optional(),
225
+ enabled: import_zod.z.boolean().default(true)
226
+ })
227
+ ).optional(),
228
+ settings: import_zod.z.object({
229
+ browser: import_zod.z.enum(["chromium", "firefox", "webkit"]).optional(),
230
+ headless: import_zod.z.boolean().optional(),
231
+ timeout: import_zod.z.number().optional()
232
+ }).optional()
233
+ });
234
+ var PluginManager = class {
235
+ plugins = [];
236
+ config = null;
237
+ initialized = false;
238
+ /**
239
+ * Shared context passed to all plugins
240
+ */
241
+ sharedPayloads = [];
242
+ sharedFindings = [];
243
+ /**
244
+ * Load configuration from vulcn.config.yml
245
+ */
246
+ async loadConfig(configPath) {
247
+ const paths = configPath ? [configPath] : [
248
+ "vulcn.config.yml",
249
+ "vulcn.config.yaml",
250
+ "vulcn.config.json",
251
+ ".vulcnrc.yml",
252
+ ".vulcnrc.yaml",
253
+ ".vulcnrc.json"
254
+ ];
255
+ for (const path of paths) {
256
+ const resolved = (0, import_node_path2.isAbsolute)(path) ? path : (0, import_node_path2.resolve)(process.cwd(), path);
257
+ if ((0, import_node_fs.existsSync)(resolved)) {
258
+ const content = await (0, import_promises.readFile)(resolved, "utf-8");
259
+ const parsed = path.endsWith(".json") ? JSON.parse(content) : import_yaml.default.parse(content);
260
+ this.config = VulcnConfigSchema.parse(parsed);
261
+ return this.config;
262
+ }
263
+ }
264
+ this.config = { version: "1", plugins: [], settings: {} };
265
+ return this.config;
266
+ }
267
+ /**
268
+ * Load all plugins from config
269
+ */
270
+ async loadPlugins() {
271
+ if (!this.config) {
272
+ await this.loadConfig();
273
+ }
274
+ const pluginConfigs = this.config?.plugins || [];
275
+ for (const pluginConfig of pluginConfigs) {
276
+ if (pluginConfig.enabled === false) continue;
277
+ try {
278
+ const loaded = await this.loadPlugin(pluginConfig);
279
+ this.plugins.push(loaded);
280
+ } catch (err) {
281
+ console.error(
282
+ `Failed to load plugin ${pluginConfig.name}:`,
283
+ err instanceof Error ? err.message : String(err)
284
+ );
285
+ }
286
+ }
287
+ }
288
+ /**
289
+ * Load a single plugin
290
+ */
291
+ async loadPlugin(config) {
292
+ const { name, config: pluginConfig = {} } = config;
293
+ let plugin;
294
+ let source;
295
+ if (name.startsWith("./") || name.startsWith("../") || (0, import_node_path2.isAbsolute)(name)) {
296
+ const resolved = (0, import_node_path2.isAbsolute)(name) ? name : (0, import_node_path2.resolve)(process.cwd(), name);
297
+ const module2 = await import(resolved);
298
+ plugin = module2.default || module2;
299
+ source = "local";
300
+ } else if (name.startsWith("@vulcn/")) {
301
+ const module2 = await import(name);
302
+ plugin = module2.default || module2;
303
+ source = "npm";
304
+ } else {
305
+ const module2 = await import(name);
306
+ plugin = module2.default || module2;
307
+ source = "npm";
308
+ }
309
+ this.validatePlugin(plugin);
310
+ let resolvedConfig = pluginConfig;
311
+ if (plugin.configSchema) {
312
+ try {
313
+ resolvedConfig = plugin.configSchema.parse(pluginConfig);
314
+ } catch (err) {
315
+ throw new Error(
316
+ `Invalid config for plugin ${name}: ${err instanceof Error ? err.message : String(err)}`
317
+ );
318
+ }
319
+ }
320
+ return {
321
+ plugin,
322
+ config: resolvedConfig,
323
+ source,
324
+ enabled: true
325
+ };
326
+ }
327
+ /**
328
+ * Validate plugin structure
329
+ */
330
+ validatePlugin(plugin) {
331
+ if (!plugin || typeof plugin !== "object") {
332
+ throw new Error("Plugin must be an object");
333
+ }
334
+ const p = plugin;
335
+ if (typeof p.name !== "string" || !p.name) {
336
+ throw new Error("Plugin must have a name");
337
+ }
338
+ if (typeof p.version !== "string" || !p.version) {
339
+ throw new Error("Plugin must have a version");
340
+ }
341
+ const apiVersion = p.apiVersion || 1;
342
+ if (apiVersion > PLUGIN_API_VERSION) {
343
+ throw new Error(
344
+ `Plugin requires API version ${apiVersion}, but engine supports ${PLUGIN_API_VERSION}`
345
+ );
346
+ }
347
+ }
348
+ /**
349
+ * Add a plugin programmatically (for testing or dynamic loading)
350
+ */
351
+ addPlugin(plugin, config = {}) {
352
+ this.validatePlugin(plugin);
353
+ this.plugins.push({
354
+ plugin,
355
+ config,
356
+ source: "custom",
357
+ enabled: true
358
+ });
359
+ }
360
+ /**
361
+ * Initialize all plugins (call onInit hooks)
362
+ */
363
+ async initialize() {
364
+ if (this.initialized) return;
365
+ for (const loaded of this.plugins) {
366
+ if (loaded.plugin.payloads) {
367
+ const payloads = typeof loaded.plugin.payloads === "function" ? await loaded.plugin.payloads() : loaded.plugin.payloads;
368
+ this.sharedPayloads.push(...payloads);
369
+ }
370
+ }
371
+ await this.callHook("onInit", (hook, ctx) => hook(ctx));
372
+ this.initialized = true;
373
+ }
374
+ /**
375
+ * Destroy all plugins (call onDestroy hooks)
376
+ */
377
+ async destroy() {
378
+ await this.callHook("onDestroy", (hook, ctx) => hook(ctx));
379
+ this.plugins = [];
380
+ this.sharedPayloads = [];
381
+ this.sharedFindings = [];
382
+ this.initialized = false;
383
+ }
384
+ /**
385
+ * Get all loaded payloads
386
+ */
387
+ getPayloads() {
388
+ return this.sharedPayloads;
389
+ }
390
+ /**
391
+ * Get all collected findings
392
+ */
393
+ getFindings() {
394
+ return this.sharedFindings;
395
+ }
396
+ /**
397
+ * Add a finding (used by detectors)
398
+ */
399
+ addFinding(finding) {
400
+ this.sharedFindings.push(finding);
401
+ }
402
+ /**
403
+ * Add payloads (used by loaders)
404
+ */
405
+ addPayloads(payloads) {
406
+ this.sharedPayloads.push(...payloads);
407
+ }
408
+ /**
409
+ * Clear findings (for new run)
410
+ */
411
+ clearFindings() {
412
+ this.sharedFindings = [];
413
+ }
414
+ /**
415
+ * Get loaded plugins
416
+ */
417
+ getPlugins() {
418
+ return this.plugins;
419
+ }
420
+ /**
421
+ * Check if a plugin is loaded by name
422
+ */
423
+ hasPlugin(name) {
424
+ return this.plugins.some((p) => p.plugin.name === name);
425
+ }
426
+ /**
427
+ * Create base context for plugins
428
+ */
429
+ createContext(pluginConfig) {
430
+ const engineInfo = {
431
+ version: ENGINE_VERSION,
432
+ pluginApiVersion: PLUGIN_API_VERSION
433
+ };
434
+ return {
435
+ config: pluginConfig,
436
+ engine: engineInfo,
437
+ payloads: this.sharedPayloads,
438
+ findings: this.sharedFindings,
439
+ logger: this.createLogger("plugin"),
440
+ fetch: globalThis.fetch
441
+ };
442
+ }
443
+ /**
444
+ * Create scoped logger for a plugin
445
+ */
446
+ createLogger(name) {
447
+ const prefix = `[${name}]`;
448
+ return {
449
+ debug: (msg, ...args) => console.debug(prefix, msg, ...args),
450
+ info: (msg, ...args) => console.info(prefix, msg, ...args),
451
+ warn: (msg, ...args) => console.warn(prefix, msg, ...args),
452
+ error: (msg, ...args) => console.error(prefix, msg, ...args)
453
+ };
454
+ }
455
+ /**
456
+ * Call a hook on all plugins sequentially
457
+ */
458
+ async callHook(hookName, executor) {
459
+ for (const loaded of this.plugins) {
460
+ const hook = loaded.plugin.hooks?.[hookName];
461
+ if (hook) {
462
+ const ctx = this.createContext(loaded.config);
463
+ ctx.logger = this.createLogger(loaded.plugin.name);
464
+ try {
465
+ await executor(hook, ctx);
466
+ } catch (err) {
467
+ console.error(
468
+ `Error in plugin ${loaded.plugin.name}.${hookName}:`,
469
+ err instanceof Error ? err.message : String(err)
470
+ );
471
+ }
472
+ }
473
+ }
474
+ }
475
+ /**
476
+ * Call a hook and collect results
477
+ */
478
+ async callHookCollect(hookName, executor) {
479
+ const results = [];
480
+ for (const loaded of this.plugins) {
481
+ const hook = loaded.plugin.hooks?.[hookName];
482
+ if (hook) {
483
+ const ctx = this.createContext(loaded.config);
484
+ ctx.logger = this.createLogger(loaded.plugin.name);
485
+ try {
486
+ const result = await executor(
487
+ hook,
488
+ ctx
489
+ );
490
+ if (result !== null && result !== void 0) {
491
+ if (Array.isArray(result)) {
492
+ results.push(...result);
493
+ } else {
494
+ results.push(result);
495
+ }
496
+ }
497
+ } catch (err) {
498
+ console.error(
499
+ `Error in plugin ${loaded.plugin.name}.${hookName}:`,
500
+ err instanceof Error ? err.message : String(err)
501
+ );
502
+ }
503
+ }
504
+ }
505
+ return results;
506
+ }
507
+ /**
508
+ * Call a hook that transforms a value through the pipeline
509
+ */
510
+ async callHookPipe(hookName, initial, executor) {
511
+ let value = initial;
512
+ for (const loaded of this.plugins) {
513
+ const hook = loaded.plugin.hooks?.[hookName];
514
+ if (hook) {
515
+ const ctx = this.createContext(loaded.config);
516
+ ctx.logger = this.createLogger(loaded.plugin.name);
517
+ try {
518
+ value = await executor(
519
+ hook,
520
+ value,
521
+ ctx
522
+ );
523
+ } catch (err) {
524
+ console.error(
525
+ `Error in plugin ${loaded.plugin.name}.${hookName}:`,
526
+ err instanceof Error ? err.message : String(err)
527
+ );
528
+ }
529
+ }
530
+ }
531
+ return value;
532
+ }
533
+ };
534
+ var pluginManager = new PluginManager();
535
+
536
+ // src/session.ts
537
+ var import_zod2 = require("zod");
538
+ var import_yaml2 = require("yaml");
539
+ var StepSchema = import_zod2.z.discriminatedUnion("type", [
540
+ import_zod2.z.object({
541
+ id: import_zod2.z.string(),
542
+ type: import_zod2.z.literal("navigate"),
543
+ url: import_zod2.z.string(),
544
+ timestamp: import_zod2.z.number()
50
545
  }),
51
- import_zod.z.object({
52
- id: import_zod.z.string(),
53
- type: import_zod.z.literal("click"),
54
- selector: import_zod.z.string(),
55
- position: import_zod.z.object({ x: import_zod.z.number(), y: import_zod.z.number() }).optional(),
56
- timestamp: import_zod.z.number()
546
+ import_zod2.z.object({
547
+ id: import_zod2.z.string(),
548
+ type: import_zod2.z.literal("click"),
549
+ selector: import_zod2.z.string(),
550
+ position: import_zod2.z.object({ x: import_zod2.z.number(), y: import_zod2.z.number() }).optional(),
551
+ timestamp: import_zod2.z.number()
57
552
  }),
58
- import_zod.z.object({
59
- id: import_zod.z.string(),
60
- type: import_zod.z.literal("input"),
61
- selector: import_zod.z.string(),
62
- value: import_zod.z.string(),
63
- injectable: import_zod.z.boolean().optional().default(true),
64
- timestamp: import_zod.z.number()
553
+ import_zod2.z.object({
554
+ id: import_zod2.z.string(),
555
+ type: import_zod2.z.literal("input"),
556
+ selector: import_zod2.z.string(),
557
+ value: import_zod2.z.string(),
558
+ injectable: import_zod2.z.boolean().optional().default(true),
559
+ timestamp: import_zod2.z.number()
65
560
  }),
66
- import_zod.z.object({
67
- id: import_zod.z.string(),
68
- type: import_zod.z.literal("keypress"),
69
- key: import_zod.z.string(),
70
- modifiers: import_zod.z.array(import_zod.z.string()).optional(),
71
- timestamp: import_zod.z.number()
561
+ import_zod2.z.object({
562
+ id: import_zod2.z.string(),
563
+ type: import_zod2.z.literal("keypress"),
564
+ key: import_zod2.z.string(),
565
+ modifiers: import_zod2.z.array(import_zod2.z.string()).optional(),
566
+ timestamp: import_zod2.z.number()
72
567
  }),
73
- import_zod.z.object({
74
- id: import_zod.z.string(),
75
- type: import_zod.z.literal("scroll"),
76
- selector: import_zod.z.string().optional(),
77
- position: import_zod.z.object({ x: import_zod.z.number(), y: import_zod.z.number() }),
78
- timestamp: import_zod.z.number()
568
+ import_zod2.z.object({
569
+ id: import_zod2.z.string(),
570
+ type: import_zod2.z.literal("scroll"),
571
+ selector: import_zod2.z.string().optional(),
572
+ position: import_zod2.z.object({ x: import_zod2.z.number(), y: import_zod2.z.number() }),
573
+ timestamp: import_zod2.z.number()
79
574
  }),
80
- import_zod.z.object({
81
- id: import_zod.z.string(),
82
- type: import_zod.z.literal("wait"),
83
- duration: import_zod.z.number(),
84
- timestamp: import_zod.z.number()
575
+ import_zod2.z.object({
576
+ id: import_zod2.z.string(),
577
+ type: import_zod2.z.literal("wait"),
578
+ duration: import_zod2.z.number(),
579
+ timestamp: import_zod2.z.number()
85
580
  })
86
581
  ]);
87
- var SessionSchema = import_zod.z.object({
88
- version: import_zod.z.string().default("1"),
89
- name: import_zod.z.string(),
90
- recordedAt: import_zod.z.string(),
91
- browser: import_zod.z.enum(["chromium", "firefox", "webkit"]).default("chromium"),
92
- viewport: import_zod.z.object({
93
- width: import_zod.z.number(),
94
- height: import_zod.z.number()
582
+ var SessionSchema = import_zod2.z.object({
583
+ version: import_zod2.z.string().default("1"),
584
+ name: import_zod2.z.string(),
585
+ recordedAt: import_zod2.z.string(),
586
+ browser: import_zod2.z.enum(["chromium", "firefox", "webkit"]).default("chromium"),
587
+ viewport: import_zod2.z.object({
588
+ width: import_zod2.z.number(),
589
+ height: import_zod2.z.number()
95
590
  }),
96
- startUrl: import_zod.z.string(),
97
- steps: import_zod.z.array(StepSchema)
591
+ startUrl: import_zod2.z.string(),
592
+ steps: import_zod2.z.array(StepSchema)
98
593
  });
99
594
  function createSession(options) {
100
595
  return {
@@ -108,14 +603,14 @@ function createSession(options) {
108
603
  };
109
604
  }
110
605
  function parseSession(yaml) {
111
- const data = (0, import_yaml.parse)(yaml);
606
+ const data = (0, import_yaml2.parse)(yaml);
112
607
  return SessionSchema.parse(data);
113
608
  }
114
609
  function serializeSession(session) {
115
- return (0, import_yaml.stringify)(session, { lineWidth: 0 });
610
+ return (0, import_yaml2.stringify)(session, { lineWidth: 0 });
116
611
  }
117
612
 
118
- // browser.ts
613
+ // src/browser.ts
119
614
  var import_playwright = require("playwright");
120
615
  var import_node_child_process = require("child_process");
121
616
  var import_node_util = require("util");
@@ -228,16 +723,18 @@ async function checkBrowsers() {
228
723
  return results;
229
724
  }
230
725
 
231
- // recorder.ts
726
+ // src/recorder.ts
232
727
  var Recorder = class _Recorder {
233
728
  /**
234
729
  * Start a new recording session
235
730
  * Opens a browser window for the user to interact with
236
731
  */
237
- static async start(startUrl, options = {}) {
732
+ static async start(startUrl, options = {}, config = {}) {
733
+ const manager = config.pluginManager ?? pluginManager;
238
734
  const browserType = options.browser ?? "chromium";
239
735
  const viewport = options.viewport ?? { width: 1280, height: 720 };
240
736
  const headless = options.headless ?? false;
737
+ await manager.initialize();
241
738
  const { browser } = await launchBrowser({
242
739
  browser: browserType,
243
740
  headless
@@ -258,18 +755,61 @@ var Recorder = class _Recorder {
258
755
  stepCounter++;
259
756
  return `step_${String(stepCounter).padStart(3, "0")}`;
260
757
  };
261
- steps.push({
758
+ const baseRecordContext = {
759
+ startUrl,
760
+ browser: browserType,
761
+ page,
762
+ engine: { version: "0.2.0", pluginApiVersion: 1 },
763
+ payloads: manager.getPayloads(),
764
+ findings: manager.getFindings(),
765
+ logger: {
766
+ debug: console.debug.bind(console),
767
+ info: console.info.bind(console),
768
+ warn: console.warn.bind(console),
769
+ error: console.error.bind(console)
770
+ },
771
+ fetch: globalThis.fetch
772
+ };
773
+ await manager.callHook("onRecordStart", async (hook, ctx) => {
774
+ const recordCtx = { ...baseRecordContext, ...ctx };
775
+ await hook(recordCtx);
776
+ });
777
+ const initialStep = {
262
778
  id: generateStepId(),
263
779
  type: "navigate",
264
780
  url: startUrl,
265
781
  timestamp: 0
266
- });
267
- _Recorder.attachListeners(page, steps, startTime, generateStepId);
782
+ };
783
+ const transformedInitialStep = await _Recorder.transformStep(
784
+ initialStep,
785
+ manager,
786
+ baseRecordContext
787
+ );
788
+ if (transformedInitialStep) {
789
+ steps.push(transformedInitialStep);
790
+ }
791
+ _Recorder.attachListeners(
792
+ page,
793
+ steps,
794
+ startTime,
795
+ generateStepId,
796
+ manager,
797
+ baseRecordContext
798
+ );
268
799
  return {
269
800
  async stop() {
270
801
  session.steps = steps;
802
+ let finalSession = session;
803
+ for (const loaded of manager.getPlugins()) {
804
+ const hook = loaded.plugin.hooks?.onRecordEnd;
805
+ if (hook) {
806
+ const ctx = manager.createContext(loaded.config);
807
+ const recordCtx = { ...baseRecordContext, ...ctx };
808
+ finalSession = await hook(finalSession, recordCtx);
809
+ }
810
+ }
271
811
  await browser.close();
272
- return session;
812
+ return finalSession;
273
813
  },
274
814
  getSteps() {
275
815
  return [...steps];
@@ -279,8 +819,34 @@ var Recorder = class _Recorder {
279
819
  }
280
820
  };
281
821
  }
282
- static attachListeners(page, steps, startTime, generateStepId) {
822
+ /**
823
+ * Transform a step through plugin hooks
824
+ * Returns null if the step should be filtered out
825
+ */
826
+ static async transformStep(step, manager, baseContext) {
827
+ let transformedStep = step;
828
+ for (const loaded of manager.getPlugins()) {
829
+ const hook = loaded.plugin.hooks?.onRecordStep;
830
+ if (hook) {
831
+ const ctx = manager.createContext(loaded.config);
832
+ const recordCtx = { ...baseContext, ...ctx };
833
+ transformedStep = await hook(transformedStep, recordCtx);
834
+ }
835
+ }
836
+ return transformedStep;
837
+ }
838
+ static attachListeners(page, steps, startTime, generateStepId, manager, baseContext) {
283
839
  const getTimestamp = () => Date.now() - startTime;
840
+ const addStep = async (step) => {
841
+ const transformed = await _Recorder.transformStep(
842
+ step,
843
+ manager,
844
+ baseContext
845
+ );
846
+ if (transformed) {
847
+ steps.push(transformed);
848
+ }
849
+ };
284
850
  page.on("framenavigated", (frame) => {
285
851
  if (frame === page.mainFrame()) {
286
852
  const url = frame.url();
@@ -288,7 +854,7 @@ var Recorder = class _Recorder {
288
854
  if (steps.length > 0 && lastStep.type === "navigate" && lastStep.url === url) {
289
855
  return;
290
856
  }
291
- steps.push({
857
+ addStep({
292
858
  id: generateStepId(),
293
859
  type: "navigate",
294
860
  url,
@@ -298,12 +864,12 @@ var Recorder = class _Recorder {
298
864
  });
299
865
  page.exposeFunction(
300
866
  "__vulcn_record",
301
- (event) => {
867
+ async (event) => {
302
868
  const timestamp = getTimestamp();
303
869
  switch (event.type) {
304
870
  case "click": {
305
871
  const data = event.data;
306
- steps.push({
872
+ await addStep({
307
873
  id: generateStepId(),
308
874
  type: "click",
309
875
  selector: data.selector,
@@ -314,7 +880,7 @@ var Recorder = class _Recorder {
314
880
  }
315
881
  case "input": {
316
882
  const data = event.data;
317
- steps.push({
883
+ await addStep({
318
884
  id: generateStepId(),
319
885
  type: "input",
320
886
  selector: data.selector,
@@ -326,7 +892,7 @@ var Recorder = class _Recorder {
326
892
  }
327
893
  case "keypress": {
328
894
  const data = event.data;
329
- steps.push({
895
+ await addStep({
330
896
  id: generateStepId(),
331
897
  type: "keypress",
332
898
  key: data.key,
@@ -463,163 +1029,156 @@ var Recorder = class _Recorder {
463
1029
  }
464
1030
  };
465
1031
 
466
- // payloads.ts
467
- var BUILTIN_PAYLOADS = {
468
- "xss-basic": {
469
- name: "xss-basic",
470
- category: "xss",
471
- description: "Basic XSS payloads with script tags and event handlers",
472
- payloads: [
473
- '<script>alert("XSS")</script>',
474
- '<img src=x onerror=alert("XSS")>',
475
- '"><script>alert("XSS")</script>',
476
- "javascript:alert('XSS')",
477
- '<svg onload=alert("XSS")>'
478
- ],
479
- detectPatterns: [
480
- /<script[^>]*>alert\(/i,
481
- /onerror\s*=\s*alert\(/i,
482
- /onload\s*=\s*alert\(/i,
483
- /javascript:alert\(/i
484
- ]
485
- },
486
- "xss-event": {
487
- name: "xss-event",
488
- category: "xss",
489
- description: "XSS via event handlers",
490
- payloads: [
491
- '" onfocus="alert(1)" autofocus="',
492
- "' onmouseover='alert(1)'",
493
- '<body onload=alert("XSS")>',
494
- "<input onfocus=alert(1) autofocus>",
495
- "<marquee onstart=alert(1)>"
496
- ],
497
- detectPatterns: [
498
- /onfocus\s*=\s*["']?alert/i,
499
- /onmouseover\s*=\s*["']?alert/i,
500
- /onload\s*=\s*["']?alert/i,
501
- /onstart\s*=\s*["']?alert/i
502
- ]
503
- },
504
- "xss-svg": {
505
- name: "xss-svg",
506
- category: "xss",
507
- description: "XSS via SVG elements",
508
- payloads: [
509
- '<svg/onload=alert("XSS")>',
510
- "<svg><script>alert(1)</script></svg>",
511
- "<svg><animate onbegin=alert(1)>",
512
- "<svg><set onbegin=alert(1)>"
513
- ],
514
- detectPatterns: [
515
- /<svg[^>]*onload\s*=/i,
516
- /<svg[^>]*>.*<script>/i,
517
- /onbegin\s*=\s*alert/i
518
- ]
519
- },
520
- "sqli-basic": {
521
- name: "sqli-basic",
522
- category: "sqli",
523
- description: "Basic SQL injection payloads",
524
- payloads: [
525
- "' OR '1'='1",
526
- "' OR '1'='1' --",
527
- "1' OR '1'='1",
528
- "admin'--",
529
- "' UNION SELECT NULL--"
530
- ],
531
- detectPatterns: [
532
- /sql.*syntax/i,
533
- /mysql.*error/i,
534
- /ORA-\d{5}/i,
535
- /pg_query/i,
536
- /sqlite.*error/i,
537
- /unclosed.*quotation/i
538
- ]
539
- },
540
- "sqli-error": {
541
- name: "sqli-error",
542
- category: "sqli",
543
- description: "SQL injection payloads to trigger errors",
544
- payloads: ["'", "''", "`", '"', "')", `'"`, "1' AND '1'='2", "1 AND 1=2"],
545
- detectPatterns: [
546
- /sql.*syntax/i,
547
- /mysql.*error/i,
548
- /ORA-\d{5}/i,
549
- /postgresql.*error/i,
550
- /sqlite.*error/i,
551
- /quoted.*string.*properly.*terminated/i
552
- ]
553
- },
554
- "sqli-blind": {
555
- name: "sqli-blind",
556
- category: "sqli",
557
- description: "Blind SQL injection payloads",
558
- payloads: [
559
- "1' AND SLEEP(5)--",
560
- "1; WAITFOR DELAY '0:0:5'--",
561
- "1' AND (SELECT COUNT(*) FROM information_schema.tables)>0--"
562
- ],
563
- detectPatterns: [
564
- // Blind SQLi is detected by timing, not content
565
- ]
566
- }
567
- };
568
- function getPayload(name) {
569
- return BUILTIN_PAYLOADS[name];
570
- }
571
- function getPayloadNames() {
572
- return Object.keys(BUILTIN_PAYLOADS);
573
- }
574
- function getPayloadsByCategory(category) {
575
- return Object.values(BUILTIN_PAYLOADS).filter((p) => p.category === category);
576
- }
577
-
578
- // runner.ts
1032
+ // src/runner.ts
579
1033
  var Runner = class _Runner {
580
1034
  /**
581
- * Execute a session with security payloads
1035
+ * Execute a session with security payloads from plugins
1036
+ *
1037
+ * @param session - The recorded session to replay
1038
+ * @param options - Runner configuration
1039
+ * @param config - Plugin manager configuration
582
1040
  */
583
- static async execute(session, payloadNames, options = {}) {
1041
+ static async execute(session, options = {}, config = {}) {
1042
+ const manager = config.pluginManager ?? pluginManager;
584
1043
  const browserType = options.browser ?? session.browser ?? "chromium";
585
1044
  const headless = options.headless ?? true;
586
1045
  const startTime = Date.now();
587
- const findings = [];
588
1046
  const errors = [];
589
1047
  let payloadsTested = 0;
1048
+ await manager.initialize();
1049
+ manager.clearFindings();
1050
+ const payloads = manager.getPayloads();
1051
+ if (payloads.length === 0) {
1052
+ return {
1053
+ findings: [],
1054
+ stepsExecuted: session.steps.length,
1055
+ payloadsTested: 0,
1056
+ duration: Date.now() - startTime,
1057
+ errors: [
1058
+ "No payloads loaded. Add a payload plugin or configure payloads."
1059
+ ]
1060
+ };
1061
+ }
590
1062
  const { browser } = await launchBrowser({
591
1063
  browser: browserType,
592
1064
  headless
593
1065
  });
594
1066
  const context = await browser.newContext({ viewport: session.viewport });
595
1067
  const page = await context.newPage();
1068
+ const baseRunContext = {
1069
+ session,
1070
+ page,
1071
+ browser: browserType,
1072
+ headless,
1073
+ engine: { version: "0.2.0", pluginApiVersion: 1 },
1074
+ payloads: manager.getPayloads(),
1075
+ findings: manager.getFindings(),
1076
+ logger: {
1077
+ debug: console.debug.bind(console),
1078
+ info: console.info.bind(console),
1079
+ warn: console.warn.bind(console),
1080
+ error: console.error.bind(console)
1081
+ },
1082
+ fetch: globalThis.fetch
1083
+ };
1084
+ await manager.callHook("onRunStart", async (hook, ctx) => {
1085
+ const runCtx = { ...baseRunContext, ...ctx };
1086
+ await hook(runCtx);
1087
+ });
1088
+ const eventFindings = [];
1089
+ let currentDetectContext = null;
1090
+ const dialogHandler = async (dialog) => {
1091
+ if (currentDetectContext) {
1092
+ const findings = await manager.callHookCollect(
1093
+ "onDialog",
1094
+ async (hook, ctx) => {
1095
+ const detectCtx = {
1096
+ ...currentDetectContext,
1097
+ ...ctx
1098
+ };
1099
+ return hook(dialog, detectCtx);
1100
+ }
1101
+ );
1102
+ eventFindings.push(...findings);
1103
+ }
1104
+ try {
1105
+ await dialog.dismiss();
1106
+ } catch {
1107
+ }
1108
+ };
1109
+ const consoleHandler = async (msg) => {
1110
+ if (currentDetectContext) {
1111
+ const findings = await manager.callHookCollect("onConsoleMessage", async (hook, ctx) => {
1112
+ const detectCtx = { ...currentDetectContext, ...ctx };
1113
+ return hook(msg, detectCtx);
1114
+ });
1115
+ eventFindings.push(...findings);
1116
+ }
1117
+ };
1118
+ page.on("dialog", dialogHandler);
1119
+ page.on("console", consoleHandler);
596
1120
  try {
597
1121
  const injectableSteps = session.steps.filter(
598
1122
  (step) => step.type === "input" && step.injectable !== false
599
1123
  );
600
1124
  const allPayloads = [];
601
- for (const name of payloadNames) {
602
- const payload = BUILTIN_PAYLOADS[name];
603
- if (payload) {
604
- for (const value of payload.payloads) {
605
- allPayloads.push({ name, value });
606
- }
1125
+ for (const payloadSet of payloads) {
1126
+ for (const value of payloadSet.payloads) {
1127
+ allPayloads.push({ payloadSet, value });
607
1128
  }
608
1129
  }
609
1130
  for (const injectableStep of injectableSteps) {
610
- for (const payload of allPayloads) {
1131
+ for (const { payloadSet, value: originalValue } of allPayloads) {
611
1132
  try {
612
- const finding = await _Runner.replayWithPayload(
1133
+ let transformedPayload = originalValue;
1134
+ for (const loaded of manager.getPlugins()) {
1135
+ const hook = loaded.plugin.hooks?.onBeforePayload;
1136
+ if (hook) {
1137
+ const ctx = manager.createContext(loaded.config);
1138
+ const runCtx = { ...baseRunContext, ...ctx };
1139
+ transformedPayload = await hook(
1140
+ transformedPayload,
1141
+ injectableStep,
1142
+ runCtx
1143
+ );
1144
+ }
1145
+ }
1146
+ currentDetectContext = {
1147
+ ...baseRunContext,
1148
+ config: {},
1149
+ step: injectableStep,
1150
+ payloadSet,
1151
+ payloadValue: transformedPayload,
1152
+ stepId: injectableStep.id
1153
+ };
1154
+ await _Runner.replayWithPayload(
613
1155
  page,
614
1156
  session,
615
1157
  injectableStep,
616
- payload.name,
617
- payload.value
1158
+ transformedPayload
618
1159
  );
619
- if (finding) {
620
- findings.push(finding);
1160
+ const afterFindings = await manager.callHookCollect("onAfterPayload", async (hook, ctx) => {
1161
+ const detectCtx = {
1162
+ ...currentDetectContext,
1163
+ ...ctx
1164
+ };
1165
+ return hook(detectCtx);
1166
+ });
1167
+ const reflectionFinding = await _Runner.checkReflection(
1168
+ page,
1169
+ injectableStep,
1170
+ payloadSet,
1171
+ transformedPayload
1172
+ );
1173
+ const allFindings = [...afterFindings, ...eventFindings];
1174
+ if (reflectionFinding) {
1175
+ allFindings.push(reflectionFinding);
1176
+ }
1177
+ for (const finding of allFindings) {
1178
+ manager.addFinding(finding);
621
1179
  options.onFinding?.(finding);
622
1180
  }
1181
+ eventFindings.length = 0;
623
1182
  payloadsTested++;
624
1183
  } catch (err) {
625
1184
  errors.push(`${injectableStep.id}: ${String(err)}`);
@@ -627,73 +1186,94 @@ var Runner = class _Runner {
627
1186
  }
628
1187
  }
629
1188
  } finally {
1189
+ page.off("dialog", dialogHandler);
1190
+ page.off("console", consoleHandler);
1191
+ currentDetectContext = null;
630
1192
  await browser.close();
631
1193
  }
632
- return {
633
- findings,
1194
+ let result = {
1195
+ findings: manager.getFindings(),
634
1196
  stepsExecuted: session.steps.length,
635
1197
  payloadsTested,
636
1198
  duration: Date.now() - startTime,
637
1199
  errors
638
1200
  };
1201
+ for (const loaded of manager.getPlugins()) {
1202
+ const hook = loaded.plugin.hooks?.onRunEnd;
1203
+ if (hook) {
1204
+ const ctx = manager.createContext(loaded.config);
1205
+ const runCtx = { ...baseRunContext, ...ctx };
1206
+ result = await hook(result, runCtx);
1207
+ }
1208
+ }
1209
+ return result;
639
1210
  }
640
- static async replayWithPayload(page, session, targetStep, payloadName, payloadValue) {
641
- await page.goto(session.startUrl);
1211
+ /**
1212
+ * Execute with explicit payloads (legacy API, for backwards compatibility)
1213
+ */
1214
+ static async executeWithPayloads(session, payloads, options = {}) {
1215
+ const manager = new PluginManager();
1216
+ manager.addPayloads(payloads);
1217
+ return _Runner.execute(session, options, { pluginManager: manager });
1218
+ }
1219
+ /**
1220
+ * Replay session steps with payload injected at target step
1221
+ */
1222
+ static async replayWithPayload(page, session, targetStep, payloadValue) {
1223
+ await page.goto(session.startUrl, { waitUntil: "domcontentloaded" });
642
1224
  for (const step of session.steps) {
643
- if (step.type === "navigate") {
644
- await page.goto(step.url);
645
- } else if (step.type === "click") {
646
- await page.click(step.selector, { timeout: 5e3 });
647
- } else if (step.type === "input") {
648
- const value = step.id === targetStep.id ? payloadValue : step.value;
649
- await page.fill(step.selector, value, { timeout: 5e3 });
650
- } else if (step.type === "keypress") {
651
- const modifiers = step.modifiers ?? [];
652
- for (const mod of modifiers) {
653
- await page.keyboard.down(mod);
654
- }
655
- await page.keyboard.press(step.key);
656
- for (const mod of modifiers.reverse()) {
657
- await page.keyboard.up(mod);
1225
+ try {
1226
+ if (step.type === "navigate") {
1227
+ await page.goto(step.url, { waitUntil: "domcontentloaded" });
1228
+ } else if (step.type === "click") {
1229
+ await page.click(step.selector, { timeout: 5e3 });
1230
+ } else if (step.type === "input") {
1231
+ const value = step.id === targetStep.id ? payloadValue : step.value;
1232
+ await page.fill(step.selector, value, { timeout: 5e3 });
1233
+ } else if (step.type === "keypress") {
1234
+ const modifiers = step.modifiers ?? [];
1235
+ for (const mod of modifiers) {
1236
+ await page.keyboard.down(
1237
+ mod
1238
+ );
1239
+ }
1240
+ await page.keyboard.press(step.key);
1241
+ for (const mod of modifiers.reverse()) {
1242
+ await page.keyboard.up(mod);
1243
+ }
658
1244
  }
1245
+ } catch {
659
1246
  }
660
1247
  if (step.id === targetStep.id) {
661
- const finding = await _Runner.checkForVulnerability(
662
- page,
663
- targetStep,
664
- payloadName,
665
- payloadValue
666
- );
667
- if (finding) {
668
- return finding;
669
- }
1248
+ await page.waitForTimeout(100);
1249
+ break;
670
1250
  }
671
1251
  }
672
- return void 0;
673
1252
  }
674
- static async checkForVulnerability(page, step, payloadName, payloadValue) {
675
- const payload = BUILTIN_PAYLOADS[payloadName];
676
- if (!payload) return void 0;
1253
+ /**
1254
+ * Basic reflection check - fallback when no detection plugin is loaded
1255
+ */
1256
+ static async checkReflection(page, step, payloadSet, payloadValue) {
677
1257
  const content = await page.content();
678
- for (const pattern of payload.detectPatterns) {
1258
+ for (const pattern of payloadSet.detectPatterns) {
679
1259
  if (pattern.test(content)) {
680
1260
  return {
681
- type: payload.category,
682
- severity: payload.category === "xss" ? "high" : "critical",
683
- title: `${payload.category.toUpperCase()} vulnerability detected`,
684
- description: `Payload was reflected in page content`,
1261
+ type: payloadSet.category,
1262
+ severity: _Runner.getSeverity(payloadSet.category),
1263
+ title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
1264
+ description: `Payload pattern was reflected in page content`,
685
1265
  stepId: step.id,
686
1266
  payload: payloadValue,
687
1267
  url: page.url(),
688
- evidence: content.match(pattern)?.[0]
1268
+ evidence: content.match(pattern)?.[0]?.slice(0, 200)
689
1269
  };
690
1270
  }
691
1271
  }
692
1272
  if (content.includes(payloadValue)) {
693
1273
  return {
694
- type: payload.category,
1274
+ type: payloadSet.category,
695
1275
  severity: "medium",
696
- title: `Potential ${payload.category.toUpperCase()} - payload reflection`,
1276
+ title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
697
1277
  description: `Payload was reflected in page without encoding`,
698
1278
  stepId: step.id,
699
1279
  payload: payloadValue,
@@ -702,23 +1282,44 @@ var Runner = class _Runner {
702
1282
  }
703
1283
  return void 0;
704
1284
  }
1285
+ /**
1286
+ * Determine severity based on vulnerability category
1287
+ */
1288
+ static getSeverity(category) {
1289
+ switch (category) {
1290
+ case "sqli":
1291
+ case "command-injection":
1292
+ case "xxe":
1293
+ return "critical";
1294
+ case "xss":
1295
+ case "ssrf":
1296
+ case "path-traversal":
1297
+ return "high";
1298
+ case "open-redirect":
1299
+ return "medium";
1300
+ default:
1301
+ return "medium";
1302
+ }
1303
+ }
705
1304
  };
706
1305
  // Annotate the CommonJS export names for ESM import in node:
707
1306
  0 && (module.exports = {
708
- BUILTIN_PAYLOADS,
709
1307
  BrowserNotFoundError,
1308
+ DRIVER_API_VERSION,
1309
+ DriverManager,
1310
+ PLUGIN_API_VERSION,
1311
+ PluginManager,
710
1312
  Recorder,
711
1313
  Runner,
712
1314
  SessionSchema,
713
1315
  StepSchema,
714
1316
  checkBrowsers,
715
1317
  createSession,
716
- getPayload,
717
- getPayloadNames,
718
- getPayloadsByCategory,
1318
+ driverManager,
719
1319
  installBrowsers,
720
1320
  launchBrowser,
721
1321
  parseSession,
1322
+ pluginManager,
722
1323
  serializeSession
723
1324
  });
724
1325
  //# sourceMappingURL=index.cjs.map