sentinelayer-cli 0.6.2 → 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/README.md +996 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +64 -63
- package/src/agents/jules/config/definition.js +160 -160
- package/src/agents/jules/config/system-prompt.js +182 -182
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +17 -17
- package/src/agents/jules/loop.js +457 -450
- package/src/agents/jules/pulse.js +10 -10
- package/src/agents/jules/stream.js +187 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +311 -309
- package/src/agents/jules/tools/aidenid-email.js +189 -189
- package/src/agents/jules/tools/auth-audit.js +1699 -1691
- package/src/agents/jules/tools/dispatch.js +340 -335
- package/src/agents/jules/tools/file-edit.js +2 -2
- package/src/agents/jules/tools/file-read.js +2 -2
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +2 -2
- package/src/agents/jules/tools/grep.js +2 -2
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +2 -2
- package/src/agents/jules/tools/runtime-audit.js +507 -507
- package/src/agents/jules/tools/shell.js +2 -2
- package/src/agents/jules/tools/url-policy.js +100 -100
- package/src/agents/persona-visuals.js +64 -61
- package/src/agents/shared-tools/dispatch-core.js +320 -315
- package/src/agents/shared-tools/file-edit.js +180 -180
- package/src/agents/shared-tools/file-read.js +100 -100
- package/src/agents/shared-tools/glob.js +168 -168
- package/src/agents/shared-tools/grep.js +228 -228
- package/src/agents/shared-tools/index.js +46 -46
- package/src/agents/shared-tools/path-guards.js +161 -161
- package/src/agents/shared-tools/shell.js +383 -383
- package/src/ai/aidenid.js +1021 -1009
- package/src/ai/client.js +553 -553
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/proxy.js +137 -137
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +400 -371
- package/src/auth/http.js +681 -611
- package/src/auth/service.js +1106 -1106
- package/src/auth/session-store.js +813 -813
- package/src/cli.js +257 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1338
- package/src/commands/ai/provision-governance.js +1272 -1272
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1171 -1166
- package/src/commands/auth.js +419 -419
- package/src/commands/chat.js +191 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +10 -10
- package/src/commands/mcp.js +461 -461
- package/src/commands/omargate.js +29 -29
- package/src/commands/persona.js +20 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +872 -872
- package/src/commands/session.js +590 -0
- package/src/commands/spec.js +778 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +511 -511
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tracker.js +171 -171
- package/src/daemon/artifact-lineage.js +534 -534
- package/src/daemon/assignment-ledger.js +966 -770
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +1209 -626
- package/src/daemon/fix-cycle.js +384 -377
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/ingest-refresh.js +10 -9
- package/src/daemon/jira-lifecycle.js +767 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/pulse.js +327 -327
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/scope-engine.js +1068 -0
- package/src/daemon/watchdog.js +971 -971
- package/src/events/schema.js +190 -0
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +918 -918
- package/src/interactive/index.js +97 -97
- package/src/legacy-cli.js +3161 -2994
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +136 -118
- package/src/review/ai-review.js +679 -679
- package/src/review/local-review.js +1351 -1305
- package/src/review/omargate-interactive.js +68 -68
- package/src/review/omargate-orchestrator.js +404 -300
- package/src/review/persona-prompts.js +296 -296
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/scan-modes.js +48 -42
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -67
- package/src/scaffold/templates.js +150 -150
- package/src/scan/generator.js +418 -418
- package/src/scan/gh-secrets.js +107 -107
- package/src/session/agent-registry.js +352 -0
- package/src/session/daemon.js +801 -0
- package/src/session/paths.js +33 -0
- package/src/session/runtime-bridge.js +739 -0
- package/src/session/store.js +388 -0
- package/src/session/stream.js +325 -0
- package/src/spec/generator.js +619 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +576 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/session-tracker.js +234 -234
- package/src/telemetry/sync.js +203 -203
- package/src/ui/command-hints.js +13 -13
- package/src/ui/markdown.js +220 -220
|
@@ -1,161 +1,161 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const POSIX_BLOCKED_PREFIXES = ["/dev", "/proc", "/sys"];
|
|
5
|
-
const WINDOWS_DEVICE_SEGMENT = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
6
|
-
const WINDOWS_DEVICE_NAMESPACE_PATTERN = /^\\\\[?.]\\.+/;
|
|
7
|
-
const WINDOWS_UNC_PATTERN = /^\\\\(?![?.]\\)/;
|
|
8
|
-
const POSIX_UNC_PATTERN = /^\/\/[^/]/;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Resolve a user-provided file path and enforce sandbox-style guardrails.
|
|
12
|
-
* Returns the resolved path and realpath so callers can safely read/write.
|
|
13
|
-
*/
|
|
14
|
-
export function resolveGuardedPath({ filePath, allowedRoot }) {
|
|
15
|
-
const rawFilePath = normalizeInputPath(filePath);
|
|
16
|
-
assertPathNotNetwork(rawFilePath);
|
|
17
|
-
assertPathNotDeviceNamespace(rawFilePath);
|
|
18
|
-
|
|
19
|
-
const resolvedPath = path.resolve(rawFilePath);
|
|
20
|
-
const realPath = resolveRealPathOrFallback(resolvedPath);
|
|
21
|
-
|
|
22
|
-
assertPathNotNetwork(resolvedPath);
|
|
23
|
-
assertPathNotNetwork(realPath);
|
|
24
|
-
assertPathNotDeviceNamespace(resolvedPath);
|
|
25
|
-
assertPathNotDeviceNamespace(realPath);
|
|
26
|
-
assertPathNotBlockedPosixSystemPath(resolvedPath);
|
|
27
|
-
assertPathNotBlockedPosixSystemPath(realPath);
|
|
28
|
-
assertPathNotWindowsDeviceSegment(resolvedPath);
|
|
29
|
-
assertPathNotWindowsDeviceSegment(realPath);
|
|
30
|
-
|
|
31
|
-
if (allowedRoot !== undefined && allowedRoot !== null && String(allowedRoot).trim()) {
|
|
32
|
-
const resolvedAllowedRoot = path.resolve(String(allowedRoot));
|
|
33
|
-
const allowedRootRealPath = resolveRealPathOrFallback(resolvedAllowedRoot);
|
|
34
|
-
assertPathWithinAllowedRoot(resolvedPath, resolvedAllowedRoot);
|
|
35
|
-
assertPathWithinAllowedRoot(realPath, allowedRootRealPath);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
resolvedPath,
|
|
40
|
-
realPath,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function normalizeInputPath(filePath) {
|
|
45
|
-
if (!filePath || typeof filePath !== "string") {
|
|
46
|
-
throw new PathGuardError(
|
|
47
|
-
"PATH_INVALID",
|
|
48
|
-
"file_path is required and must be a non-empty string.",
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const trimmed = filePath.trim();
|
|
53
|
-
if (!trimmed) {
|
|
54
|
-
throw new PathGuardError(
|
|
55
|
-
"PATH_INVALID",
|
|
56
|
-
"file_path is required and must be a non-empty string.",
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
return trimmed;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function resolveRealPathOrFallback(candidatePath) {
|
|
63
|
-
try {
|
|
64
|
-
if (typeof fs.realpathSync.native === "function") {
|
|
65
|
-
return fs.realpathSync.native(candidatePath);
|
|
66
|
-
}
|
|
67
|
-
return fs.realpathSync(candidatePath);
|
|
68
|
-
} catch {
|
|
69
|
-
return candidatePath;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function assertPathNotNetwork(candidatePath) {
|
|
74
|
-
const normalized = String(candidatePath || "");
|
|
75
|
-
if (WINDOWS_UNC_PATTERN.test(normalized) || POSIX_UNC_PATTERN.test(normalized)) {
|
|
76
|
-
throw new PathGuardError(
|
|
77
|
-
"PATH_UNC_BLOCKED",
|
|
78
|
-
`Network paths are not allowed: ${candidatePath}`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function assertPathNotDeviceNamespace(candidatePath) {
|
|
84
|
-
const normalized = String(candidatePath || "");
|
|
85
|
-
if (WINDOWS_DEVICE_NAMESPACE_PATTERN.test(normalized)) {
|
|
86
|
-
throw new PathGuardError(
|
|
87
|
-
"PATH_DEVICE_NAMESPACE_BLOCKED",
|
|
88
|
-
`Device namespace paths are not allowed: ${candidatePath}`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function assertPathNotBlockedPosixSystemPath(candidatePath) {
|
|
94
|
-
const normalized = String(candidatePath || "").replace(/\\/g, "/");
|
|
95
|
-
for (const prefix of POSIX_BLOCKED_PREFIXES) {
|
|
96
|
-
if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
|
|
97
|
-
throw new PathGuardError(
|
|
98
|
-
"PATH_SYSTEM_BLOCKED",
|
|
99
|
-
`Blocked system path: ${candidatePath}`,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function assertPathNotWindowsDeviceSegment(candidatePath) {
|
|
106
|
-
if (process.platform !== "win32") {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const normalized = String(candidatePath || "").replace(/\//g, "\\");
|
|
111
|
-
const segments = normalized.split("\\").filter(Boolean);
|
|
112
|
-
for (const segment of segments) {
|
|
113
|
-
if (/^[a-z]:$/i.test(segment)) {
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (WINDOWS_DEVICE_SEGMENT.test(segment)) {
|
|
117
|
-
throw new PathGuardError(
|
|
118
|
-
"PATH_WINDOWS_DEVICE_BLOCKED",
|
|
119
|
-
`Blocked device path segment: ${candidatePath}`,
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function assertPathWithinAllowedRoot(candidatePath, allowedRoot) {
|
|
126
|
-
if (isPathInsideRoot(candidatePath, allowedRoot)) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
throw new PathGuardError(
|
|
130
|
-
"PATH_OUTSIDE_ALLOWED_ROOT",
|
|
131
|
-
`Path escapes allowed root: ${candidatePath} (root: ${allowedRoot})`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function isPathInsideRoot(candidatePath, rootPath) {
|
|
136
|
-
const normalizedCandidate = normalizeForComparison(candidatePath);
|
|
137
|
-
const normalizedRoot = normalizeForComparison(rootPath);
|
|
138
|
-
const relative = path.relative(normalizedRoot, normalizedCandidate);
|
|
139
|
-
|
|
140
|
-
if (!relative) {
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function normalizeForComparison(candidatePath) {
|
|
148
|
-
const resolved = path.resolve(candidatePath);
|
|
149
|
-
if (process.platform === "win32") {
|
|
150
|
-
return resolved.toLowerCase();
|
|
151
|
-
}
|
|
152
|
-
return resolved;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export class PathGuardError extends Error {
|
|
156
|
-
constructor(code, message) {
|
|
157
|
-
super(`[${code}] ${message}`);
|
|
158
|
-
this.name = "PathGuardError";
|
|
159
|
-
this.code = code;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const POSIX_BLOCKED_PREFIXES = ["/dev", "/proc", "/sys"];
|
|
5
|
+
const WINDOWS_DEVICE_SEGMENT = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
6
|
+
const WINDOWS_DEVICE_NAMESPACE_PATTERN = /^\\\\[?.]\\.+/;
|
|
7
|
+
const WINDOWS_UNC_PATTERN = /^\\\\(?![?.]\\)/;
|
|
8
|
+
const POSIX_UNC_PATTERN = /^\/\/[^/]/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a user-provided file path and enforce sandbox-style guardrails.
|
|
12
|
+
* Returns the resolved path and realpath so callers can safely read/write.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveGuardedPath({ filePath, allowedRoot }) {
|
|
15
|
+
const rawFilePath = normalizeInputPath(filePath);
|
|
16
|
+
assertPathNotNetwork(rawFilePath);
|
|
17
|
+
assertPathNotDeviceNamespace(rawFilePath);
|
|
18
|
+
|
|
19
|
+
const resolvedPath = path.resolve(rawFilePath);
|
|
20
|
+
const realPath = resolveRealPathOrFallback(resolvedPath);
|
|
21
|
+
|
|
22
|
+
assertPathNotNetwork(resolvedPath);
|
|
23
|
+
assertPathNotNetwork(realPath);
|
|
24
|
+
assertPathNotDeviceNamespace(resolvedPath);
|
|
25
|
+
assertPathNotDeviceNamespace(realPath);
|
|
26
|
+
assertPathNotBlockedPosixSystemPath(resolvedPath);
|
|
27
|
+
assertPathNotBlockedPosixSystemPath(realPath);
|
|
28
|
+
assertPathNotWindowsDeviceSegment(resolvedPath);
|
|
29
|
+
assertPathNotWindowsDeviceSegment(realPath);
|
|
30
|
+
|
|
31
|
+
if (allowedRoot !== undefined && allowedRoot !== null && String(allowedRoot).trim()) {
|
|
32
|
+
const resolvedAllowedRoot = path.resolve(String(allowedRoot));
|
|
33
|
+
const allowedRootRealPath = resolveRealPathOrFallback(resolvedAllowedRoot);
|
|
34
|
+
assertPathWithinAllowedRoot(resolvedPath, resolvedAllowedRoot);
|
|
35
|
+
assertPathWithinAllowedRoot(realPath, allowedRootRealPath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
resolvedPath,
|
|
40
|
+
realPath,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeInputPath(filePath) {
|
|
45
|
+
if (!filePath || typeof filePath !== "string") {
|
|
46
|
+
throw new PathGuardError(
|
|
47
|
+
"PATH_INVALID",
|
|
48
|
+
"file_path is required and must be a non-empty string.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const trimmed = filePath.trim();
|
|
53
|
+
if (!trimmed) {
|
|
54
|
+
throw new PathGuardError(
|
|
55
|
+
"PATH_INVALID",
|
|
56
|
+
"file_path is required and must be a non-empty string.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveRealPathOrFallback(candidatePath) {
|
|
63
|
+
try {
|
|
64
|
+
if (typeof fs.realpathSync.native === "function") {
|
|
65
|
+
return fs.realpathSync.native(candidatePath);
|
|
66
|
+
}
|
|
67
|
+
return fs.realpathSync(candidatePath);
|
|
68
|
+
} catch {
|
|
69
|
+
return candidatePath;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertPathNotNetwork(candidatePath) {
|
|
74
|
+
const normalized = String(candidatePath || "");
|
|
75
|
+
if (WINDOWS_UNC_PATTERN.test(normalized) || POSIX_UNC_PATTERN.test(normalized)) {
|
|
76
|
+
throw new PathGuardError(
|
|
77
|
+
"PATH_UNC_BLOCKED",
|
|
78
|
+
`Network paths are not allowed: ${candidatePath}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function assertPathNotDeviceNamespace(candidatePath) {
|
|
84
|
+
const normalized = String(candidatePath || "");
|
|
85
|
+
if (WINDOWS_DEVICE_NAMESPACE_PATTERN.test(normalized)) {
|
|
86
|
+
throw new PathGuardError(
|
|
87
|
+
"PATH_DEVICE_NAMESPACE_BLOCKED",
|
|
88
|
+
`Device namespace paths are not allowed: ${candidatePath}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertPathNotBlockedPosixSystemPath(candidatePath) {
|
|
94
|
+
const normalized = String(candidatePath || "").replace(/\\/g, "/");
|
|
95
|
+
for (const prefix of POSIX_BLOCKED_PREFIXES) {
|
|
96
|
+
if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
|
|
97
|
+
throw new PathGuardError(
|
|
98
|
+
"PATH_SYSTEM_BLOCKED",
|
|
99
|
+
`Blocked system path: ${candidatePath}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assertPathNotWindowsDeviceSegment(candidatePath) {
|
|
106
|
+
if (process.platform !== "win32") {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const normalized = String(candidatePath || "").replace(/\//g, "\\");
|
|
111
|
+
const segments = normalized.split("\\").filter(Boolean);
|
|
112
|
+
for (const segment of segments) {
|
|
113
|
+
if (/^[a-z]:$/i.test(segment)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (WINDOWS_DEVICE_SEGMENT.test(segment)) {
|
|
117
|
+
throw new PathGuardError(
|
|
118
|
+
"PATH_WINDOWS_DEVICE_BLOCKED",
|
|
119
|
+
`Blocked device path segment: ${candidatePath}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assertPathWithinAllowedRoot(candidatePath, allowedRoot) {
|
|
126
|
+
if (isPathInsideRoot(candidatePath, allowedRoot)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
throw new PathGuardError(
|
|
130
|
+
"PATH_OUTSIDE_ALLOWED_ROOT",
|
|
131
|
+
`Path escapes allowed root: ${candidatePath} (root: ${allowedRoot})`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isPathInsideRoot(candidatePath, rootPath) {
|
|
136
|
+
const normalizedCandidate = normalizeForComparison(candidatePath);
|
|
137
|
+
const normalizedRoot = normalizeForComparison(rootPath);
|
|
138
|
+
const relative = path.relative(normalizedRoot, normalizedCandidate);
|
|
139
|
+
|
|
140
|
+
if (!relative) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeForComparison(candidatePath) {
|
|
148
|
+
const resolved = path.resolve(candidatePath);
|
|
149
|
+
if (process.platform === "win32") {
|
|
150
|
+
return resolved.toLowerCase();
|
|
151
|
+
}
|
|
152
|
+
return resolved;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export class PathGuardError extends Error {
|
|
156
|
+
constructor(code, message) {
|
|
157
|
+
super(`[${code}] ${message}`);
|
|
158
|
+
this.name = "PathGuardError";
|
|
159
|
+
this.code = code;
|
|
160
|
+
}
|
|
161
|
+
}
|