@vulcn/engine 0.5.0 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 15d8504: ### Authenticated Scanning
8
+
9
+ End-to-end support for scanning applications behind login pages.
10
+
11
+ #### `@vulcn/engine`
12
+ - **Credential encryption module** (`src/auth.ts`): AES-256-GCM encryption/decryption for credentials and Playwright storage state, with PBKDF2 key derivation (600k iterations)
13
+ - **Auth types**: `FormCredentials`, `HeaderCredentials`, `AuthConfig` with session expiry detection config
14
+ - **Scan-level hooks**: `onScanStart` / `onScanEnd` — fire once per scan wrapping all sessions, with `ScanContext` providing full session list and scan metadata
15
+ - **`onScanEnd` result transformation**: uses `callHookPipe` so plugins can transform the aggregate `RunResult` (e.g. deduplication, risk scoring)
16
+ - **v2 session format**: `.vulcn/` directory structure with manifest, encrypted auth state, and config alongside session files
17
+ - **`CrawlOptions.storageState`**: pass authenticated browser state (cookies + localStorage) to the crawler
18
+ - **New exports**: `ScanContext`, `encryptCredentials`, `decryptCredentials`, `encryptStorageState`, `decryptStorageState`, `getPassphrase`
19
+
20
+ #### `@vulcn/driver-browser`
21
+ - **Authenticated crawling**: `crawlAndBuildSessions` accepts `storageState` via `CrawlOptions` and injects it into the Playwright browser context
22
+ - **Authenticated scanning**: `BrowserRunner` reads `storageState` from `RunOptions` and applies it to the scanner's browser context
23
+ - **Login form auto-detection**: `performLogin` navigates to the login URL, auto-detects username/password fields, fills credentials, and submits
24
+ - **Storage state capture**: after successful login, captures full browser storage state (cookies, localStorage, sessionStorage)
25
+
26
+ #### `vulcn` (CLI)
27
+ - **`vulcn store`**: new command to encrypt and save credentials (form-based or header-based) to `.vulcn/auth.enc`
28
+ - **`vulcn crawl --creds`**: decrypt credentials → perform login → capture storage state → crawl all authenticated pages
29
+ - **`vulcn run --creds`**: decrypt credentials → perform login → inject storage state into scanner browser context → run all payloads authenticated
30
+ - **Auth state persistence**: crawl saves encrypted auth state + config alongside sessions in the output directory
31
+
32
+ ## 0.7.0
33
+
34
+ ### Minor Changes
35
+
36
+ - 458572e: ### @vulcn/engine
37
+ - **`addFinding` on PluginContext**: Plugins now have `ctx.addFinding()` to report findings through the proper callback chain. This ensures consumers are notified via `onFinding` and findings survive timeouts. Plugins should use this instead of `ctx.findings.push()`.
38
+ - **`onPageReady` callback**: New `RunOptions.onPageReady` callback fires after the driver creates the browser page. The engine uses this to defer `onRunStart` plugin hooks until the page is ready, so plugins receive a real page object (not `null`).
39
+ - **`onBeforeClose` hook**: New plugin lifecycle hook called before the browser is closed. Plugins can flush in-flight async work here (e.g., pending response handlers that need browser access).
40
+ - **`onBeforeClose` callback**: New `RunOptions.onBeforeClose` callback fires before browser teardown, triggering plugin `onBeforeClose` hooks.
41
+
42
+ ### @vulcn/driver-browser
43
+ - **`onPageReady` signal**: Runner now calls `ctx.options.onPageReady(page)` after creating the browser page, enabling plugins to attach event listeners before any navigation occurs.
44
+ - **`onBeforeClose` signal**: Runner now calls `ctx.options.onBeforeClose(page)` before `browser.close()`, giving plugins time to drain pending async work.
45
+ - **Payload interleaving**: Payloads are now ordered round-robin across categories (e.g., `[sqli1, xss1, sqli2, xss2, ...]`) instead of sequentially. This ensures faster category coverage and earlier dedup early-breaks on slow SPAs.
46
+ - **Extended dedup early-break**: The per-step category dedup now treats any finding (dialog, console, or reflection) as confirmation, not just dialog-based detections. One confirmed finding per category per step is sufficient.
47
+
48
+ ### @vulcn/plugin-passive
49
+ - **Uses `ctx.addFinding()`**: All findings are now reported through the proper callback chain instead of pushing to `ctx.findings` directly. This fixes passive findings being invisible to `onFinding` consumers.
50
+ - **Cross-session dedup**: `reportedFindings` is no longer cleared between sessions, so the same passive finding (e.g., "Missing CSP" on the same origin) is reported once per scan, not once per crawled form.
51
+ - **Async handler drain**: Response handlers are tracked as promises and drained in the new `onBeforeClose` hook, preventing findings from being lost when the browser closes before async `response.allHeaders()` calls complete.
52
+
3
53
  ## 0.5.0
