@prodcycle/prodcycle 0.6.2 → 0.6.4
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/api-client.js +16 -4
- package/dist/utils/fs.js +31 -12
- package/package.json +1 -1
package/dist/api-client.js
CHANGED
|
@@ -322,6 +322,7 @@ class ComplianceApiClient {
|
|
|
322
322
|
let lastError = null;
|
|
323
323
|
for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
|
|
324
324
|
let response;
|
|
325
|
+
let responseText;
|
|
325
326
|
try {
|
|
326
327
|
response = await fetch(url, {
|
|
327
328
|
method,
|
|
@@ -332,11 +333,23 @@ class ComplianceApiClient {
|
|
|
332
333
|
...(data !== null ? { body: JSON.stringify(data) } : {}),
|
|
333
334
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
334
335
|
});
|
|
336
|
+
// Body read inside the same try/catch as fetch() because undici can
|
|
337
|
+
// throw mid-stream (ALB drops the connection, abort signal fires
|
|
338
|
+
// during body read, server sends a partial response). Pre-fix this
|
|
339
|
+
// leaked out of `request()` as an unhandled error instead of being
|
|
340
|
+
// retried — long chunked-session scans were especially exposed,
|
|
341
|
+
// since every `appendChunk` call is its own request and any one
|
|
342
|
+
// mid-stream drop torpedoed the whole scan. The chunk write is
|
|
343
|
+
// idempotent on the server (unique `(scan_id, fingerprint)` index),
|
|
344
|
+
// so retry is safe. Mirror of the Python client's catch over
|
|
345
|
+
// `OSError, http.client.HTTPException` — keep both in lockstep.
|
|
346
|
+
responseText = await response.text();
|
|
335
347
|
}
|
|
336
348
|
catch (networkErr) {
|
|
337
|
-
// Connection-level failures (DNS, TCP, TLS)
|
|
338
|
-
//
|
|
339
|
-
// the
|
|
349
|
+
// Connection-level failures (DNS, TCP, TLS) OR body-read-level
|
|
350
|
+
// failures (abort, RST mid-stream). Treat as retryable up to the
|
|
351
|
+
// same cap as 503 — the server may be momentarily down or the
|
|
352
|
+
// network blip may resolve.
|
|
340
353
|
lastError =
|
|
341
354
|
networkErr instanceof Error ? networkErr : new Error(String(networkErr));
|
|
342
355
|
if (attempt < MAX_RETRY_ATTEMPTS - 1) {
|
|
@@ -345,7 +358,6 @@ class ComplianceApiClient {
|
|
|
345
358
|
}
|
|
346
359
|
throw new Error(`Failed to connect to ProdCycle API: ${lastError.message}`);
|
|
347
360
|
}
|
|
348
|
-
const responseText = await response.text();
|
|
349
361
|
let parsed = null;
|
|
350
362
|
try {
|
|
351
363
|
parsed = responseText ? JSON.parse(responseText) : null;
|
package/dist/utils/fs.js
CHANGED
|
@@ -38,6 +38,12 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const minimatch_1 = require("minimatch");
|
|
40
40
|
const MAX_FILE_SIZE = 256 * 1024; // 256 KB
|
|
41
|
+
// Reusable strict UTF-8 decoder. `TextDecoder` is stateless on the same
|
|
42
|
+
// instance (encoding + fatal are fixed at construction), so we allocate
|
|
43
|
+
// once at module load instead of once per file in the walk loop —
|
|
44
|
+
// repos with thousands of files would otherwise produce thousands of
|
|
45
|
+
// short-lived decoder objects per scan.
|
|
46
|
+
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
|
|
41
47
|
/**
|
|
42
48
|
* Total file ceiling per scan. Hit on the OSS-CLI benchmark scanning
|
|
43
49
|
* `hapifhir/hapi-fhir` (~13k files) — the CLI silently dropped ~3k files
|
|
@@ -397,18 +403,31 @@ function walk(dir, repoRoot, gitignores, prodcycleIgnores, includePatterns, user
|
|
|
397
403
|
}
|
|
398
404
|
if (isBinary(buffer))
|
|
399
405
|
continue;
|
|
400
|
-
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
406
|
+
// Strict UTF-8 decode: throw (and skip the file) on invalid byte
|
|
407
|
+
// sequences. Pre-fix this was `buffer.toString('utf8')`, which
|
|
408
|
+
// silently replaces invalid bytes with U+FFFD and includes the file
|
|
409
|
+
// anyway. Python's `open(encoding='utf-8')` raises UnicodeDecodeError
|
|
410
|
+
// on the same input and skips the file via its except clause, so
|
|
411
|
+
// Node ended up sending files Python wouldn't. Those files were
|
|
412
|
+
// overwhelmingly garbage (U+FFFD soup with no real content), but
|
|
413
|
+
// the inflated payload pushed many scans over the sync /validate
|
|
414
|
+
// limit and into the chunked-session fallback — Node ran 5–75x
|
|
415
|
+
// slower than Python on the same repos (dexidp-dex: npm=525s vs
|
|
416
|
+
// py=7s, frappe-erpnext: 1448s vs 42s, both 0 finding differences)
|
|
417
|
+
// during the 2026-05-12 GA-validation sweep. Catch the decode
|
|
418
|
+
// error and treat exactly like Python.
|
|
419
|
+
let content;
|
|
420
|
+
try {
|
|
421
|
+
content = UTF8_DECODER.decode(buffer);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
// Post-decode size filter mirrors the service's 256 KB per-file
|
|
427
|
+
// enforcement. Without invalid-UTF-8 inflation (now skipped above),
|
|
428
|
+
// post-decode byte length usually matches `stats.size`, but a BOM
|
|
429
|
+
// or rare normalization edge case can still differ — keep the
|
|
430
|
+
// re-measure as defense-in-depth.
|
|
412
431
|
if (Buffer.byteLength(content, 'utf8') > MAX_FILE_SIZE)
|
|
413
432
|
continue;
|
|
414
433
|
files[relPath] = content;
|
package/package.json
CHANGED