@odavl/guardian 2.0.0 → 2.0.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/CHANGELOG.md +210 -210
- package/LICENSE +21 -21
- package/README.md +297 -184
- package/bin/guardian.js +2242 -2221
- package/config/README.md +59 -59
- package/config/guardian.config.json +54 -54
- package/config/guardian.policy.json +12 -12
- package/config/profiles/docs.yaml +18 -18
- package/config/profiles/ecommerce.yaml +17 -17
- package/config/profiles/landing-demo.yaml +16 -16
- package/config/profiles/marketing.yaml +18 -18
- package/config/profiles/saas.yaml +21 -21
- package/flows/example-login-flow.json +36 -36
- package/flows/example-signup-flow.json +44 -44
- package/package.json +124 -116
- package/policies/enterprise.json +12 -12
- package/policies/landing-demo.json +22 -22
- package/policies/saas.json +12 -12
- package/policies/startup.json +12 -12
- package/src/enterprise/audit-logger.js +166 -166
- package/src/enterprise/pdf-exporter.js +267 -267
- package/src/enterprise/rbac-gate.js +142 -142
- package/src/enterprise/rbac.js +239 -239
- package/src/enterprise/site-manager.js +180 -180
- package/src/founder/feedback-system.js +156 -156
- package/src/founder/founder-tracker.js +213 -213
- package/src/founder/usage-signals.js +141 -141
- package/src/guardian/action-hints.js +439 -439
- package/src/guardian/alert-ledger.js +121 -121
- package/src/guardian/artifact-sanitizer.js +56 -56
- package/src/guardian/attempt-engine.js +1069 -1029
- package/src/guardian/attempt-registry.js +267 -267
- package/src/guardian/attempt-relevance.js +106 -106
- package/src/guardian/attempt-reporter.js +513 -507
- package/src/guardian/attempt.js +274 -273
- package/src/guardian/attempts-filter.js +63 -63
- package/src/guardian/auto-attempt-builder.js +283 -283
- package/src/guardian/baseline-registry.js +177 -177
- package/src/guardian/baseline-reporter.js +143 -143
- package/src/guardian/baseline-storage.js +285 -285
- package/src/guardian/baseline.js +535 -534
- package/src/guardian/behavioral-signals.js +261 -261
- package/src/guardian/breakage-intelligence.js +224 -224
- package/src/guardian/browser-pool.js +131 -131
- package/src/guardian/browser.js +119 -119
- package/src/guardian/canonical-truth.js +308 -308
- package/src/guardian/ci-cli.js +121 -121
- package/src/guardian/ci-gate.js +96 -96
- package/src/guardian/ci-mode.js +15 -15
- package/src/guardian/ci-output.js +55 -38
- package/src/guardian/cli-summary.js +102 -102
- package/src/guardian/confidence-signals.js +251 -251
- package/src/guardian/config-loader.js +161 -161
- package/src/guardian/config-validator.js +285 -283
- package/src/guardian/coverage-model.js +239 -239
- package/src/guardian/coverage-packs.js +58 -58
- package/src/guardian/crawler.js +142 -142
- package/src/guardian/data-guardian-detector.js +189 -189
- package/src/guardian/decision-authority.js +746 -725
- package/src/guardian/detection-layers.js +271 -271
- package/src/guardian/determinism.js +146 -146
- package/src/guardian/discovery-engine.js +661 -661
- package/src/guardian/drift-detector.js +100 -100
- package/src/guardian/enhanced-html-reporter.js +522 -522
- package/src/guardian/env-guard.js +128 -127
- package/src/guardian/error-clarity.js +399 -399
- package/src/guardian/export-contract.js +196 -196
- package/src/guardian/fail-safe.js +212 -212
- package/src/guardian/failure-intelligence.js +173 -173
- package/src/guardian/failure-taxonomy.js +169 -169
- package/src/guardian/final-outcome.js +206 -206
- package/src/guardian/first-run-profile.js +89 -89
- package/src/guardian/first-run.js +65 -67
- package/src/guardian/flag-validator.js +111 -111
- package/src/guardian/flow-executor.js +641 -639
- package/src/guardian/flow-registry.js +67 -67
- package/src/guardian/honesty.js +394 -394
- package/src/guardian/html-reporter.js +416 -416
- package/src/guardian/human-intent-resolver.js +296 -296
- package/src/guardian/human-interaction-model.js +351 -351
- package/src/guardian/human-journey-context.js +184 -184
- package/src/guardian/human-navigator.js +544 -544
- package/src/guardian/human-reporter.js +435 -431
- package/src/guardian/index.js +226 -221
- package/src/guardian/init-command.js +143 -143
- package/src/guardian/intent-detector.js +148 -146
- package/src/guardian/journey-definitions.js +132 -132
- package/src/guardian/journey-scan-cli.js +142 -145
- package/src/guardian/journey-scanner.js +583 -583
- package/src/guardian/junit-reporter.js +281 -281
- package/src/guardian/language-detection.js +99 -99
- package/src/guardian/live-alert.js +56 -56
- package/src/guardian/live-baseline-compare.js +146 -146
- package/src/guardian/live-cli.js +95 -95
- package/src/guardian/live-guardian.js +210 -210
- package/src/guardian/live-scheduler-runner.js +137 -137
- package/src/guardian/live-scheduler-state.js +167 -168
- package/src/guardian/live-scheduler.js +146 -146
- package/src/guardian/live-state.js +110 -110
- package/src/guardian/market-criticality.js +335 -335
- package/src/guardian/market-reporter.js +577 -577
- package/src/guardian/network-trace.js +178 -178
- package/src/guardian/obs-logger.js +110 -110
- package/src/guardian/observed-capabilities.js +427 -427
- package/src/guardian/output-contract.js +154 -0
- package/src/guardian/output-readability.js +264 -264
- package/src/guardian/parallel-executor.js +116 -116
- package/src/guardian/path-safety.js +56 -56
- package/src/guardian/pattern-analyzer.js +348 -348
- package/src/guardian/policy.js +432 -434
- package/src/guardian/prelaunch-gate.js +193 -193
- package/src/guardian/prerequisite-checker.js +101 -101
- package/src/guardian/preset-loader.js +152 -157
- package/src/guardian/profile-loader.js +96 -96
- package/src/guardian/reality.js +3025 -2826
- package/src/guardian/realworld-scenarios.js +94 -94
- package/src/guardian/reporter.js +167 -167
- package/src/guardian/retry-policy.js +123 -123
- package/src/guardian/root-cause-analysis.js +171 -171
- package/src/guardian/rules-engine.js +558 -558
- package/src/guardian/run-artifacts.js +212 -212
- package/src/guardian/run-cleanup.js +207 -207
- package/src/guardian/run-export.js +522 -522
- package/src/guardian/run-latest.js +90 -90
- package/src/guardian/run-list.js +211 -211
- package/src/guardian/run-summary.js +20 -20
- package/src/guardian/runtime-root.js +246 -246
- package/src/guardian/safety.js +248 -248
- package/src/guardian/scan-presets.js +133 -149
- package/src/guardian/screenshot.js +152 -152
- package/src/guardian/secret-hygiene.js +44 -44
- package/src/guardian/selector-fallbacks.js +394 -394
- package/src/guardian/semantic-contact-detection.js +255 -255
- package/src/guardian/semantic-contact-finder.js +201 -201
- package/src/guardian/semantic-targets.js +234 -234
- package/src/guardian/site-intelligence.js +588 -588
- package/src/guardian/site-introspection.js +257 -257
- package/src/guardian/sitemap.js +225 -225
- package/src/guardian/smoke.js +283 -258
- package/src/guardian/snapshot-schema.js +177 -290
- package/src/guardian/snapshot.js +430 -397
- package/src/guardian/stability-scorer.js +169 -169
- package/src/guardian/success-evaluator.js +214 -214
- package/src/guardian/template-command.js +184 -184
- package/src/guardian/text-formatters.js +426 -426
- package/src/guardian/timeout-profiles.js +57 -57
- package/src/guardian/truth/attempt.contract.js +158 -0
- package/src/guardian/truth/decision.contract.js +275 -0
- package/src/guardian/truth/snapshot.contract.js +363 -0
- package/src/guardian/validators.js +323 -323
- package/src/guardian/verdict-card.js +474 -474
- package/src/guardian/verdict-clarity.js +298 -298
- package/src/guardian/verdict-policy.js +363 -363
- package/src/guardian/verdict.js +333 -333
- package/src/guardian/verdicts.js +79 -74
- package/src/guardian/visual-diff.js +247 -247
- package/src/guardian/wait-for-outcome.js +119 -119
- package/src/guardian/watch-runner.js +181 -181
- package/src/guardian/watchdog-diff.js +167 -167
- package/src/guardian/webhook.js +206 -206
- package/src/payments/stripe-checkout.js +169 -169
- package/src/plans/plan-definitions.js +148 -148
- package/src/plans/plan-manager.js +211 -211
- package/src/plans/usage-tracker.js +210 -210
- package/src/recipes/recipe-engine.js +188 -188
- package/src/recipes/recipe-failure-analysis.js +159 -159
- package/src/recipes/recipe-registry.js +134 -134
- package/src/recipes/recipe-runtime.js +507 -507
- package/src/recipes/recipe-store.js +410 -410
- package/SECURITY.md +0 -77
- package/VERSIONING.md +0 -100
- package/guardian-contract-v1.md +0 -502
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 7.2 - Parallel Executor
|
|
3
|
-
* Controlled parallel execution of attempts with bounded concurrency
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Execute attempts with bounded parallelism
|
|
8
|
-
* @param {Array} attempts - Array of attempts to execute
|
|
9
|
-
* @param {Function} executeAttemptFn - Async function(attempt) => result
|
|
10
|
-
* @param {number} maxConcurrency - Max concurrent attempts (must be >= 1)
|
|
11
|
-
* @param {Object} options - { shouldStop?: Function }
|
|
12
|
-
* @returns {Promise<Array>} Results in original input order
|
|
13
|
-
*/
|
|
14
|
-
async function executeParallel(attempts, executeAttemptFn, maxConcurrency = 1, options = {}) {
|
|
15
|
-
if (maxConcurrency < 1) {
|
|
16
|
-
throw new Error('maxConcurrency must be >= 1');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Handle empty queue immediately
|
|
20
|
-
if (!attempts || attempts.length === 0) {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const results = new Array(attempts.length);
|
|
25
|
-
const queue = attempts.map((attempt, index) => ({ attempt, index }));
|
|
26
|
-
let executing = 0;
|
|
27
|
-
let queueIndex = 0;
|
|
28
|
-
let shouldStop = false;
|
|
29
|
-
|
|
30
|
-
return new Promise((resolve, reject) => {
|
|
31
|
-
const processNext = async () => {
|
|
32
|
-
// Check if we should stop (e.g., fail-fast triggered)
|
|
33
|
-
if (options.shouldStop && options.shouldStop()) {
|
|
34
|
-
shouldStop = true;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// If no more work and nothing executing, we're done
|
|
38
|
-
if (queueIndex >= queue.length && executing === 0) {
|
|
39
|
-
resolve(results);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// If we've hit the concurrency limit or have no more items, wait
|
|
44
|
-
if (executing >= maxConcurrency || queueIndex >= queue.length) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Get next item from queue
|
|
49
|
-
const { attempt, index } = queue[queueIndex];
|
|
50
|
-
queueIndex++;
|
|
51
|
-
executing++;
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
// Only execute if we haven't stopped
|
|
55
|
-
if (!shouldStop) {
|
|
56
|
-
results[index] = await executeAttemptFn(attempt);
|
|
57
|
-
} else {
|
|
58
|
-
// Mark as skipped if we stopped
|
|
59
|
-
results[index] = { skipped: true, attemptId: attempt };
|
|
60
|
-
}
|
|
61
|
-
} catch (err) {
|
|
62
|
-
results[index] = { error: err, skipped: false, attemptId: attempt };
|
|
63
|
-
} finally {
|
|
64
|
-
executing--;
|
|
65
|
-
// Process next item(s)
|
|
66
|
-
processNext();
|
|
67
|
-
if (executing < maxConcurrency && queueIndex < queue.length && !shouldStop) {
|
|
68
|
-
processNext();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Start workers
|
|
74
|
-
for (let i = 0; i < Math.min(maxConcurrency, queue.length); i++) {
|
|
75
|
-
processNext();
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Validate parallel concurrency value
|
|
82
|
-
* @param {number|string} value - Value to validate
|
|
83
|
-
* @returns {{ valid: boolean, parallel?: number, error?: string, hint?: string }}
|
|
84
|
-
*/
|
|
85
|
-
function validateParallel(value) {
|
|
86
|
-
if (value === undefined || value === null) {
|
|
87
|
-
return { valid: true, parallel: 1 };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const num = parseInt(value, 10);
|
|
91
|
-
|
|
92
|
-
// Check for NaN
|
|
93
|
-
if (isNaN(num)) {
|
|
94
|
-
return {
|
|
95
|
-
valid: false,
|
|
96
|
-
error: `Invalid --parallel value: '${value}' (expected integer >= 1)`,
|
|
97
|
-
hint: 'Example: --parallel 2'
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Check if less than 1
|
|
102
|
-
if (num < 1) {
|
|
103
|
-
return {
|
|
104
|
-
valid: false,
|
|
105
|
-
error: `Invalid --parallel value: ${num} (must be >= 1)`,
|
|
106
|
-
hint: 'Example: --parallel 2'
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { valid: true, parallel: num };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
module.exports = {
|
|
114
|
-
executeParallel,
|
|
115
|
-
validateParallel
|
|
116
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Phase 7.2 - Parallel Executor
|
|
3
|
+
* Controlled parallel execution of attempts with bounded concurrency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Execute attempts with bounded parallelism
|
|
8
|
+
* @param {Array} attempts - Array of attempts to execute
|
|
9
|
+
* @param {Function} executeAttemptFn - Async function(attempt) => result
|
|
10
|
+
* @param {number} maxConcurrency - Max concurrent attempts (must be >= 1)
|
|
11
|
+
* @param {Object} options - { shouldStop?: Function }
|
|
12
|
+
* @returns {Promise<Array>} Results in original input order
|
|
13
|
+
*/
|
|
14
|
+
async function executeParallel(attempts, executeAttemptFn, maxConcurrency = 1, options = {}) {
|
|
15
|
+
if (maxConcurrency < 1) {
|
|
16
|
+
throw new Error('maxConcurrency must be >= 1');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Handle empty queue immediately
|
|
20
|
+
if (!attempts || attempts.length === 0) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const results = new Array(attempts.length);
|
|
25
|
+
const queue = attempts.map((attempt, index) => ({ attempt, index }));
|
|
26
|
+
let executing = 0;
|
|
27
|
+
let queueIndex = 0;
|
|
28
|
+
let shouldStop = false;
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const processNext = async () => {
|
|
32
|
+
// Check if we should stop (e.g., fail-fast triggered)
|
|
33
|
+
if (options.shouldStop && options.shouldStop()) {
|
|
34
|
+
shouldStop = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If no more work and nothing executing, we're done
|
|
38
|
+
if (queueIndex >= queue.length && executing === 0) {
|
|
39
|
+
resolve(results);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If we've hit the concurrency limit or have no more items, wait
|
|
44
|
+
if (executing >= maxConcurrency || queueIndex >= queue.length) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get next item from queue
|
|
49
|
+
const { attempt, index } = queue[queueIndex];
|
|
50
|
+
queueIndex++;
|
|
51
|
+
executing++;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Only execute if we haven't stopped
|
|
55
|
+
if (!shouldStop) {
|
|
56
|
+
results[index] = await executeAttemptFn(attempt);
|
|
57
|
+
} else {
|
|
58
|
+
// Mark as skipped if we stopped
|
|
59
|
+
results[index] = { skipped: true, attemptId: attempt };
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
results[index] = { error: err, skipped: false, attemptId: attempt };
|
|
63
|
+
} finally {
|
|
64
|
+
executing--;
|
|
65
|
+
// Process next item(s)
|
|
66
|
+
processNext();
|
|
67
|
+
if (executing < maxConcurrency && queueIndex < queue.length && !shouldStop) {
|
|
68
|
+
processNext();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Start workers
|
|
74
|
+
for (let i = 0; i < Math.min(maxConcurrency, queue.length); i++) {
|
|
75
|
+
processNext();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate parallel concurrency value
|
|
82
|
+
* @param {number|string} value - Value to validate
|
|
83
|
+
* @returns {{ valid: boolean, parallel?: number, error?: string, hint?: string }}
|
|
84
|
+
*/
|
|
85
|
+
function validateParallel(value) {
|
|
86
|
+
if (value === undefined || value === null) {
|
|
87
|
+
return { valid: true, parallel: 1 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const num = parseInt(value, 10);
|
|
91
|
+
|
|
92
|
+
// Check for NaN
|
|
93
|
+
if (isNaN(num)) {
|
|
94
|
+
return {
|
|
95
|
+
valid: false,
|
|
96
|
+
error: `Invalid --parallel value: '${value}' (expected integer >= 1)`,
|
|
97
|
+
hint: 'Example: --parallel 2'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if less than 1
|
|
102
|
+
if (num < 1) {
|
|
103
|
+
return {
|
|
104
|
+
valid: false,
|
|
105
|
+
error: `Invalid --parallel value: ${num} (must be >= 1)`,
|
|
106
|
+
hint: 'Example: --parallel 2'
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { valid: true, parallel: num };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
executeParallel,
|
|
115
|
+
validateParallel
|
|
116
|
+
};
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
const { resolveArtifactsDir, getRuntimeRoot } = require('./runtime-root');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolve the base artifacts directory to an absolute, normalized path.
|
|
6
|
-
* Now defaults to ~/.odavlguardian/artifacts instead of project-local ./.odavlguardian
|
|
7
|
-
*
|
|
8
|
-
* This function maintains backward compatibility:
|
|
9
|
-
* - Legacy project paths (./.odavlguardian, .guardian) are migrated to runtime root
|
|
10
|
-
* - Custom absolute paths are respected (for enterprise/test overrides)
|
|
11
|
-
* - Runtime root paths are preserved
|
|
12
|
-
*/
|
|
13
|
-
function resolveBaseDir(rawBaseDir) {
|
|
14
|
-
return resolveArtifactsDir(rawBaseDir);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Ensure a candidate path remains strictly inside the base directory.
|
|
19
|
-
* Throws if the resolved path escapes the base (fail-closed).
|
|
20
|
-
*
|
|
21
|
-
* Special allowances for backward compatibility and testing:
|
|
22
|
-
* - Paths under runtime root (~/.odavlguardian) are always allowed
|
|
23
|
-
*/
|
|
24
|
-
function ensurePathWithinBase(baseDir, candidatePath, label = 'path') {
|
|
25
|
-
const normalizedBase = path.resolve(baseDir);
|
|
26
|
-
const resolved = path.resolve(candidatePath);
|
|
27
|
-
const runtimeRoot = getRuntimeRoot();
|
|
28
|
-
|
|
29
|
-
// Check if path is within the specified base
|
|
30
|
-
const isSame = resolved === normalizedBase;
|
|
31
|
-
const isInside = resolved.startsWith(normalizedBase + path.sep);
|
|
32
|
-
|
|
33
|
-
// Also allow paths under runtime root (for migration)
|
|
34
|
-
const isUnderRuntimeRoot = resolved === runtimeRoot || resolved.startsWith(runtimeRoot + path.sep);
|
|
35
|
-
|
|
36
|
-
if (!isSame && !isInside && !isUnderRuntimeRoot) {
|
|
37
|
-
const err = new Error(`${label} must stay within artifacts base directory: ${normalizedBase}`);
|
|
38
|
-
err.code = 'EOUTOFBASE';
|
|
39
|
-
throw err;
|
|
40
|
-
}
|
|
41
|
-
return resolved;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Resolve a path by joining segments and enforcing containment in base.
|
|
46
|
-
*/
|
|
47
|
-
function resolveWithinBase(baseDir, label, ...segments) {
|
|
48
|
-
const candidate = path.join(...segments);
|
|
49
|
-
return ensurePathWithinBase(baseDir, candidate, label);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
module.exports = {
|
|
53
|
-
resolveBaseDir,
|
|
54
|
-
ensurePathWithinBase,
|
|
55
|
-
resolveWithinBase
|
|
56
|
-
};
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { resolveArtifactsDir, getRuntimeRoot } = require('./runtime-root');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the base artifacts directory to an absolute, normalized path.
|
|
6
|
+
* Now defaults to ~/.odavlguardian/artifacts instead of project-local ./.odavlguardian
|
|
7
|
+
*
|
|
8
|
+
* This function maintains backward compatibility:
|
|
9
|
+
* - Legacy project paths (./.odavlguardian, .guardian) are migrated to runtime root
|
|
10
|
+
* - Custom absolute paths are respected (for enterprise/test overrides)
|
|
11
|
+
* - Runtime root paths are preserved
|
|
12
|
+
*/
|
|
13
|
+
function resolveBaseDir(rawBaseDir) {
|
|
14
|
+
return resolveArtifactsDir(rawBaseDir);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensure a candidate path remains strictly inside the base directory.
|
|
19
|
+
* Throws if the resolved path escapes the base (fail-closed).
|
|
20
|
+
*
|
|
21
|
+
* Special allowances for backward compatibility and testing:
|
|
22
|
+
* - Paths under runtime root (~/.odavlguardian) are always allowed
|
|
23
|
+
*/
|
|
24
|
+
function ensurePathWithinBase(baseDir, candidatePath, label = 'path') {
|
|
25
|
+
const normalizedBase = path.resolve(baseDir);
|
|
26
|
+
const resolved = path.resolve(candidatePath);
|
|
27
|
+
const runtimeRoot = getRuntimeRoot();
|
|
28
|
+
|
|
29
|
+
// Check if path is within the specified base
|
|
30
|
+
const isSame = resolved === normalizedBase;
|
|
31
|
+
const isInside = resolved.startsWith(normalizedBase + path.sep);
|
|
32
|
+
|
|
33
|
+
// Also allow paths under runtime root (for migration)
|
|
34
|
+
const isUnderRuntimeRoot = resolved === runtimeRoot || resolved.startsWith(runtimeRoot + path.sep);
|
|
35
|
+
|
|
36
|
+
if (!isSame && !isInside && !isUnderRuntimeRoot) {
|
|
37
|
+
const err = new Error(`${label} must stay within artifacts base directory: ${normalizedBase}`);
|
|
38
|
+
err.code = 'EOUTOFBASE';
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a path by joining segments and enforcing containment in base.
|
|
46
|
+
*/
|
|
47
|
+
function resolveWithinBase(baseDir, label, ...segments) {
|
|
48
|
+
const candidate = path.join(...segments);
|
|
49
|
+
return ensurePathWithinBase(baseDir, candidate, label);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
resolveBaseDir,
|
|
54
|
+
ensurePathWithinBase,
|
|
55
|
+
resolveWithinBase
|
|
56
|
+
};
|