@westbayberry/dg 1.0.0 → 1.0.2
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/dist/index.js +94 -6
- package/package.json +14 -5
package/dist/index.js
CHANGED
|
@@ -19,7 +19,78 @@ class APIError extends Error {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
exports.APIError = APIError;
|
|
22
|
-
|
|
22
|
+
const BATCH_SIZE = 15;
|
|
23
|
+
const MAX_RETRIES = 2;
|
|
24
|
+
const RETRY_DELAY_MS = 5000;
|
|
25
|
+
async function callAnalyzeAPI(packages, config, onProgress) {
|
|
26
|
+
if (packages.length <= BATCH_SIZE) {
|
|
27
|
+
return callAnalyzeBatch(packages, config);
|
|
28
|
+
}
|
|
29
|
+
const batches = [];
|
|
30
|
+
for (let i = 0; i < packages.length; i += BATCH_SIZE) {
|
|
31
|
+
batches.push(packages.slice(i, i + BATCH_SIZE));
|
|
32
|
+
}
|
|
33
|
+
// Process batches sequentially — parallel batches overload the server on cold scans
|
|
34
|
+
const results = [];
|
|
35
|
+
let completed = 0;
|
|
36
|
+
for (const batch of batches) {
|
|
37
|
+
const result = await callBatchWithRetry(batch, config);
|
|
38
|
+
completed += batch.length;
|
|
39
|
+
if (onProgress) {
|
|
40
|
+
onProgress(completed, packages.length);
|
|
41
|
+
}
|
|
42
|
+
results.push(result);
|
|
43
|
+
}
|
|
44
|
+
return mergeResponses(results, config);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Retry a batch on 504 (gateway timeout). The server continues processing
|
|
48
|
+
* during a timeout, so retries find previously-analyzed packages cached
|
|
49
|
+
* in Redis — fewer cold packages means faster response.
|
|
50
|
+
*/
|
|
51
|
+
async function callBatchWithRetry(packages, config) {
|
|
52
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
53
|
+
try {
|
|
54
|
+
return await callAnalyzeBatch(packages, config);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const is504 = error instanceof APIError && error.statusCode === 504;
|
|
58
|
+
const isTimeout = error instanceof APIError && error.statusCode === 408;
|
|
59
|
+
if ((is504 || isTimeout) && attempt < MAX_RETRIES) {
|
|
60
|
+
// Server is still working — wait, then retry.
|
|
61
|
+
// Packages that finished before the timeout are now cached.
|
|
62
|
+
const delay = RETRY_DELAY_MS * (attempt + 1);
|
|
63
|
+
process.stderr.write(` Batch timed out, retrying in ${delay / 1000}s (attempt ${attempt + 2}/${MAX_RETRIES + 1})...\n`);
|
|
64
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Unreachable, but TypeScript needs it
|
|
71
|
+
throw new Error("Exhausted retries");
|
|
72
|
+
}
|
|
73
|
+
function mergeResponses(results, config) {
|
|
74
|
+
const allPackages = results.flatMap((r) => r.packages);
|
|
75
|
+
const maxScore = Math.max(0, ...allPackages.map((p) => p.score));
|
|
76
|
+
const action = maxScore >= config.blockThreshold
|
|
77
|
+
? "block"
|
|
78
|
+
: maxScore >= config.warnThreshold
|
|
79
|
+
? "warn"
|
|
80
|
+
: "pass";
|
|
81
|
+
const safeVersions = {};
|
|
82
|
+
for (const r of results) {
|
|
83
|
+
Object.assign(safeVersions, r.safeVersions);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
score: maxScore,
|
|
87
|
+
action: action,
|
|
88
|
+
packages: allPackages,
|
|
89
|
+
safeVersions,
|
|
90
|
+
durationMs: Math.max(0, ...results.map((r) => r.durationMs)),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function callAnalyzeBatch(packages, config) {
|
|
23
94
|
const url = `${config.apiUrl}/v1/analyze`;
|
|
24
95
|
const payload = {
|
|
25
96
|
packages: packages.map((p) => ({
|
|
@@ -53,7 +124,10 @@ async function callAnalyzeAPI(packages, config) {
|
|
|
53
124
|
if (error instanceof Error && error.name === "AbortError") {
|
|
54
125
|
throw new APIError("Request timed out after 120s. Try scanning fewer packages.", 408, "");
|
|
55
126
|
}
|
|
56
|
-
|
|
127
|
+
// Node.js fetch wraps the real error in .cause
|
|
128
|
+
const cause = error instanceof Error && error.cause;
|
|
129
|
+
const detail = cause ? `: ${cause.message || cause}` : "";
|
|
130
|
+
throw new Error(`fetch failed${detail}`);
|
|
57
131
|
}
|
|
58
132
|
clearTimeout(timeoutId);
|
|
59
133
|
if (response.status === 401) {
|
|
@@ -186,11 +260,15 @@ function parseConfig(argv) {
|
|
|
186
260
|
process.exit(0);
|
|
187
261
|
}
|
|
188
262
|
const command = positionals[0] ?? "scan";
|
|
263
|
+
if (values["api-key"]) {
|
|
264
|
+
process.stderr.write("Warning: --api-key is deprecated (visible in process list). Use DG_API_KEY env var instead.\n");
|
|
265
|
+
}
|
|
189
266
|
const apiKey = values["api-key"] ??
|
|
190
267
|
process.env.DG_API_KEY ??
|
|
191
268
|
"";
|
|
192
269
|
if (!apiKey) {
|
|
193
|
-
process.stderr.write("Error: API key required. Set
|
|
270
|
+
process.stderr.write("Error: API key required. Set DG_API_KEY environment variable.\n" +
|
|
271
|
+
"Do NOT pass keys via --api-key (visible in process list).\n" +
|
|
194
272
|
"Get your key at https://westbayberry.com/dashboard\n");
|
|
195
273
|
process.exit(1);
|
|
196
274
|
}
|
|
@@ -291,15 +369,23 @@ function discoverChanges(cwd, config) {
|
|
|
291
369
|
skipped: diff.skipped,
|
|
292
370
|
};
|
|
293
371
|
}
|
|
294
|
-
// 4. Fallback: try package.json diff,
|
|
372
|
+
// 4. Fallback: try package.json diff, resolve ranges via lockfile
|
|
295
373
|
const pkgJsonPath = (0, node_path_1.join)(cwd, "package.json");
|
|
296
374
|
if ((0, node_fs_1.existsSync)(pkgJsonPath)) {
|
|
297
375
|
const headPkgJson = (0, node_fs_1.readFileSync)(pkgJsonPath, "utf-8");
|
|
298
376
|
const basePkgJson = getGitBaseFile(cwd, "package.json");
|
|
299
377
|
if (basePkgJson !== null) {
|
|
300
378
|
const diff = (0, parse_package_json_1.diffPackageJsons)(basePkgJson, headPkgJson, config.maxPackages);
|
|
379
|
+
// Resolve semver ranges (e.g. "^3.0.1") to exact versions from the lockfile
|
|
380
|
+
const resolved = diff.changes.map((change) => {
|
|
381
|
+
const lockEntry = headParsed.packages.get(change.name);
|
|
382
|
+
return {
|
|
383
|
+
...change,
|
|
384
|
+
newVersion: lockEntry?.version ?? change.newVersion,
|
|
385
|
+
};
|
|
386
|
+
});
|
|
301
387
|
return {
|
|
302
|
-
packages:
|
|
388
|
+
packages: resolved.map(toPackageInput),
|
|
303
389
|
method: "fallback",
|
|
304
390
|
skipped: [],
|
|
305
391
|
};
|
|
@@ -779,7 +865,9 @@ async function main() {
|
|
|
779
865
|
process.stderr.write((0, color_1.yellow)(` Warning: ${discovery.skipped.length} package(s) skipped (max-packages=${config.maxPackages})\n`));
|
|
780
866
|
}
|
|
781
867
|
// Call Detection API
|
|
782
|
-
const result = await (0, api_1.callAnalyzeAPI)(packages, config)
|
|
868
|
+
const result = await (0, api_1.callAnalyzeAPI)(packages, config, (done, total) => {
|
|
869
|
+
process.stderr.write((0, color_1.dim)(` Analyzed ${done}/${total}...\n`));
|
|
870
|
+
});
|
|
783
871
|
// Render output
|
|
784
872
|
const output = (0, output_1.renderResult)(result, config);
|
|
785
873
|
process.stdout.write(output + "\n");
|
package/package.json
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@westbayberry/dg",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Supply chain security scanner — scan npm dependencies in any CI or terminal",
|
|
5
5
|
"bin": {
|
|
6
|
-
"dependency-guardian": "
|
|
7
|
-
"dg": "
|
|
6
|
+
"dependency-guardian": "dist/index.js",
|
|
7
|
+
"dg": "dist/index.js"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
10
12
|
"license": "UNLICENSED",
|
|
11
13
|
"author": "WestBayBerry",
|
|
12
14
|
"homepage": "https://westbayberry.com",
|
|
13
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"security",
|
|
17
|
+
"npm",
|
|
18
|
+
"supply-chain",
|
|
19
|
+
"scanner",
|
|
20
|
+
"cli",
|
|
21
|
+
"dependencies"
|
|
22
|
+
],
|
|
14
23
|
"publishConfig": {
|
|
15
24
|
"access": "public"
|
|
16
25
|
},
|