4
54
 
5
55
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -34,8 +34,21 @@ __export(index_exports, {
34
34
  DriverManager: () => DriverManager,
35
35
  PLUGIN_API_VERSION: () => PLUGIN_API_VERSION,
36
36
  PluginManager: () => PluginManager,
37
+ decrypt: () => decrypt,
38
+ decryptCredentials: () => decryptCredentials,
39
+ decryptStorageState: () => decryptStorageState,
37
40
  driverManager: () => driverManager,
38
- pluginManager: () => pluginManager
41
+ encrypt: () => encrypt,
42
+ encryptCredentials: () => encryptCredentials,
43
+ encryptStorageState: () => encryptStorageState,
44
+ getPassphrase: () => getPassphrase,
45
+ isSessionDir: () => isSessionDir,
46
+ loadSessionDir: () => loadSessionDir,
47
+ looksLikeSessionDir: () => looksLikeSessionDir,
48
+ pluginManager: () => pluginManager,
49
+ readAuthState: () => readAuthState,
50
+ readCapturedRequests: () => readCapturedRequests,
51
+ saveSessionDir: () => saveSessionDir
39
52
  });
40
53
  module.exports = __toCommonJS(index_exports);
41
54
 
@@ -197,23 +210,17 @@ var DriverManager = class {
197
210
  /**
198
211
  * Execute a session
199
212
  * Invokes plugin hooks (onRunStart, onRunEnd) around the driver runner.
213
+ * Plugin onRunStart is deferred until the driver signals the page is ready
214
+ * via the onPageReady callback, ensuring plugins get a real page object.
200
215
  */
201
216
  async execute(session, pluginManager2, options = {}) {
202
217
  const driver = this.getForSession(session);
203
218
  const findings = [];
204
219
  const logger = this.createLogger(driver.name);
205
- const ctx = {
206
- session,
207
- pluginManager: pluginManager2,
208
- payloads: pluginManager2.getPayloads(),
209
- findings,
210
- addFinding: (finding) => {
211
- findings.push(finding);
212
- pluginManager2.addFinding(finding);
213
- options.onFinding?.(finding);
214
- },
215
- logger,
216
- options
220
+ const addFinding = (finding) => {
221
+ findings.push(finding);
222
+ pluginManager2.addFinding(finding);
223
+ options.onFinding?.(finding);
217
224
  };
218
225
  const pluginCtx = {
219
226
  session,
@@ -223,21 +230,57 @@ var DriverManager = class {
223
230
  engine: { version: "0.3.0", pluginApiVersion: 1 },
224
231
  payloads: pluginManager2.getPayloads(),
225
232
  findings,
233
+ addFinding,
226
234
  logger,
227
235
  fetch: globalThis.fetch
228
236
  };
229
- for (const loaded of pluginManager2.getPlugins()) {
230
- if (loaded.enabled && loaded.plugin.hooks?.onRunStart) {
231
- try {
232
- await loaded.plugin.hooks.onRunStart({
233
- ...pluginCtx,
234
- config: loaded.config
235
- });
236
- } catch (err) {
237
- logger.warn(`Plugin ${loaded.plugin.name} onRunStart failed: ${err}`);
237
+ const ctx = {
238
+ session,
239
+ pluginManager: pluginManager2,
240
+ payloads: pluginManager2.getPayloads(),
241
+ findings,
242
+ addFinding,
243
+ logger,
244
+ options: {
245
+ ...options,
246
+ // Provide onPageReady callback — fires plugin onRunStart hooks
247
+ // with the real page object once the driver has created it
248
+ onPageReady: async (page) => {
249
+ pluginCtx.page = page;
250
+ for (const loaded of pluginManager2.getPlugins()) {
251
+ if (loaded.enabled && loaded.plugin.hooks?.onRunStart) {
252
+ try {
253
+ await loaded.plugin.hooks.onRunStart({
254
+ ...pluginCtx,
255
+ config: loaded.config
256
+ });
257
+ } catch (err) {
258
+ logger.warn(
259
+ `Plugin ${loaded.plugin.name} onRunStart failed: ${err}`
260
+ );
261
+ }
262
+ }
263
+ }
264
+ },
265
+ // Fires before browser closes — lets plugins flush pending async work
266
+ onBeforeClose: async (_page) => {
267
+ for (const loaded of pluginManager2.getPlugins()) {
268
+ if (loaded.enabled && loaded.plugin.hooks?.onBeforeClose) {
269
+ try {
270
+ await loaded.plugin.hooks.onBeforeClose({
271
+ ...pluginCtx,
272
+ config: loaded.config
273
+ });
274
+ } catch (err) {
275
+ logger.warn(
276
+ `Plugin ${loaded.plugin.name} onBeforeClose failed: ${err}`
277
+ );
278
+ }
279
+ }
280
+ }
238
281
  }
239
282
  }
240
- }
283
+ };
241
284
  let result = await driver.runner.execute(session, ctx);
242
285
  for (const loaded of pluginManager2.getPlugins()) {
243
286
  if (loaded.enabled && loaded.plugin.hooks?.onRunEnd) {
@@ -254,6 +297,109 @@ var DriverManager = class {
254
297
  }
255
298
  return result;
256
299
  }
300
+ /**
301
+ * Execute multiple sessions with a shared browser (scan-level orchestration).
302
+ *
303
+ * This is the preferred entry point for running a full scan. It:
304
+ * 1. Launches ONE browser for the entire scan
305
+ * 2. Passes the browser to each session's runner via options.browser
306
+ * 3. Each session creates its own context (lightweight, isolated cookies)
307
+ * 4. Aggregates results across all sessions
308
+ * 5. Closes the browser once at the end
309
+ *
310
+ * This is 5-10x faster than calling execute() per session because
311
+ * launching a browser takes 2-3 seconds.
312
+ */
313
+ async executeScan(sessions, pluginManager2, options = {}) {
314
+ if (sessions.length === 0) {
315
+ const empty = {
316
+ findings: [],
317
+ stepsExecuted: 0,
318
+ payloadsTested: 0,
319
+ duration: 0,
320
+ errors: ["No sessions to execute"]
321
+ };
322
+ return { results: [], aggregate: empty };
323
+ }
324
+ const startTime = Date.now();
325
+ const results = [];
326
+ const allFindings = [];
327
+ let totalSteps = 0;
328
+ let totalPayloads = 0;
329
+ const allErrors = [];
330
+ const firstDriver = this.getForSession(sessions[0]);
331
+ let sharedBrowser = null;
332
+ if (firstDriver.name === "browser") {
333
+ try {
334
+ const driverPkg = "@vulcn/driver-browser";
335
+ const { launchBrowser } = await import(
336
+ /* @vite-ignore */
337
+ driverPkg
338
+ );
339
+ const browserType = sessions[0].driverConfig.browser ?? "chromium";
340
+ const headless = options.headless ?? true;
341
+ const result = await launchBrowser({
342
+ browser: browserType,
343
+ headless
344
+ });
345
+ sharedBrowser = result.browser;
346
+ } catch {
347
+ }
348
+ }
349
+ try {
350
+ await pluginManager2.callHook("onScanStart", async (hook, ctx) => {
351
+ const scanCtx = {
352
+ ...ctx,
353
+ sessions,
354
+ headless: options.headless ?? true,
355
+ sessionCount: sessions.length
356
+ };
357
+ await hook(scanCtx);
358
+ });
359
+ for (const session of sessions) {
360
+ const sessionOptions = {
361
+ ...options,
362
+ ...sharedBrowser ? { browser: sharedBrowser } : {}
363
+ };
364
+ const result = await this.execute(
365
+ session,
366
+ pluginManager2,
367
+ sessionOptions
368
+ );
369
+ results.push(result);
370
+ allFindings.push(...result.findings);
371
+ totalSteps += result.stepsExecuted;
372
+ totalPayloads += result.payloadsTested;
373
+ allErrors.push(...result.errors);
374
+ }
375
+ } finally {
376
+ if (sharedBrowser && typeof sharedBrowser.close === "function") {
377
+ await sharedBrowser.close();
378
+ }
379
+ }
380
+ const aggregate = {
381
+ findings: allFindings,
382
+ stepsExecuted: totalSteps,
383
+ payloadsTested: totalPayloads,
384
+ duration: Date.now() - startTime,
385
+ errors: allErrors
386
+ };
387
+ let finalAggregate = aggregate;
388
+ finalAggregate = await pluginManager2.callHookPipe(
389
+ "onScanEnd",
390
+ finalAggregate,
391
+ async (hook, value, ctx) => {
392
+ const scanCtx = {
393
+ ...ctx,
394
+ sessions,
395
+ headless: options.headless ?? true,
396
+ sessionCount: sessions.length
397
+ };
398
+ return await hook(value, scanCtx);
399
+ }
400
+ );
401
+ return { results, aggregate: finalAggregate };
402
+ }
257
403
  /**
258
404
  * Validate driver structure
259
405
  */
@@ -528,6 +674,9 @@ var PluginManager = class {
528
674
  engine: engineInfo,
529
675
  payloads: this.sharedPayloads,
530
676
  findings: this.sharedFindings,
677
+ addFinding: (finding) => {
678
+ this.sharedFindings.push(finding);
679
+ },
531
680
  logger: this.createLogger("plugin"),
532
681
  fetch: globalThis.fetch
533
682
  };
@@ -624,13 +773,265 @@ var PluginManager = class {
624
773
  }
625
774
  };
626
775
  var pluginManager = new PluginManager();
776
+
777
+ // src/auth.ts
778
+ var import_node_crypto = require("crypto");
779
+ var ALGORITHM = "aes-256-gcm";
780
+ var KEY_LENGTH = 32;
781
+ var IV_LENGTH = 16;
782
+ var SALT_LENGTH = 32;
783
+ var PBKDF2_ITERATIONS = 1e5;
784
+ var PBKDF2_DIGEST = "sha512";
785
+ function deriveKey(passphrase, salt) {
786
+ return (0, import_node_crypto.pbkdf2Sync)(
787
+ passphrase,
788
+ salt,
789
+ PBKDF2_ITERATIONS,
790
+ KEY_LENGTH,
791
+ PBKDF2_DIGEST
792
+ );
793
+ }
794
+ function encrypt(data, passphrase) {
795
+ const salt = (0, import_node_crypto.randomBytes)(SALT_LENGTH);
796
+ const iv = (0, import_node_crypto.randomBytes)(IV_LENGTH);
797
+ const key = deriveKey(passphrase, salt);
798
+ const cipher = (0, import_node_crypto.createCipheriv)(ALGORITHM, key, iv);
799
+ let encrypted = cipher.update(data, "utf8", "hex");
800
+ encrypted += cipher.final("hex");
801
+ const tag = cipher.getAuthTag();
802
+ const payload = {
803
+ version: 1,
804
+ salt: salt.toString("hex"),
805
+ iv: iv.toString("hex"),
806
+ tag: tag.toString("hex"),
807
+ data: encrypted,
808
+ iterations: PBKDF2_ITERATIONS
809
+ };
810
+ return JSON.stringify(payload);
811
+ }
812
+ function decrypt(encrypted, passphrase) {
813
+ const payload = JSON.parse(encrypted);
814
+ if (payload.version !== 1) {
815
+ throw new Error(`Unsupported encryption version: ${payload.version}`);
816
+ }
817
+ const salt = Buffer.from(payload.salt, "hex");
818
+ const iv = Buffer.from(payload.iv, "hex");
819
+ const tag = Buffer.from(payload.tag, "hex");
820
+ const key = deriveKey(passphrase, salt);
821
+ const decipher = (0, import_node_crypto.createDecipheriv)(ALGORITHM, key, iv);
822
+ decipher.setAuthTag(tag);
823
+ let decrypted = decipher.update(payload.data, "hex", "utf8");
824
+ decrypted += decipher.final("utf8");
825
+ return decrypted;
826
+ }
827
+ function encryptCredentials(credentials, passphrase) {
828
+ return encrypt(JSON.stringify(credentials), passphrase);
829
+ }
830
+ function decryptCredentials(encrypted, passphrase) {
831
+ const json = decrypt(encrypted, passphrase);
832
+ return JSON.parse(json);
833
+ }
834
+ function encryptStorageState(storageState, passphrase) {
835
+ return encrypt(storageState, passphrase);
836
+ }
837
+ function decryptStorageState(encrypted, passphrase) {
838
+ return decrypt(encrypted, passphrase);
839
+ }
840
+ function getPassphrase(interactive) {
841
+ if (interactive) return interactive;
842
+ const envKey = process.env.VULCN_KEY;
843
+ if (envKey) return envKey;
844
+ throw new Error(
845
+ "No passphrase provided. Set VULCN_KEY environment variable or pass --passphrase."
846
+ );
847
+ }
848
+
849
+ // src/session.ts
850
+ var import_promises2 = require("fs/promises");
851
+ var import_node_fs2 = require("fs");
852
+ var import_node_path3 = require("path");
853
+ var import_yaml3 = require("yaml");
854
+ async function loadSessionDir(dirPath) {
855
+ const manifestPath = (0, import_node_path3.join)(dirPath, "manifest.yml");
856
+ if (!(0, import_node_fs2.existsSync)(manifestPath)) {
857
+ throw new Error(
858
+ `No manifest.yml found in ${dirPath}. Is this a v2 session directory?`
859
+ );
860
+ }
861
+ const manifestYaml = await (0, import_promises2.readFile)(manifestPath, "utf-8");
862
+ const manifest = (0, import_yaml3.parse)(manifestYaml);
863
+ if (manifest.version !== "2") {
864
+ throw new Error(
865
+ `Unsupported session format version: ${manifest.version}. Expected "2".`
866
+ );
867
+ }
868
+ let authConfig;
869
+ if (manifest.auth?.configFile) {
870
+ const authPath = (0, import_node_path3.join)(dirPath, manifest.auth.configFile);
871
+ if ((0, import_node_fs2.existsSync)(authPath)) {
872
+ const authYaml = await (0, import_promises2.readFile)(authPath, "utf-8");
873
+ authConfig = (0, import_yaml3.parse)(authYaml);
874
+ }
875
+ }
876
+ const sessions = [];
877
+ for (const ref of manifest.sessions) {
878
+ if (ref.injectable === false) continue;
879
+ const sessionPath = (0, import_node_path3.join)(dirPath, ref.file);
880
+ if (!(0, import_node_fs2.existsSync)(sessionPath)) {
881
+ console.warn(`Session file not found: ${sessionPath}, skipping`);
882
+ continue;
883
+ }
884
+ const sessionYaml = await (0, import_promises2.readFile)(sessionPath, "utf-8");
885
+ const sessionData = (0, import_yaml3.parse)(sessionYaml);
886
+ const session = {
887
+ name: sessionData.name ?? (0, import_node_path3.basename)(ref.file, ".yml"),
888
+ driver: manifest.driver,
889
+ driverConfig: {
890
+ ...manifest.driverConfig,
891
+ startUrl: resolveUrl(
892
+ manifest.target,
893
+ sessionData.page
894
+ )
895
+ },
896
+ steps: sessionData.steps ?? [],
897
+ metadata: {
898
+ recordedAt: manifest.recordedAt,
899
+ version: "2",
900
+ manifestDir: dirPath
901
+ }
902
+ };
903
+ sessions.push(session);
904
+ }
905
+ return { manifest, sessions, authConfig };
906
+ }
907
+ function isSessionDir(path) {
908
+ return (0, import_node_fs2.existsSync)((0, import_node_path3.join)(path, "manifest.yml"));
909
+ }
910
+ function looksLikeSessionDir(path) {
911
+ return path.endsWith(".vulcn") || path.endsWith(".vulcn/");
912
+ }
913
+ async function saveSessionDir(dirPath, options) {
914
+ await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "sessions"), { recursive: true });
915
+ const sessionRefs = [];
916
+ for (const session of options.sessions) {
917
+ const safeName = slugify(session.name);
918
+ const fileName = `sessions/${safeName}.yml`;
919
+ const sessionPath = (0, import_node_path3.join)(dirPath, fileName);
920
+ const startUrl = session.driverConfig.startUrl;
921
+ const page = startUrl ? startUrl.replace(options.target, "").replace(/^\//, "/") : void 0;
922
+ const sessionData = {
923
+ name: session.name,
924
+ ...page ? { page } : {},
925
+ steps: session.steps
926
+ };
927
+ await (0, import_promises2.writeFile)(sessionPath, (0, import_yaml3.stringify)(sessionData), "utf-8");
928
+ const hasInjectable = session.steps.some(
929
+ (s) => s.type === "browser.input" && s.injectable !== false
930
+ );
931
+ sessionRefs.push({
932
+ file: fileName,
933
+ injectable: hasInjectable
934
+ });
935
+ }
936
+ if (options.authConfig) {
937
+ await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "auth"), { recursive: true });
938
+ await (0, import_promises2.writeFile)(
939
+ (0, import_node_path3.join)(dirPath, "auth", "config.yml"),
940
+ (0, import_yaml3.stringify)(options.authConfig),
941
+ "utf-8"
942
+ );
943
+ }
944
+ if (options.encryptedState) {
945
+ await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "auth"), { recursive: true });
946
+ await (0, import_promises2.writeFile)(
947
+ (0, import_node_path3.join)(dirPath, "auth", "state.enc"),
948
+ options.encryptedState,
949
+ "utf-8"
950
+ );
951
+ }
952
+ if (options.requests && options.requests.length > 0) {
953
+ await (0, import_promises2.mkdir)((0, import_node_path3.join)(dirPath, "requests"), { recursive: true });
954
+ for (const req of options.requests) {
955
+ const safeName = slugify(req.sessionName);
956
+ await (0, import_promises2.writeFile)(
957
+ (0, import_node_path3.join)(dirPath, "requests", `${safeName}.json`),
958
+ JSON.stringify(req, null, 2),
959
+ "utf-8"
960
+ );
961
+ }
962
+ }
963
+ const manifest = {
964
+ version: "2",
965
+ name: options.name,
966
+ target: options.target,
967
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
968
+ driver: options.driver,
969
+ driverConfig: options.driverConfig,
970
+ ...options.authConfig ? {
971
+ auth: {
972
+ strategy: options.authConfig.strategy,
973
+ configFile: "auth/config.yml",
974
+ stateFile: options.encryptedState ? "auth/state.enc" : void 0,
975
+ loggedInIndicator: options.authConfig.loggedInIndicator,
976
+ loggedOutIndicator: options.authConfig.loggedOutIndicator
977
+ }
978
+ } : {},
979
+ sessions: sessionRefs,
980
+ scan: {
981
+ tier: "auto",
982
+ parallel: 1,
983
+ timeout: 12e4
984
+ }
985
+ };
986
+ await (0, import_promises2.writeFile)((0, import_node_path3.join)(dirPath, "manifest.yml"), (0, import_yaml3.stringify)(manifest), "utf-8");
987
+ }
988
+ async function readAuthState(dirPath) {
989
+ const statePath = (0, import_node_path3.join)(dirPath, "auth", "state.enc");
990
+ if (!(0, import_node_fs2.existsSync)(statePath)) return null;
991
+ return (0, import_promises2.readFile)(statePath, "utf-8");
992
+ }
993
+ async function readCapturedRequests(dirPath) {
994
+ const requestsDir = (0, import_node_path3.join)(dirPath, "requests");
995
+ if (!(0, import_node_fs2.existsSync)(requestsDir)) return [];
996
+ const files = await (0, import_promises2.readdir)(requestsDir);
997
+ const requests = [];
998
+ for (const file of files) {
999
+ if (!file.endsWith(".json")) continue;
1000
+ const content = await (0, import_promises2.readFile)((0, import_node_path3.join)(requestsDir, file), "utf-8");
1001
+ requests.push(JSON.parse(content));
1002
+ }
1003
+ return requests;
1004
+ }
1005
+ function resolveUrl(target, page) {
1006
+ if (!page) return target;
1007
+ if (page.startsWith("http")) return page;
1008
+ const base = target.replace(/\/$/, "");
1009
+ const path = page.startsWith("/") ? page : `/${page}`;
1010
+ return `${base}${path}`;
1011
+ }
1012
+ function slugify(text) {
1013
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1014
+ }
627
1015
  // Annotate the CommonJS export names for ESM import in node:
628
1016
  0 && (module.exports = {
629
1017
  DRIVER_API_VERSION,
630
1018
  DriverManager,
631
1019
  PLUGIN_API_VERSION,
632
1020
  PluginManager,
1021
+ decrypt,
1022
+ decryptCredentials,
1023
+ decryptStorageState,
633
1024
  driverManager,
634
- pluginManager
1025
+ encrypt,
1026
+ encryptCredentials,
1027
+ encryptStorageState,
1028
+ getPassphrase,
1029
+ isSessionDir,
1030
+ loadSessionDir,
1031
+ looksLikeSessionDir,
1032
+ pluginManager,
1033
+ readAuthState,
1034
+ readCapturedRequests,
1035
+ saveSessionDir
635
1036
  });
636
1037
  //# sourceMappingURL=index.cjs.map