@vulcn/engine 0.7.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 +29 -0
- package/dist/index.cjs +370 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -1
- package/dist/index.d.ts +246 -1
- package/dist/index.js +361 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
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
|
+
|
|
3
32
|
## 0.7.0
|
|
4
33
|
|
|
5
34
|
### 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
|
-
|
|
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
|
|
|
@@ -284,6 +297,109 @@ var DriverManager = class {
|
|
|
284
297
|
}
|
|
285
298
|
return result;
|
|
286
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
|
+
}
|
|
287
403
|
/**
|
|
288
404
|
* Validate driver structure
|
|
289
405
|
*/
|
|
@@ -657,13 +773,265 @@ var PluginManager = class {
|
|
|
657
773
|
}
|
|
658
774
|
};
|
|
659
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
|
+
}
|
|
660
1015
|
// Annotate the CommonJS export names for ESM import in node:
|
|
661
1016
|
0 && (module.exports = {
|
|
662
1017
|
DRIVER_API_VERSION,
|
|
663
1018
|
DriverManager,
|
|
664
1019
|
PLUGIN_API_VERSION,
|
|
665
1020
|
PluginManager,
|
|
1021
|
+
decrypt,
|
|
1022
|
+
decryptCredentials,
|
|
1023
|
+
decryptStorageState,
|
|
666
1024
|
driverManager,
|
|
667
|
-
|
|
1025
|
+
encrypt,
|
|
1026
|
+
encryptCredentials,
|
|
1027
|
+
encryptStorageState,
|
|
1028
|
+
getPassphrase,
|
|
1029
|
+
isSessionDir,
|
|
1030
|
+
loadSessionDir,
|
|
1031
|
+
looksLikeSessionDir,
|
|
1032
|
+
pluginManager,
|
|
1033
|
+
readAuthState,
|
|
1034
|
+
readCapturedRequests,
|
|
1035
|
+
saveSessionDir
|
|
668
1036
|
});
|
|
669
1037
|
//# sourceMappingURL=index.cjs.map
|