deepadb 1.1.1 → 1.1.3
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/LICENSE +202 -21
- package/NOTICE +16 -0
- package/README.md +55 -29
- package/build/bridge/adb-bridge.d.ts +14 -0
- package/build/bridge/adb-bridge.d.ts.map +1 -1
- package/build/bridge/adb-bridge.js +34 -5
- package/build/bridge/adb-bridge.js.map +1 -1
- package/build/bridge/device-manager.d.ts.map +1 -1
- package/build/bridge/device-manager.js +18 -9
- package/build/bridge/device-manager.js.map +1 -1
- package/build/bridge/local-bridge.d.ts +10 -2
- package/build/bridge/local-bridge.d.ts.map +1 -1
- package/build/bridge/local-bridge.js +51 -16
- package/build/bridge/local-bridge.js.map +1 -1
- package/build/config/config.d.ts +7 -0
- package/build/config/config.d.ts.map +1 -1
- package/build/config/config.js +26 -10
- package/build/config/config.js.map +1 -1
- package/build/graphql-api.d.ts.map +1 -1
- package/build/graphql-api.js +73 -3
- package/build/graphql-api.js.map +1 -1
- package/build/http-transport.d.ts.map +1 -1
- package/build/http-transport.js +44 -7
- package/build/http-transport.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +41 -16
- package/build/index.js.map +1 -1
- package/build/middleware/auth.d.ts.map +1 -1
- package/build/middleware/auth.js +43 -6
- package/build/middleware/auth.js.map +1 -1
- package/build/middleware/chipset.d.ts.map +1 -1
- package/build/middleware/chipset.js +16 -1
- package/build/middleware/chipset.js.map +1 -1
- package/build/middleware/cleanup.d.ts +39 -7
- package/build/middleware/cleanup.d.ts.map +1 -1
- package/build/middleware/cleanup.js +148 -16
- package/build/middleware/cleanup.js.map +1 -1
- package/build/middleware/fetch-utils.d.ts +50 -7
- package/build/middleware/fetch-utils.d.ts.map +1 -1
- package/build/middleware/fetch-utils.js +127 -14
- package/build/middleware/fetch-utils.js.map +1 -1
- package/build/middleware/fs-utils.d.ts +126 -0
- package/build/middleware/fs-utils.d.ts.map +1 -0
- package/build/middleware/fs-utils.js +229 -0
- package/build/middleware/fs-utils.js.map +1 -0
- package/build/middleware/logger.d.ts +16 -0
- package/build/middleware/logger.d.ts.map +1 -1
- package/build/middleware/logger.js +18 -0
- package/build/middleware/logger.js.map +1 -1
- package/build/middleware/output-processor.d.ts.map +1 -1
- package/build/middleware/output-processor.js +43 -8
- package/build/middleware/output-processor.js.map +1 -1
- package/build/middleware/parse-utils.d.ts +19 -0
- package/build/middleware/parse-utils.d.ts.map +1 -0
- package/build/middleware/parse-utils.js +30 -0
- package/build/middleware/parse-utils.js.map +1 -0
- package/build/middleware/png-utils.d.ts.map +1 -1
- package/build/middleware/png-utils.js +33 -11
- package/build/middleware/png-utils.js.map +1 -1
- package/build/middleware/result-handle.d.ts +76 -0
- package/build/middleware/result-handle.d.ts.map +1 -0
- package/build/middleware/result-handle.js +378 -0
- package/build/middleware/result-handle.js.map +1 -0
- package/build/middleware/sanitize.d.ts +26 -0
- package/build/middleware/sanitize.d.ts.map +1 -1
- package/build/middleware/sanitize.js +43 -2
- package/build/middleware/sanitize.js.map +1 -1
- package/build/middleware/security.d.ts.map +1 -1
- package/build/middleware/security.js +30 -5
- package/build/middleware/security.js.map +1 -1
- package/build/middleware/ui-dump.d.ts.map +1 -1
- package/build/middleware/ui-dump.js +30 -10
- package/build/middleware/ui-dump.js.map +1 -1
- package/build/server.d.ts.map +1 -1
- package/build/server.js +34 -5
- package/build/server.js.map +1 -1
- package/build/tool-context.d.ts.map +1 -1
- package/build/tool-context.js +2 -0
- package/build/tool-context.js.map +1 -1
- package/build/tools/accessibility.d.ts.map +1 -1
- package/build/tools/accessibility.js +39 -40
- package/build/tools/accessibility.js.map +1 -1
- package/build/tools/at-commands.d.ts.map +1 -1
- package/build/tools/at-commands.js +31 -6
- package/build/tools/at-commands.js.map +1 -1
- package/build/tools/baseband.d.ts.map +1 -1
- package/build/tools/baseband.js +18 -7
- package/build/tools/baseband.js.map +1 -1
- package/build/tools/build.d.ts.map +1 -1
- package/build/tools/build.js +30 -8
- package/build/tools/build.js.map +1 -1
- package/build/tools/ci.d.ts.map +1 -1
- package/build/tools/ci.js +16 -5
- package/build/tools/ci.js.map +1 -1
- package/build/tools/control.d.ts.map +1 -1
- package/build/tools/control.js +19 -11
- package/build/tools/control.js.map +1 -1
- package/build/tools/device-farm.d.ts.map +1 -1
- package/build/tools/device-farm.js +27 -3
- package/build/tools/device-farm.js.map +1 -1
- package/build/tools/device-profiles.d.ts.map +1 -1
- package/build/tools/device-profiles.js +70 -8
- package/build/tools/device-profiles.js.map +1 -1
- package/build/tools/device.d.ts.map +1 -1
- package/build/tools/device.js +4 -2
- package/build/tools/device.js.map +1 -1
- package/build/tools/diagnostics.d.ts.map +1 -1
- package/build/tools/diagnostics.js +17 -9
- package/build/tools/diagnostics.js.map +1 -1
- package/build/tools/emulator.d.ts.map +1 -1
- package/build/tools/emulator.js +12 -13
- package/build/tools/emulator.js.map +1 -1
- package/build/tools/files.d.ts +1 -1
- package/build/tools/files.d.ts.map +1 -1
- package/build/tools/files.js +98 -56
- package/build/tools/files.js.map +1 -1
- package/build/tools/firmware-analysis.d.ts.map +1 -1
- package/build/tools/firmware-analysis.js +35 -12
- package/build/tools/firmware-analysis.js.map +1 -1
- package/build/tools/forwarding.d.ts.map +1 -1
- package/build/tools/forwarding.js +248 -6
- package/build/tools/forwarding.js.map +1 -1
- package/build/tools/health.d.ts.map +1 -1
- package/build/tools/health.js +14 -3
- package/build/tools/health.js.map +1 -1
- package/build/tools/input-gestures.d.ts.map +1 -1
- package/build/tools/input-gestures.js +65 -13
- package/build/tools/input-gestures.js.map +1 -1
- package/build/tools/logcat-watch.d.ts.map +1 -1
- package/build/tools/logcat-watch.js +21 -14
- package/build/tools/logcat-watch.js.map +1 -1
- package/build/tools/logs.d.ts.map +1 -1
- package/build/tools/logs.js +8 -4
- package/build/tools/logs.js.map +1 -1
- package/build/tools/mirroring.d.ts.map +1 -1
- package/build/tools/mirroring.js +21 -19
- package/build/tools/mirroring.js.map +1 -1
- package/build/tools/multi-device.d.ts.map +1 -1
- package/build/tools/multi-device.js +10 -0
- package/build/tools/multi-device.js.map +1 -1
- package/build/tools/network-capture.d.ts.map +1 -1
- package/build/tools/network-capture.js +32 -8
- package/build/tools/network-capture.js.map +1 -1
- package/build/tools/network-discovery.d.ts.map +1 -1
- package/build/tools/network-discovery.js +11 -2
- package/build/tools/network-discovery.js.map +1 -1
- package/build/tools/ota-monitor.d.ts.map +1 -1
- package/build/tools/ota-monitor.js +26 -12
- package/build/tools/ota-monitor.js.map +1 -1
- package/build/tools/packages.d.ts.map +1 -1
- package/build/tools/packages.js +18 -11
- package/build/tools/packages.js.map +1 -1
- package/build/tools/plugins.d.ts +18 -0
- package/build/tools/plugins.d.ts.map +1 -1
- package/build/tools/plugins.js +47 -2
- package/build/tools/plugins.js.map +1 -1
- package/build/tools/prompts.d.ts.map +1 -1
- package/build/tools/prompts.js +2 -0
- package/build/tools/prompts.js.map +1 -1
- package/build/tools/qemu.d.ts +6 -3
- package/build/tools/qemu.d.ts.map +1 -1
- package/build/tools/qemu.js +28 -16
- package/build/tools/qemu.js.map +1 -1
- package/build/tools/registry.d.ts.map +1 -1
- package/build/tools/registry.js +76 -26
- package/build/tools/registry.js.map +1 -1
- package/build/tools/regression.d.ts +37 -0
- package/build/tools/regression.d.ts.map +1 -1
- package/build/tools/regression.js +63 -22
- package/build/tools/regression.js.map +1 -1
- package/build/tools/resources.d.ts.map +1 -1
- package/build/tools/resources.js +62 -15
- package/build/tools/resources.js.map +1 -1
- package/build/tools/result-handles.d.ts +37 -0
- package/build/tools/result-handles.d.ts.map +1 -0
- package/build/tools/result-handles.js +286 -0
- package/build/tools/result-handles.js.map +1 -0
- package/build/tools/ril-intercept.d.ts.map +1 -1
- package/build/tools/ril-intercept.js +15 -18
- package/build/tools/ril-intercept.js.map +1 -1
- package/build/tools/screen-record.d.ts.map +1 -1
- package/build/tools/screen-record.js +34 -5
- package/build/tools/screen-record.js.map +1 -1
- package/build/tools/screenshot-diff.d.ts.map +1 -1
- package/build/tools/screenshot-diff.js +29 -21
- package/build/tools/screenshot-diff.js.map +1 -1
- package/build/tools/selinux-audit.d.ts.map +1 -1
- package/build/tools/selinux-audit.js +7 -5
- package/build/tools/selinux-audit.js.map +1 -1
- package/build/tools/sensors.d.ts.map +1 -1
- package/build/tools/sensors.js +26 -4
- package/build/tools/sensors.js.map +1 -1
- package/build/tools/shell.d.ts.map +1 -1
- package/build/tools/shell.js +9 -0
- package/build/tools/shell.js.map +1 -1
- package/build/tools/snapshot.d.ts.map +1 -1
- package/build/tools/snapshot.js +38 -19
- package/build/tools/snapshot.js.map +1 -1
- package/build/tools/split-apk.d.ts.map +1 -1
- package/build/tools/split-apk.js +48 -24
- package/build/tools/split-apk.js.map +1 -1
- package/build/tools/test-gen.d.ts.map +1 -1
- package/build/tools/test-gen.js +29 -10
- package/build/tools/test-gen.js.map +1 -1
- package/build/tools/testing.d.ts.map +1 -1
- package/build/tools/testing.js +27 -9
- package/build/tools/testing.js.map +1 -1
- package/build/tools/thermal-power.d.ts +8 -1
- package/build/tools/thermal-power.d.ts.map +1 -1
- package/build/tools/thermal-power.js +35 -10
- package/build/tools/thermal-power.js.map +1 -1
- package/build/tools/ui.d.ts.map +1 -1
- package/build/tools/ui.js +20 -13
- package/build/tools/ui.js.map +1 -1
- package/build/tools/wireless-firmware.d.ts.map +1 -1
- package/build/tools/wireless-firmware.js +21 -7
- package/build/tools/wireless-firmware.js.map +1 -1
- package/build/tools/wireless.d.ts.map +1 -1
- package/build/tools/wireless.js +39 -3
- package/build/tools/wireless.js.map +1 -1
- package/build/tools/workflow-market.d.ts.map +1 -1
- package/build/tools/workflow-market.js +67 -13
- package/build/tools/workflow-market.js.map +1 -1
- package/build/tools/workflow.d.ts.map +1 -1
- package/build/tools/workflow.js +77 -8
- package/build/tools/workflow.js.map +1 -1
- package/build/ws-transport.d.ts.map +1 -1
- package/build/ws-transport.js +21 -3
- package/build/ws-transport.js.map +1 -1
- package/package.json +20 -8
|
@@ -1,20 +1,53 @@
|
|
|
1
|
+
// Copyright 2026 Jason <fullread@github>
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* Process Cleanup Registry — Centralized signal handler for child process cleanup.
|
|
3
5
|
*
|
|
4
|
-
* Multiple modules (logcat-watch, ril-intercept, mirroring
|
|
5
|
-
* child processes that must be killed on server exit.
|
|
6
|
-
* independently registering SIGINT/SIGTERM/exit
|
|
7
|
-
* ordering dependencies and redundant
|
|
8
|
-
* register their cleanup functions here.
|
|
6
|
+
* Multiple modules (logcat-watch, ril-intercept, mirroring, emulator, qemu)
|
|
7
|
+
* spawn long-running child processes that must be killed on server exit.
|
|
8
|
+
* Instead of each module independently registering SIGINT/SIGTERM/exit
|
|
9
|
+
* handlers (which creates ordering dependencies and redundant
|
|
10
|
+
* process.exit() calls), all modules register their cleanup functions here.
|
|
9
11
|
*
|
|
10
12
|
* One set of signal handlers runs all registered cleanups in order.
|
|
13
|
+
*
|
|
14
|
+
* Cleanup functions may return a Promise; the SIGINT/SIGTERM handlers
|
|
15
|
+
* await them. The synchronous 'exit' handler is best-effort — it fires
|
|
16
|
+
* the cleanups but cannot wait for async work to complete.
|
|
11
17
|
*/
|
|
12
18
|
const registry = new Map();
|
|
13
19
|
let registered = false;
|
|
14
|
-
function runAllCleanups() {
|
|
15
|
-
for (const [, fn] of registry) {
|
|
20
|
+
async function runAllCleanups() {
|
|
21
|
+
for (const [key, fn] of registry) {
|
|
22
|
+
try {
|
|
23
|
+
await fn();
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
// K7 fix: log the failing cleanup's key + error to stderr so
|
|
27
|
+
// operators can diagnose process-zombification (the previous
|
|
28
|
+
// empty catch made this invisible).
|
|
29
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
30
|
+
process.stderr.write(`[cleanup] "${key}" failed: ${detail}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
let warnedAsyncOnExit = false;
|
|
35
|
+
function runAllCleanupsSync() {
|
|
36
|
+
// Used by the 'exit' event handler, which cannot await. Async cleanups
|
|
37
|
+
// are still invoked but their promises are discarded — best-effort.
|
|
38
|
+
// K4 fix: log a one-shot stderr warning the first time we discard an
|
|
39
|
+
// async cleanup so developers see "this cleanup may be cut short on
|
|
40
|
+
// abnormal exit; for guaranteed completion route shutdown through
|
|
41
|
+
// SIGINT or SIGTERM."
|
|
42
|
+
for (const [key, fn] of registry) {
|
|
16
43
|
try {
|
|
17
|
-
fn();
|
|
44
|
+
const ret = fn();
|
|
45
|
+
if (ret && typeof ret.then === "function") {
|
|
46
|
+
if (!warnedAsyncOnExit) {
|
|
47
|
+
warnedAsyncOnExit = true;
|
|
48
|
+
process.stderr.write(`[cleanup] async cleanup "${key}" invoked from 'exit' handler — promise discarded. Async work may be cut short on abnormal exit; SIGINT/SIGTERM awaits all cleanups.\n`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
18
51
|
}
|
|
19
52
|
catch {
|
|
20
53
|
// Cleanup must not throw during shutdown
|
|
@@ -25,20 +58,50 @@ function ensureHandlers() {
|
|
|
25
58
|
if (registered)
|
|
26
59
|
return;
|
|
27
60
|
registered = true;
|
|
28
|
-
process.on("exit",
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
61
|
+
process.on("exit", runAllCleanupsSync);
|
|
62
|
+
// K3 fix: SIGINT/SIGTERM handlers exit with the conventional Unix
|
|
63
|
+
// signal-based code (128 + signum) instead of always 0, so wrapper
|
|
64
|
+
// scripts checking $? can distinguish "completed normally" from
|
|
65
|
+
// "interrupted by user".
|
|
66
|
+
// SIGINT = signum 2 → exit 130
|
|
67
|
+
// SIGTERM = signum 15 → exit 143
|
|
68
|
+
process.on("SIGINT", async () => {
|
|
69
|
+
await runAllCleanups();
|
|
70
|
+
process.exit(130);
|
|
71
|
+
});
|
|
72
|
+
process.on("SIGTERM", async () => {
|
|
73
|
+
await runAllCleanups();
|
|
74
|
+
process.exit(143);
|
|
75
|
+
});
|
|
76
|
+
// K1 fix: cover additional shutdown paths that previously leaked child
|
|
77
|
+
// processes — SIGHUP (terminal disconnect), SIGBREAK (Windows Ctrl+Break),
|
|
78
|
+
// and catastrophic JS errors (uncaughtException, unhandledRejection).
|
|
79
|
+
// Without these, closing a parent terminal without Ctrl+C left orphan
|
|
80
|
+
// emulator/scrcpy/logcat children. Process.on for unsupported signals on
|
|
81
|
+
// a given platform is a no-op, so this is safe cross-platform.
|
|
82
|
+
process.on("SIGHUP", async () => {
|
|
83
|
+
await runAllCleanups();
|
|
84
|
+
process.exit(129); // 128 + SIGHUP(1)
|
|
32
85
|
});
|
|
33
|
-
process.on("
|
|
34
|
-
runAllCleanups();
|
|
35
|
-
process.exit(
|
|
86
|
+
process.on("SIGBREAK", async () => {
|
|
87
|
+
await runAllCleanups();
|
|
88
|
+
process.exit(149); // 128 + SIGBREAK(21) on Windows
|
|
89
|
+
});
|
|
90
|
+
process.on("uncaughtException", async (err) => {
|
|
91
|
+
process.stderr.write(`[cleanup] uncaughtException: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
92
|
+
await runAllCleanups();
|
|
93
|
+
process.exit(1);
|
|
94
|
+
});
|
|
95
|
+
process.on("unhandledRejection", async (reason) => {
|
|
96
|
+
process.stderr.write(`[cleanup] unhandledRejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}\n`);
|
|
97
|
+
await runAllCleanups();
|
|
98
|
+
process.exit(1);
|
|
36
99
|
});
|
|
37
100
|
}
|
|
38
101
|
/**
|
|
39
102
|
* Register a cleanup function that will be called on process exit/SIGINT/SIGTERM.
|
|
40
103
|
* Each module should register under a unique key. Re-registering the same key
|
|
41
|
-
* replaces the previous function.
|
|
104
|
+
* replaces the previous function. The function may return a Promise.
|
|
42
105
|
*/
|
|
43
106
|
export function registerCleanup(key, fn) {
|
|
44
107
|
ensureHandlers();
|
|
@@ -50,4 +113,73 @@ export function registerCleanup(key, fn) {
|
|
|
50
113
|
export function unregisterCleanup(key) {
|
|
51
114
|
registry.delete(key);
|
|
52
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Kill a child process with two-stage SIGTERM → SIGKILL fallback.
|
|
118
|
+
*
|
|
119
|
+
* Sends SIGTERM, waits up to `graceMs` for the child to exit voluntarily,
|
|
120
|
+
* then escalates to SIGKILL if it's still running. Resolves when the
|
|
121
|
+
* child has exited OR after the SIGKILL has been sent.
|
|
122
|
+
*
|
|
123
|
+
* This closes audit findings AC3 (emulator) and AK8 (mirroring) — and
|
|
124
|
+
* matches the same hazard in logcat-watch.ts and ril-intercept.ts where
|
|
125
|
+
* a bare `proc.kill()` could leave a hung child orphaned if it ignored
|
|
126
|
+
* SIGTERM (common with mid-snapshot Android emulators, hung video pipes
|
|
127
|
+
* in scrcpy, and stuck logcat readers on a wedged device).
|
|
128
|
+
*
|
|
129
|
+
* Default `graceMs` = 1500: enough for a healthy child to clean up,
|
|
130
|
+
* short enough that shutdown stays responsive.
|
|
131
|
+
*
|
|
132
|
+
* Safe to call on an already-exited child (no-op). Safe to call from
|
|
133
|
+
* SIGINT/SIGTERM contexts via async cleanups registered with
|
|
134
|
+
* `registerCleanup` — the registry awaits each cleanup before exiting.
|
|
135
|
+
*/
|
|
136
|
+
export async function gracefulKill(child, graceMs = 1500) {
|
|
137
|
+
// No-op if already dead or never started.
|
|
138
|
+
if (!child.pid || child.exitCode !== null || child.signalCode !== null) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Stage 1: Send SIGTERM.
|
|
142
|
+
try {
|
|
143
|
+
child.kill("SIGTERM");
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Child may have died between the check and the kill — treat as success.
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Wait for the child to exit, OR for graceMs to elapse.
|
|
150
|
+
const exited = await new Promise((resolve) => {
|
|
151
|
+
let resolved = false;
|
|
152
|
+
const finish = (didExit) => {
|
|
153
|
+
if (resolved)
|
|
154
|
+
return;
|
|
155
|
+
resolved = true;
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
child.off("exit", onExit);
|
|
158
|
+
resolve(didExit);
|
|
159
|
+
};
|
|
160
|
+
const onExit = () => finish(true);
|
|
161
|
+
const timer = setTimeout(() => finish(false), graceMs);
|
|
162
|
+
child.once("exit", onExit);
|
|
163
|
+
});
|
|
164
|
+
// Stage 2: If still alive, escalate to SIGKILL.
|
|
165
|
+
if (!exited) {
|
|
166
|
+
try {
|
|
167
|
+
child.kill("SIGKILL");
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Child raced us — already dead.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Convenience: gracefully kill multiple children in parallel.
|
|
176
|
+
* All children get SIGTERM concurrently; each gets SIGKILL after graceMs
|
|
177
|
+
* if still running. Resolves when all have settled.
|
|
178
|
+
*/
|
|
179
|
+
export async function gracefulKillAll(children, graceMs = 1500) {
|
|
180
|
+
const tasks = [];
|
|
181
|
+
for (const c of children)
|
|
182
|
+
tasks.push(gracefulKill(c, graceMs));
|
|
183
|
+
await Promise.all(tasks);
|
|
184
|
+
}
|
|
53
185
|
//# sourceMappingURL=cleanup.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/middleware/cleanup.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../src/middleware/cleanup.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,19 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fetch Utilities — Shared HTTP helpers with
|
|
2
|
+
* Fetch Utilities — Shared HTTP helpers with protocol, timeout, redirect, and size limits.
|
|
3
3
|
*
|
|
4
|
-
* Used by the plugin registry, workflow marketplace, and any module
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Used by the plugin registry, workflow marketplace, and any module that
|
|
5
|
+
* fetches external resources. Layered defenses against malicious or
|
|
6
|
+
* misconfigured external sources:
|
|
7
|
+
* - Protocol allowlist: https:// only (no http://, data:, file://, etc.).
|
|
8
|
+
* Validated on the INITIAL url AND on every redirect target. Defends
|
|
9
|
+
* against SSRF (file://, http://internal.rfc1918), plaintext credential
|
|
10
|
+
* leak (http://), and content injection via local-file or inline-data URLs.
|
|
11
|
+
* - Manual redirect handling: redirects are NOT auto-followed by the
|
|
12
|
+
* underlying fetch — we follow them ourselves with per-hop protocol
|
|
13
|
+
* re-validation and a 5-hop cap. Defends against a compromised or
|
|
14
|
+
* hijacked initial endpoint that 302-redirects to http://attacker.com
|
|
15
|
+
* or file:///etc/passwd, which the default `redirect: "follow"` would
|
|
16
|
+
* traverse silently and bypass the initial protocol check.
|
|
17
|
+
* - Request timeout: 30 seconds via AbortSignal.timeout() — shared across
|
|
18
|
+
* redirect hops, header arrival, AND body read. Total operation is
|
|
19
|
+
* bounded by one 30s window, not 30s × hops. Defends against slow-loris
|
|
20
|
+
* servers and slow-drip redirect chains.
|
|
21
|
+
* - Response size cap: 5 MB streaming check. Defends against memory
|
|
22
|
+
* exhaustion from oversized or pathological responses, even when the
|
|
23
|
+
* server omits Content-Length.
|
|
24
|
+
*
|
|
25
|
+
* Tests that need to exercise post-fetch parsing logic should mock
|
|
26
|
+
* fetchText/fetchJson at the function level rather than feeding non-https
|
|
27
|
+
* URLs through the real fetcher. Redirect-following behavior is validated
|
|
28
|
+
* during manual audit at release prep (per the test-supply-chain.mjs convention).
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Throw if the URL does not begin with `https://`. Applied to the initial
|
|
32
|
+
* URL and re-applied to every redirect target so a `Location: http://...`
|
|
33
|
+
* header cannot bypass the protocol allowlist.
|
|
34
|
+
*
|
|
35
|
+
* Exported for unit testing.
|
|
7
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* H1 note: case-sensitivity is INTENTIONAL. RFC 3986 says scheme is
|
|
39
|
+
* case-insensitive, so technically `HTTPS://example.com` is valid. But
|
|
40
|
+
* any URL produced by a real client library normalizes to lowercase, so
|
|
41
|
+
* an uppercase scheme in practice signals either (a) a hand-typed URL by
|
|
42
|
+
* an operator (the strict check catches typos), or (b) an adversarial
|
|
43
|
+
* input trying to bypass an allowlist check by using a non-canonical
|
|
44
|
+
* form. Strict-lowercase is the safer choice for an allowlist gate.
|
|
45
|
+
*/
|
|
46
|
+
export declare function requireHttps(url: string, context: string): void;
|
|
8
47
|
/**
|
|
9
48
|
* Fetch a URL and parse the response as JSON.
|
|
10
|
-
* Enforces
|
|
49
|
+
* Enforces protocol, timeout, redirect, and size limits via fetchText().
|
|
11
50
|
*/
|
|
12
51
|
export declare function fetchJson(url: string): Promise<unknown>;
|
|
13
52
|
/**
|
|
14
53
|
* Fetch a URL and return the response body as text.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
54
|
+
*
|
|
55
|
+
* Enforces:
|
|
56
|
+
* - https:// protocol on the INITIAL url AND every redirect target
|
|
57
|
+
* - At most 5 redirect hops (fail-closed beyond)
|
|
58
|
+
* - 30s total timeout via shared AbortSignal (covers hops + body read)
|
|
59
|
+
* - 5 MB body size cap via streaming size check
|
|
17
60
|
*/
|
|
18
61
|
export declare function fetchText(url: string): Promise<string>;
|
|
19
62
|
//# sourceMappingURL=fetch-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-utils.d.ts","sourceRoot":"","sources":["../../src/middleware/fetch-utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fetch-utils.d.ts","sourceRoot":"","sources":["../../src/middleware/fetch-utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAcH;;;;;;GAMG;AACH;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAM/D;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAY7D;AAED;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoG5D"}
|
|
@@ -1,37 +1,143 @@
|
|
|
1
|
+
// Copyright 2026 Jason <fullread@github>
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
|
-
* Fetch Utilities — Shared HTTP helpers with
|
|
4
|
+
* Fetch Utilities — Shared HTTP helpers with protocol, timeout, redirect, and size limits.
|
|
3
5
|
*
|
|
4
|
-
* Used by the plugin registry, workflow marketplace, and any module
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* Used by the plugin registry, workflow marketplace, and any module that
|
|
7
|
+
* fetches external resources. Layered defenses against malicious or
|
|
8
|
+
* misconfigured external sources:
|
|
9
|
+
* - Protocol allowlist: https:// only (no http://, data:, file://, etc.).
|
|
10
|
+
* Validated on the INITIAL url AND on every redirect target. Defends
|
|
11
|
+
* against SSRF (file://, http://internal.rfc1918), plaintext credential
|
|
12
|
+
* leak (http://), and content injection via local-file or inline-data URLs.
|
|
13
|
+
* - Manual redirect handling: redirects are NOT auto-followed by the
|
|
14
|
+
* underlying fetch — we follow them ourselves with per-hop protocol
|
|
15
|
+
* re-validation and a 5-hop cap. Defends against a compromised or
|
|
16
|
+
* hijacked initial endpoint that 302-redirects to http://attacker.com
|
|
17
|
+
* or file:///etc/passwd, which the default `redirect: "follow"` would
|
|
18
|
+
* traverse silently and bypass the initial protocol check.
|
|
19
|
+
* - Request timeout: 30 seconds via AbortSignal.timeout() — shared across
|
|
20
|
+
* redirect hops, header arrival, AND body read. Total operation is
|
|
21
|
+
* bounded by one 30s window, not 30s × hops. Defends against slow-loris
|
|
22
|
+
* servers and slow-drip redirect chains.
|
|
23
|
+
* - Response size cap: 5 MB streaming check. Defends against memory
|
|
24
|
+
* exhaustion from oversized or pathological responses, even when the
|
|
25
|
+
* server omits Content-Length.
|
|
26
|
+
*
|
|
27
|
+
* Tests that need to exercise post-fetch parsing logic should mock
|
|
28
|
+
* fetchText/fetchJson at the function level rather than feeding non-https
|
|
29
|
+
* URLs through the real fetcher. Redirect-following behavior is validated
|
|
30
|
+
* during manual audit at release prep (per the test-supply-chain.mjs convention).
|
|
7
31
|
*/
|
|
8
32
|
/** Maximum response body size in bytes (5 MB). */
|
|
9
33
|
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
34
|
+
/** Maximum time to wait for the entire fetch operation, in milliseconds (30 seconds).
|
|
35
|
+
* Covers initial request, all redirect hops, header arrival, and body read. */
|
|
36
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
37
|
+
/** Maximum number of redirect hops to follow before failing closed.
|
|
38
|
+
* Matches the typical 5-hop limits in browsers (curl defaults to 50,
|
|
39
|
+
* but for a supply-chain fetch we want tight bounds). */
|
|
40
|
+
const MAX_REDIRECTS = 5;
|
|
41
|
+
/**
|
|
42
|
+
* Throw if the URL does not begin with `https://`. Applied to the initial
|
|
43
|
+
* URL and re-applied to every redirect target so a `Location: http://...`
|
|
44
|
+
* header cannot bypass the protocol allowlist.
|
|
45
|
+
*
|
|
46
|
+
* Exported for unit testing.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* H1 note: case-sensitivity is INTENTIONAL. RFC 3986 says scheme is
|
|
50
|
+
* case-insensitive, so technically `HTTPS://example.com` is valid. But
|
|
51
|
+
* any URL produced by a real client library normalizes to lowercase, so
|
|
52
|
+
* an uppercase scheme in practice signals either (a) a hand-typed URL by
|
|
53
|
+
* an operator (the strict check catches typos), or (b) an adversarial
|
|
54
|
+
* input trying to bypass an allowlist check by using a non-canonical
|
|
55
|
+
* form. Strict-lowercase is the safer choice for an allowlist gate.
|
|
56
|
+
*/
|
|
57
|
+
export function requireHttps(url, context) {
|
|
58
|
+
if (!url.startsWith("https://")) {
|
|
59
|
+
throw new Error(`Refusing to fetch non-https URL: ${url} (${context}). Only https:// URLs are permitted.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
10
62
|
/**
|
|
11
63
|
* Fetch a URL and parse the response as JSON.
|
|
12
|
-
* Enforces
|
|
64
|
+
* Enforces protocol, timeout, redirect, and size limits via fetchText().
|
|
13
65
|
*/
|
|
14
66
|
export async function fetchJson(url) {
|
|
15
67
|
const text = await fetchText(url);
|
|
16
|
-
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(text);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// H2 fix: wrap JSON parse errors with the URL context so a misconfigured
|
|
73
|
+
// registry returning an HTML error page produces "Invalid JSON from
|
|
74
|
+
// https://... — Unexpected token <" instead of a bare parse error with
|
|
75
|
+
// no indication of which fetch failed.
|
|
76
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
77
|
+
throw new Error(`Invalid JSON from ${url} — ${detail}`, { cause: err });
|
|
78
|
+
}
|
|
17
79
|
}
|
|
18
80
|
/**
|
|
19
81
|
* Fetch a URL and return the response body as text.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
82
|
+
*
|
|
83
|
+
* Enforces:
|
|
84
|
+
* - https:// protocol on the INITIAL url AND every redirect target
|
|
85
|
+
* - At most 5 redirect hops (fail-closed beyond)
|
|
86
|
+
* - 30s total timeout via shared AbortSignal (covers hops + body read)
|
|
87
|
+
* - 5 MB body size cap via streaming size check
|
|
22
88
|
*/
|
|
23
89
|
export async function fetchText(url) {
|
|
24
|
-
|
|
90
|
+
requireHttps(url, "initial URL");
|
|
91
|
+
// Share one AbortSignal across all redirect hops, header arrival, and body
|
|
92
|
+
// read. The 30s budget is for the entire operation — a chain of slow-drip
|
|
93
|
+
// redirects cannot stack up to 30s × hops.
|
|
94
|
+
const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
95
|
+
// Manual redirect handling so we can re-validate the protocol of every
|
|
96
|
+
// hop. The default `redirect: "follow"` would silently traverse a
|
|
97
|
+
// https→http or https→file redirect from a compromised initial endpoint,
|
|
98
|
+
// defeating the protocol allowlist entirely.
|
|
99
|
+
let currentUrl = url;
|
|
100
|
+
let response = null;
|
|
101
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
102
|
+
const r = await fetch(currentUrl, { signal, redirect: "manual" });
|
|
103
|
+
// 304 Not Modified is a 3xx that is NOT a redirect — treat as terminal.
|
|
104
|
+
if (r.status >= 300 && r.status < 400 && r.status !== 304) {
|
|
105
|
+
const location = r.headers.get("location");
|
|
106
|
+
if (!location) {
|
|
107
|
+
// 3xx without Location is unusual but possible (e.g., 305 Use Proxy
|
|
108
|
+
// without a Location header). Treat as a terminal response so the
|
|
109
|
+
// caller sees the raw status.
|
|
110
|
+
response = r;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
// Drain the redirect response body to free the connection eagerly.
|
|
114
|
+
r.body?.cancel().catch(() => { });
|
|
115
|
+
if (hop === MAX_REDIRECTS) {
|
|
116
|
+
throw new Error(`Too many redirects (max ${MAX_REDIRECTS}) starting from ${url}`);
|
|
117
|
+
}
|
|
118
|
+
// Resolve relative URLs against the current URL, then re-validate.
|
|
119
|
+
currentUrl = new URL(location, currentUrl).href;
|
|
120
|
+
requireHttps(currentUrl, `redirect hop ${hop + 1} from ${url}`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
response = r;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
if (!response) {
|
|
127
|
+
// Unreachable in practice — the loop either sets response or throws.
|
|
128
|
+
throw new Error(`Failed to fetch ${url}: no response obtained`);
|
|
129
|
+
}
|
|
25
130
|
if (!response.ok) {
|
|
26
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText} — ${
|
|
131
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText} — ${currentUrl}`);
|
|
27
132
|
}
|
|
28
|
-
// Fast-reject via Content-Length header if available
|
|
133
|
+
// Fast-reject via Content-Length header if available.
|
|
29
134
|
const contentLength = response.headers.get("content-length");
|
|
30
135
|
if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_BYTES) {
|
|
31
136
|
throw new Error(`Response too large: ${contentLength} bytes (max ${MAX_RESPONSE_BYTES})`);
|
|
32
137
|
}
|
|
33
|
-
// Stream-read the body with incremental size checking.
|
|
34
|
-
//
|
|
138
|
+
// Stream-read the body with incremental size checking. The AbortSignal
|
|
139
|
+
// attached to fetch above also applies to body reads, so a slow-drip server
|
|
140
|
+
// will trip the 30s timeout here rather than hanging until MAX_RESPONSE_BYTES.
|
|
35
141
|
if (response.body) {
|
|
36
142
|
const reader = response.body.getReader();
|
|
37
143
|
const chunks = [];
|
|
@@ -54,7 +160,14 @@ export async function fetchText(url) {
|
|
|
54
160
|
}
|
|
55
161
|
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
56
162
|
}
|
|
57
|
-
// Fallback for environments where response.body is null
|
|
163
|
+
// Fallback for environments where response.body is null (extremely rare
|
|
164
|
+
// in modern Node). H4 audit note: the function-level Content-Length
|
|
165
|
+
// pre-check at the top of this function already rejects declared-oversize
|
|
166
|
+
// bodies BEFORE we get here. The remaining unguarded case is a chunked
|
|
167
|
+
// response with no Content-Length AND null response.body — buffer below
|
|
168
|
+
// and post-read length check is the best we can do without a streaming
|
|
169
|
+
// body. Accept the unbounded read for this rare path; the post-check
|
|
170
|
+
// still catches the result.
|
|
58
171
|
const text = await response.text();
|
|
59
172
|
if (text.length > MAX_RESPONSE_BYTES) {
|
|
60
173
|
throw new Error(`Response body too large: ${text.length} characters (max ${MAX_RESPONSE_BYTES})`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-utils.js","sourceRoot":"","sources":["../../src/middleware/fetch-utils.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fetch-utils.js","sourceRoot":"","sources":["../../src/middleware/fetch-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem Utilities - Shared helpers for safe on-disk state.
|
|
3
|
+
*
|
|
4
|
+
* Four primitives used across DeepADB tempDir-backed storage:
|
|
5
|
+
*
|
|
6
|
+
* 1. ensurePrivateDir(path) - Create a directory with restrictive
|
|
7
|
+
* permissions (0o700 on Unix; mode bits ignored on Windows). Use
|
|
8
|
+
* everywhere we create a subdirectory under tempDir. Defends against
|
|
9
|
+
* other-user reads on shared Unix hosts.
|
|
10
|
+
*
|
|
11
|
+
* 2. writeAtomicSync(path, data) - Write via a unique-per-call .tmp
|
|
12
|
+
* path then atomic rename. Defends against (a) mid-write crashes
|
|
13
|
+
* leaving corrupt data on disk, (b) concurrent readers seeing a
|
|
14
|
+
* partially-written file, (c) concurrent writers to the same path
|
|
15
|
+
* racing on .tmp filenames.
|
|
16
|
+
*
|
|
17
|
+
* 3. sanitizeFilenameComponent(name, maxLen?) - Replace any character
|
|
18
|
+
* outside [a-zA-Z0-9_-] with underscore. Optional maxLen truncates
|
|
19
|
+
* the result. Empty results are coerced to "_" so the caller never
|
|
20
|
+
* receives a zero-length component. Centralizes the pattern used
|
|
21
|
+
* across several tool modules for user-supplied identifier strings.
|
|
22
|
+
*
|
|
23
|
+
* 4. sanitizeFilenameComponentDotted(name, maxLen?) - Like (3) but
|
|
24
|
+
* ALSO allows "." in the output (for package names like
|
|
25
|
+
* "com.example.app"). Defangs pure-dot strings (".", "..", "...")
|
|
26
|
+
* by coercing them to "_", so a caller using the result directly
|
|
27
|
+
* as a path component cannot be steered into parent-directory
|
|
28
|
+
* traversal by an adversarial input.
|
|
29
|
+
*
|
|
30
|
+
* Note on existing directories: ensurePrivateDir only sets mode on
|
|
31
|
+
* directories it CREATES. Directories that already exist (from older
|
|
32
|
+
* DeepADB versions) keep their existing permissions. To remediate, the
|
|
33
|
+
* operator can chmod 700 the affected directories manually, or just
|
|
34
|
+
* delete them so they are recreated with the new mode.
|
|
35
|
+
*/
|
|
36
|
+
/** Create directory with owner-only permissions. Idempotent. */
|
|
37
|
+
export declare function ensurePrivateDir(path: string): void;
|
|
38
|
+
/** Atomic write: unique .tmp + rename. Throws on failure (caller handles). */
|
|
39
|
+
export declare function writeAtomicSync(path: string, data: string | Buffer): void;
|
|
40
|
+
/**
|
|
41
|
+
* Sanitize a string for use as a filename component. Replaces every
|
|
42
|
+
* character outside [a-zA-Z0-9_-] with "_".
|
|
43
|
+
*
|
|
44
|
+
* Safe against path traversal because `.`, `/`, and `\\` are all outside
|
|
45
|
+
* the allowlist (they become `_`). For cases where rejection is preferable
|
|
46
|
+
* to silent replacement, validate the raw input against /^[a-zA-Z0-9_-]+$/
|
|
47
|
+
* before calling tools.
|
|
48
|
+
*
|
|
49
|
+
* @param name Raw user-supplied identifier.
|
|
50
|
+
* @param maxLen Optional length cap applied after sanitization. Callers
|
|
51
|
+
* that build long paths (Windows MAX_PATH-sensitive) should
|
|
52
|
+
* always pass this.
|
|
53
|
+
* @returns Sanitized string, guaranteed non-empty (empty input collapses
|
|
54
|
+
* to "_" so callers never receive a zero-length component).
|
|
55
|
+
*/
|
|
56
|
+
export declare function sanitizeFilenameComponent(name: string, maxLen?: number): string;
|
|
57
|
+
/**
|
|
58
|
+
* Like sanitizeFilenameComponent but ALSO allows "." in the output.
|
|
59
|
+
* Intended for Android-style package identifiers ("com.example.app") and
|
|
60
|
+
* similar dotted names where stripping dots would damage the identifier.
|
|
61
|
+
*
|
|
62
|
+
* Pure-dot strings (".", "..", "...", etc.) are coerced to "_" so a
|
|
63
|
+
* caller that uses the result directly as a path component cannot be
|
|
64
|
+
* steered into parent-directory traversal by an adversarial input.
|
|
65
|
+
*
|
|
66
|
+
* @param name Raw user-supplied dotted identifier (e.g., package name).
|
|
67
|
+
* @param maxLen Optional length cap applied after sanitization.
|
|
68
|
+
* @returns Sanitized string, guaranteed non-empty AND not a pure-dot
|
|
69
|
+
* traversal sequence.
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* A8 note: a LEADING dot is preserved (only character-class filtering
|
|
73
|
+
* applies — the dot is in the allowed set). On Unix, a result like
|
|
74
|
+
* `.hidden` becomes an actual hidden file in the result-handle store and
|
|
75
|
+
* other directory listings. This is intentional under the design (callers
|
|
76
|
+
* who want to allow package names like com.example.app need the dot), but
|
|
77
|
+
* means operators browsing the temp directory should pass `ls -A` not
|
|
78
|
+
* bare `ls` to see all entries. If a future caller wants the no-leading-
|
|
79
|
+
* dot behavior, it should sanitize the input before calling this helper.
|
|
80
|
+
*/
|
|
81
|
+
export declare function sanitizeFilenameComponentDotted(name: string, maxLen?: number): string;
|
|
82
|
+
/**
|
|
83
|
+
* Returns true iff `candidatePath`, after path resolution, is contained
|
|
84
|
+
* within `dir` — either equal to `dir` or strictly underneath it.
|
|
85
|
+
*
|
|
86
|
+
* Containment uses a path-separator boundary check, NOT a string prefix.
|
|
87
|
+
* Without the boundary, `candidatePath = /parent/tempDir_evil/x` would
|
|
88
|
+
* pass containment against `dir = /parent/tempDir` because the absolute
|
|
89
|
+
* path string-prefix-matches — even though the two are sibling directories.
|
|
90
|
+
*
|
|
91
|
+
* Both POSIX `/` and Windows `\\` separators are recognized so a candidate
|
|
92
|
+
* resolved on either platform is handled correctly.
|
|
93
|
+
*
|
|
94
|
+
* Usage:
|
|
95
|
+
* if (!isWithinDir(userOutputDir, ctx.config.tempDir)) {
|
|
96
|
+
* throw new Error("Output directory must be inside tempDir");
|
|
97
|
+
* }
|
|
98
|
+
*
|
|
99
|
+
* This is the centralized successor to the inline pattern that previously
|
|
100
|
+
* lived in tools/qemu.ts (verifyContainment, correctly using a separator
|
|
101
|
+
* boundary) and tools/split-apk.ts / tools/registry.ts (which used a bare
|
|
102
|
+
* string-prefix check and were vulnerable to the sibling-directory bypass).
|
|
103
|
+
* Lifting to shared middleware fixes the bypass at both call sites.
|
|
104
|
+
*/
|
|
105
|
+
export declare function isWithinDir(candidatePath: string, dir: string): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Read and JSON-parse a file. On any failure (missing, unreadable, malformed),
|
|
108
|
+
* log a warning via the supplied logger and return null. Used by tools that
|
|
109
|
+
* walk a directory of JSON files (fingerprints, baselines, marketplace
|
|
110
|
+
* workflows, plugin metadata) and want to skip-but-surface corruption rather
|
|
111
|
+
* than abort the whole walk OR silently drop the entry.
|
|
112
|
+
*
|
|
113
|
+
* Closes audit findings AE8 / AO6 / AU6 / BJ7 / BN7 — the recurring silent
|
|
114
|
+
* `} catch { /* skip corrupt * / }` pattern. Surfaces corruption to the
|
|
115
|
+
* operator so a quietly-shrinking handle count doesn't go unnoticed.
|
|
116
|
+
*
|
|
117
|
+
* The logger is optional so this is safe to call from contexts without a
|
|
118
|
+
* Logger in scope (some loadFingerprints / listing functions don't take a
|
|
119
|
+
* ToolContext). When absent, the warning goes to stderr — non-silent.
|
|
120
|
+
*
|
|
121
|
+
* Returns null on any failure; caller should skip the entry.
|
|
122
|
+
*/
|
|
123
|
+
export declare function tryReadJsonOrWarn<T = unknown>(filePath: string, context: string, logger?: {
|
|
124
|
+
warn: (msg: string) => void;
|
|
125
|
+
}): T | null;
|
|
126
|
+
//# sourceMappingURL=fs-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-utils.d.ts","sourceRoot":"","sources":["../../src/middleware/fs-utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAMH,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEnD;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA4BzE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAW/E;AAED;;;;;;;;;;;;;GAaG;AACH;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBrF;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAcvE;AACD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAC3C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACvC,CAAC,GAAG,IAAI,CAUV"}
|