@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.
Files changed (2) hide show
  1. package/dist/index.js +94 -6
  2. 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
- async function callAnalyzeAPI(packages, config) {
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
- throw error;
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 --api-key or DG_API_KEY environment variable.\n" +
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, else scan all
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: diff.changes.map(toPackageInput),
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.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": "./dist/index.js",
7
- "dg": "./dist/index.js"
6
+ "dependency-guardian": "dist/index.js",
7
+ "dg": "dist/index.js"
8
8
  },
9
- "files": ["dist"],
9
+ "files": [
10
+ "dist"
11
+ ],
10
12
  "license": "UNLICENSED",
11
13
  "author": "WestBayBerry",
12
14
  "homepage": "https://westbayberry.com",
13
- "keywords": ["security", "npm", "supply-chain", "scanner", "cli", "dependencies"],
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
  },