react-doctor 0.2.14-dev.7b4ddf7 → 0.2.14-dev.ac3ca1a
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 +11 -0
- package/dist/{cli-logger-CSZagq1E.js → cli-logger-Cqq0L4Uo.js} +161 -7
- package/dist/cli.js +10 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +158 -5
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -71,6 +71,17 @@ React Doctor scans the files changed in the pull request, emits inline annotatio
|
|
|
71
71
|
|
|
72
72
|
[Add GitHub Action →](https://github.com/marketplace/actions/react-doctor)
|
|
73
73
|
|
|
74
|
+
### 4. Configure rules in `react-doctor.config.json`
|
|
75
|
+
|
|
76
|
+
Point the `$schema` key at `https://react.doctor/schema/config.json` to get autocomplete, hover docs, and typo warnings for every option in any editor that understands JSON Schema.
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
{
|
|
80
|
+
"$schema": "https://react.doctor/schema/config.json",
|
|
81
|
+
"lint": true,
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
74
85
|
## Contributing
|
|
75
86
|
|
|
76
87
|
[Issues welcome!](https://github.com/millionco/react-doctor/issues)
|
|
@@ -3189,6 +3189,7 @@ const isTailwindAtLeast = (detected, required) => {
|
|
|
3189
3189
|
const JSX_FILE_PATTERN = /\.(tsx|jsx)$/;
|
|
3190
3190
|
const MILLISECONDS_PER_SECOND = 1e3;
|
|
3191
3191
|
const SCORE_API_URL = "https://www.react.doctor/api/score";
|
|
3192
|
+
const ENTERPRISE_CONTACT_URL = "https://react.doctor/enterprise";
|
|
3192
3193
|
const SHARE_BASE_URL = "https://www.react.doctor/share";
|
|
3193
3194
|
const PROMPTS_RULES_BASE_URL = "https://www.react.doctor/prompts/rules";
|
|
3194
3195
|
const FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -5909,6 +5910,149 @@ const appendReanimatedSharedValueHint = (help, rule, project) => {
|
|
|
5909
5910
|
if (!help) return REANIMATED_SHARED_VALUE_HINT;
|
|
5910
5911
|
return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
|
|
5911
5912
|
};
|
|
5913
|
+
const REDACTED_PLACEHOLDER = "<redacted>";
|
|
5914
|
+
const KEEP_PREFIX = `$1${REDACTED_PLACEHOLDER}`;
|
|
5915
|
+
const KNOWN_SECRET_RULES = [
|
|
5916
|
+
{
|
|
5917
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
5918
|
+
replacement: REDACTED_PLACEHOLDER
|
|
5919
|
+
},
|
|
5920
|
+
{
|
|
5921
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
5922
|
+
replacement: REDACTED_PLACEHOLDER
|
|
5923
|
+
},
|
|
5924
|
+
{
|
|
5925
|
+
pattern: /(?<=:\/\/)[^\s/:@]+:[^\s/@]+(?=@)/g,
|
|
5926
|
+
replacement: REDACTED_PLACEHOLDER
|
|
5927
|
+
},
|
|
5928
|
+
{
|
|
5929
|
+
pattern: /\b(AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA|A3T[A-Z0-9])[0-9A-Z]{16,}/g,
|
|
5930
|
+
replacement: KEEP_PREFIX
|
|
5931
|
+
},
|
|
5932
|
+
{
|
|
5933
|
+
pattern: /\b(gh[pousr]_)[A-Za-z0-9]{36,}/g,
|
|
5934
|
+
replacement: KEEP_PREFIX
|
|
5935
|
+
},
|
|
5936
|
+
{
|
|
5937
|
+
pattern: /\b(github_pat_)[A-Za-z0-9_]{22,}/g,
|
|
5938
|
+
replacement: KEEP_PREFIX
|
|
5939
|
+
},
|
|
5940
|
+
{
|
|
5941
|
+
pattern: /\b(glpat-)[A-Za-z0-9_-]{20,}/g,
|
|
5942
|
+
replacement: KEEP_PREFIX
|
|
5943
|
+
},
|
|
5944
|
+
{
|
|
5945
|
+
pattern: /\b(xox[baprs]-)[A-Za-z0-9-]{10,}/g,
|
|
5946
|
+
replacement: KEEP_PREFIX
|
|
5947
|
+
},
|
|
5948
|
+
{
|
|
5949
|
+
pattern: /(?<=hooks\.slack\.com\/services\/)[A-Za-z0-9/+_-]{20,}/g,
|
|
5950
|
+
replacement: REDACTED_PLACEHOLDER
|
|
5951
|
+
},
|
|
5952
|
+
{
|
|
5953
|
+
pattern: /\b((?:sk|rk)_(?:live|test)_)[0-9A-Za-z]{10,}/g,
|
|
5954
|
+
replacement: KEEP_PREFIX
|
|
5955
|
+
},
|
|
5956
|
+
{
|
|
5957
|
+
pattern: /\b(sk-(?:proj-|ant-)?)[A-Za-z0-9_-]{20,}/g,
|
|
5958
|
+
replacement: KEEP_PREFIX
|
|
5959
|
+
},
|
|
5960
|
+
{
|
|
5961
|
+
pattern: /\b(AIza)[0-9A-Za-z_-]{35,}/g,
|
|
5962
|
+
replacement: KEEP_PREFIX
|
|
5963
|
+
},
|
|
5964
|
+
{
|
|
5965
|
+
pattern: /\b(ya29\.)[0-9A-Za-z_-]{20,}/g,
|
|
5966
|
+
replacement: KEEP_PREFIX
|
|
5967
|
+
},
|
|
5968
|
+
{
|
|
5969
|
+
pattern: /\b(npm_)[A-Za-z0-9]{36,}/g,
|
|
5970
|
+
replacement: KEEP_PREFIX
|
|
5971
|
+
},
|
|
5972
|
+
{
|
|
5973
|
+
pattern: /\b(SG\.)[A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{43,}/g,
|
|
5974
|
+
replacement: KEEP_PREFIX
|
|
5975
|
+
},
|
|
5976
|
+
{
|
|
5977
|
+
pattern: /\b(SK)[0-9a-fA-F]{32,}/g,
|
|
5978
|
+
replacement: KEEP_PREFIX
|
|
5979
|
+
},
|
|
5980
|
+
{
|
|
5981
|
+
pattern: /\b(dop_v1_)[a-f0-9]{64,}/g,
|
|
5982
|
+
replacement: KEEP_PREFIX
|
|
5983
|
+
},
|
|
5984
|
+
{
|
|
5985
|
+
pattern: /\b(shp(?:at|ca|pa|ss)_)[a-fA-F0-9]{32,}/g,
|
|
5986
|
+
replacement: KEEP_PREFIX
|
|
5987
|
+
},
|
|
5988
|
+
{
|
|
5989
|
+
pattern: /\b(sq0[a-z]{3}-)[0-9A-Za-z_-]{22,}/g,
|
|
5990
|
+
replacement: KEEP_PREFIX
|
|
5991
|
+
},
|
|
5992
|
+
{
|
|
5993
|
+
pattern: /\b([0-9]{8,10}:AA)[0-9A-Za-z_-]{32,}/g,
|
|
5994
|
+
replacement: KEEP_PREFIX
|
|
5995
|
+
},
|
|
5996
|
+
{
|
|
5997
|
+
pattern: /(?<=\bBearer\s)[A-Za-z0-9._~+/=-]{16,}/g,
|
|
5998
|
+
replacement: REDACTED_PLACEHOLDER
|
|
5999
|
+
},
|
|
6000
|
+
{
|
|
6001
|
+
pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
6002
|
+
replacement: REDACTED_PLACEHOLDER
|
|
6003
|
+
}
|
|
6004
|
+
];
|
|
6005
|
+
const CANDIDATE_TOKEN_PATTERN = /[A-Za-z0-9_][A-Za-z0-9_-]*/g;
|
|
6006
|
+
const GIT_OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
6007
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6008
|
+
const HAS_LETTER_PATTERN = /[A-Za-z]/;
|
|
6009
|
+
const HAS_DIGIT_PATTERN = /[0-9]/;
|
|
6010
|
+
const shannonEntropyBits = (value) => {
|
|
6011
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6012
|
+
for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
6013
|
+
let bits = 0;
|
|
6014
|
+
for (const count of counts.values()) {
|
|
6015
|
+
const probability = count / value.length;
|
|
6016
|
+
bits -= probability * Math.log2(probability);
|
|
6017
|
+
}
|
|
6018
|
+
return bits;
|
|
6019
|
+
};
|
|
6020
|
+
const looksLikeHighEntropySecret = (token) => {
|
|
6021
|
+
if (token.length < 32) return false;
|
|
6022
|
+
if (!HAS_LETTER_PATTERN.test(token) || !HAS_DIGIT_PATTERN.test(token)) return false;
|
|
6023
|
+
if (GIT_OBJECT_ID_PATTERN.test(token) || UUID_PATTERN.test(token)) return false;
|
|
6024
|
+
return shannonEntropyBits(token) >= 3;
|
|
6025
|
+
};
|
|
6026
|
+
const redactHighEntropyTokens = (text) => text.replace(CANDIDATE_TOKEN_PATTERN, (token) => looksLikeHighEntropySecret(token) ? REDACTED_PLACEHOLDER : token);
|
|
6027
|
+
/**
|
|
6028
|
+
* Masks API keys, tokens, private keys, credentialed URLs, and emails
|
|
6029
|
+
* found anywhere inside a free-text string, returning the scrubbed text.
|
|
6030
|
+
* Applied to every diagnostic's `message` / `help` at construction time
|
|
6031
|
+
* so secrets never reach the terminal, the JSON report, or the score
|
|
6032
|
+
* API — react-doctor must never echo or transmit a user's secrets.
|
|
6033
|
+
*
|
|
6034
|
+
* Provider tokens keep their non-secret, type-identifying prefix (e.g.
|
|
6035
|
+
* `sk_live_<redacted>`, `ghp_<redacted>`, `AKIA<redacted>`) so the leaked
|
|
6036
|
+
* credential's type stays visible; structural or unknown-format secrets
|
|
6037
|
+
* with no meaningful prefix are masked whole.
|
|
6038
|
+
*
|
|
6039
|
+
* Runs the high-precision known-shape detectors first, then a generic
|
|
6040
|
+
* entropy-gated sweep for unknown-format secrets. Idempotent: the inert
|
|
6041
|
+
* `<redacted>` placeholder matches none of the detectors and is too
|
|
6042
|
+
* short for the generic sweep, so re-running leaves the text unchanged.
|
|
6043
|
+
*
|
|
6044
|
+
* Accepts `unknown` on purpose: callers feed it diagnostic `message` /
|
|
6045
|
+
* `help` that originate from oxlint JSON, which is only shape-checked at
|
|
6046
|
+
* the top level (the per-field `string` types are assumed, not validated).
|
|
6047
|
+
* A malformed non-string value returns `""` instead of throwing on
|
|
6048
|
+
* `.replace`, so one bad diagnostic can't abort parsing the whole batch.
|
|
6049
|
+
*/
|
|
6050
|
+
const redactSensitiveText = (text) => {
|
|
6051
|
+
if (typeof text !== "string" || text === "") return "";
|
|
6052
|
+
let redacted = text;
|
|
6053
|
+
for (const rule of KNOWN_SECRET_RULES) redacted = redacted.replace(rule.pattern, rule.replacement);
|
|
6054
|
+
return redactHighEntropyTokens(redacted);
|
|
6055
|
+
};
|
|
5912
6056
|
const REACT_MODULE_SOURCE = "react";
|
|
5913
6057
|
const REQUIRE_IDENTIFIER = "require";
|
|
5914
6058
|
const USE_IDENTIFIER = "use";
|
|
@@ -6230,6 +6374,13 @@ const getRuleRecommendation = (ruleName, project) => {
|
|
|
6230
6374
|
};
|
|
6231
6375
|
const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
|
|
6232
6376
|
const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
|
|
6377
|
+
const cleaned = resolveCleanedDiagnostic(typeof message === "string" ? message : "", typeof help === "string" ? help : "", plugin, rule, project);
|
|
6378
|
+
return {
|
|
6379
|
+
message: redactSensitiveText(cleaned.message),
|
|
6380
|
+
help: redactSensitiveText(cleaned.help)
|
|
6381
|
+
};
|
|
6382
|
+
};
|
|
6383
|
+
const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
|
|
6233
6384
|
if (plugin === "react-hooks-js") return {
|
|
6234
6385
|
message: REACT_COMPILER_MESSAGE,
|
|
6235
6386
|
help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
|
|
@@ -6434,11 +6585,14 @@ const spawnLintBatches = async (input) => {
|
|
|
6434
6585
|
onFileProgress(scannedFileCount + batchFileIndex, totalFileCount);
|
|
6435
6586
|
}
|
|
6436
6587
|
}, 50) : null;
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6588
|
+
try {
|
|
6589
|
+
const batchDiagnostics = await spawnLintBatch(batch);
|
|
6590
|
+
allDiagnostics.push(...batchDiagnostics);
|
|
6591
|
+
scannedFileCount += batch.length;
|
|
6592
|
+
onFileProgress?.(scannedFileCount, totalFileCount);
|
|
6593
|
+
} finally {
|
|
6594
|
+
if (progressInterval !== null) clearInterval(progressInterval);
|
|
6595
|
+
}
|
|
6442
6596
|
}
|
|
6443
6597
|
if (droppedFiles.length > 0 && onPartialFailure) {
|
|
6444
6598
|
const previewFiles = droppedFiles.slice(0, 3).join(", ");
|
|
@@ -7559,6 +7713,6 @@ const cliLogger = {
|
|
|
7559
7713
|
}
|
|
7560
7714
|
};
|
|
7561
7715
|
//#endregion
|
|
7562
|
-
export {
|
|
7716
|
+
export { highlighter as A, discoverReactSubprojects as C, formatReactDoctorError as D, formatErrorChain as E, resolveScanTarget as F, restoreLegacyThrow as I, runInspect as L, isReactDoctorError as M, layerOtlp as N, getDiffInfo as O, listWorkspacePackages as P, toRelativePath as R, buildRulePromptUrl as S, filterSourceFiles as T, SKILL_NAME as _, DeadCode as a, buildJsonReport as b, Git as c, NodeResolver as d, OXLINT_NODE_REQUIREMENT as f, SHARE_BASE_URL as g, Reporter as h, Config as i, isMonorepoRoot as j, groupBy as k, LintPartialFailures as l, Project as m, cli_logger_exports as n, ENTERPRISE_CONTACT_URL as o, Progress as p, CANONICAL_GITHUB_URL as r, Files as s, cliLogger as t, Linter as u, Score as v, filterDiagnosticsForSurface as w, buildJsonReportError as x, StagedFiles as y };
|
|
7563
7717
|
|
|
7564
|
-
//# sourceMappingURL=cli-logger-
|
|
7718
|
+
//# sourceMappingURL=cli-logger-Cqq0L4Uo.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./rolldown-runtime-uZX_iqCz.js";
|
|
2
|
-
import { A as
|
|
2
|
+
import { A as highlighter, C as discoverReactSubprojects, D as formatReactDoctorError, E as formatErrorChain, F as resolveScanTarget, I as restoreLegacyThrow, L as runInspect, M as isReactDoctorError, N as layerOtlp, O as getDiffInfo, P as listWorkspacePackages, R as toRelativePath, S as buildRulePromptUrl, T as filterSourceFiles, _ as SKILL_NAME, a as DeadCode, b as buildJsonReport, c as Git, d as NodeResolver, f as OXLINT_NODE_REQUIREMENT, g as SHARE_BASE_URL, h as Reporter, i as Config, j as isMonorepoRoot, k as groupBy, l as LintPartialFailures, m as Project, o as ENTERPRISE_CONTACT_URL, p as Progress, r as CANONICAL_GITHUB_URL, s as Files, t as cliLogger, u as Linter, v as Score, w as filterDiagnosticsForSurface, x as buildJsonReportError, y as StagedFiles } from "./cli-logger-Cqq0L4Uo.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { execFileSync, execSync } from "node:child_process";
|
|
5
5
|
import path, { join } from "node:path";
|
|
@@ -6161,6 +6161,12 @@ const makeNoopConsole = () => ({
|
|
|
6161
6161
|
warn: () => {}
|
|
6162
6162
|
});
|
|
6163
6163
|
//#endregion
|
|
6164
|
+
//#region src/cli/utils/build-no-score-message.ts
|
|
6165
|
+
const ENTERPRISE_CONTACT_HINT = `Want something custom to your company? Contact us at ${ENTERPRISE_CONTACT_URL}.`;
|
|
6166
|
+
const buildNoScoreMessage = (isScoreDisabled) => {
|
|
6167
|
+
return `${isScoreDisabled ? "Score disabled by --no-score." : "Score unavailable (could not reach the score API)."} ${ENTERPRISE_CONTACT_HINT}`;
|
|
6168
|
+
};
|
|
6169
|
+
//#endregion
|
|
6164
6170
|
//#region src/cli/utils/render-agent-guidance.ts
|
|
6165
6171
|
const AGENT_GUIDANCE_LINES = [
|
|
6166
6172
|
"Treat React Doctor diagnostics as starting hypotheses. Read the relevant code before confirming or suppressing each finding.",
|
|
@@ -6680,7 +6686,7 @@ const resolveOxlintNodeEffect = (isLintEnabled, isQuiet) => Effect.gen(function*
|
|
|
6680
6686
|
const resolveOxlintNode = (isLintEnabled, isQuiet) => Effect.runPromise(resolveOxlintNodeEffect(isLintEnabled, isQuiet).pipe(Effect.provide(NodeResolver.layerNode)));
|
|
6681
6687
|
//#endregion
|
|
6682
6688
|
//#region src/cli/utils/version.ts
|
|
6683
|
-
const VERSION = "0.2.14-dev.
|
|
6689
|
+
const VERSION = "0.2.14-dev.ac3ca1a";
|
|
6684
6690
|
//#endregion
|
|
6685
6691
|
//#region src/inspect.ts
|
|
6686
6692
|
const silentConsole = makeNoopConsole();
|
|
@@ -6804,7 +6810,7 @@ const finalizeAndRender = (input) => Effect.gen(function* () {
|
|
|
6804
6810
|
if (didLintFail) skippedChecks.push("lint");
|
|
6805
6811
|
if (didDeadCodeFail) skippedChecks.push("dead-code");
|
|
6806
6812
|
const hasSkippedChecks = skippedChecks.length > 0;
|
|
6807
|
-
const noScoreMessage = options.noScore
|
|
6813
|
+
const noScoreMessage = buildNoScoreMessage(options.noScore);
|
|
6808
6814
|
const skippedCheckReasons = {};
|
|
6809
6815
|
if (didLintFail && lintFailureReason !== null) skippedCheckReasons.lint = lintFailureReason;
|
|
6810
6816
|
else if (lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = lintPartialFailures.join("; ");
|
|
@@ -7501,7 +7507,7 @@ const warnSetupPromptFailure = async (options, error) => {
|
|
|
7501
7507
|
return;
|
|
7502
7508
|
}
|
|
7503
7509
|
try {
|
|
7504
|
-
const { cliLogger } = await import("./cli-logger-
|
|
7510
|
+
const { cliLogger } = await import("./cli-logger-Cqq0L4Uo.js").then((n) => n.n);
|
|
7505
7511
|
cliLogger.warn(message);
|
|
7506
7512
|
} catch {}
|
|
7507
7513
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -20,9 +20,9 @@ interface ReactDoctorIgnoreConfig {
|
|
|
20
20
|
* locally but excluded from PR comments, the score, or the CI gate:
|
|
21
21
|
*
|
|
22
22
|
* - `cli` — local terminal output from `react-doctor` (`printDiagnostics`).
|
|
23
|
-
* - `prComment` —
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* - `prComment` — diagnostics destined for a sticky pull-request
|
|
24
|
+
* summary comment. Selected by running the CLI with `--pr-comment`
|
|
25
|
+
* (sets `outputSurface: "prComment"`).
|
|
26
26
|
* - `score` — diagnostics shipped to the React Doctor score API
|
|
27
27
|
* (or counted toward local score calculations).
|
|
28
28
|
* - `ciFailure` — diagnostics that count toward the `--fail-on` exit
|
package/dist/index.js
CHANGED
|
@@ -5939,6 +5939,149 @@ const appendReanimatedSharedValueHint = (help, rule, project) => {
|
|
|
5939
5939
|
if (!help) return REANIMATED_SHARED_VALUE_HINT;
|
|
5940
5940
|
return `${help}\n\n${REANIMATED_SHARED_VALUE_HINT}`;
|
|
5941
5941
|
};
|
|
5942
|
+
const REDACTED_PLACEHOLDER = "<redacted>";
|
|
5943
|
+
const KEEP_PREFIX = `$1${REDACTED_PLACEHOLDER}`;
|
|
5944
|
+
const KNOWN_SECRET_RULES = [
|
|
5945
|
+
{
|
|
5946
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
5947
|
+
replacement: REDACTED_PLACEHOLDER
|
|
5948
|
+
},
|
|
5949
|
+
{
|
|
5950
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
5951
|
+
replacement: REDACTED_PLACEHOLDER
|
|
5952
|
+
},
|
|
5953
|
+
{
|
|
5954
|
+
pattern: /(?<=:\/\/)[^\s/:@]+:[^\s/@]+(?=@)/g,
|
|
5955
|
+
replacement: REDACTED_PLACEHOLDER
|
|
5956
|
+
},
|
|
5957
|
+
{
|
|
5958
|
+
pattern: /\b(AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA|A3T[A-Z0-9])[0-9A-Z]{16,}/g,
|
|
5959
|
+
replacement: KEEP_PREFIX
|
|
5960
|
+
},
|
|
5961
|
+
{
|
|
5962
|
+
pattern: /\b(gh[pousr]_)[A-Za-z0-9]{36,}/g,
|
|
5963
|
+
replacement: KEEP_PREFIX
|
|
5964
|
+
},
|
|
5965
|
+
{
|
|
5966
|
+
pattern: /\b(github_pat_)[A-Za-z0-9_]{22,}/g,
|
|
5967
|
+
replacement: KEEP_PREFIX
|
|
5968
|
+
},
|
|
5969
|
+
{
|
|
5970
|
+
pattern: /\b(glpat-)[A-Za-z0-9_-]{20,}/g,
|
|
5971
|
+
replacement: KEEP_PREFIX
|
|
5972
|
+
},
|
|
5973
|
+
{
|
|
5974
|
+
pattern: /\b(xox[baprs]-)[A-Za-z0-9-]{10,}/g,
|
|
5975
|
+
replacement: KEEP_PREFIX
|
|
5976
|
+
},
|
|
5977
|
+
{
|
|
5978
|
+
pattern: /(?<=hooks\.slack\.com\/services\/)[A-Za-z0-9/+_-]{20,}/g,
|
|
5979
|
+
replacement: REDACTED_PLACEHOLDER
|
|
5980
|
+
},
|
|
5981
|
+
{
|
|
5982
|
+
pattern: /\b((?:sk|rk)_(?:live|test)_)[0-9A-Za-z]{10,}/g,
|
|
5983
|
+
replacement: KEEP_PREFIX
|
|
5984
|
+
},
|
|
5985
|
+
{
|
|
5986
|
+
pattern: /\b(sk-(?:proj-|ant-)?)[A-Za-z0-9_-]{20,}/g,
|
|
5987
|
+
replacement: KEEP_PREFIX
|
|
5988
|
+
},
|
|
5989
|
+
{
|
|
5990
|
+
pattern: /\b(AIza)[0-9A-Za-z_-]{35,}/g,
|
|
5991
|
+
replacement: KEEP_PREFIX
|
|
5992
|
+
},
|
|
5993
|
+
{
|
|
5994
|
+
pattern: /\b(ya29\.)[0-9A-Za-z_-]{20,}/g,
|
|
5995
|
+
replacement: KEEP_PREFIX
|
|
5996
|
+
},
|
|
5997
|
+
{
|
|
5998
|
+
pattern: /\b(npm_)[A-Za-z0-9]{36,}/g,
|
|
5999
|
+
replacement: KEEP_PREFIX
|
|
6000
|
+
},
|
|
6001
|
+
{
|
|
6002
|
+
pattern: /\b(SG\.)[A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{43,}/g,
|
|
6003
|
+
replacement: KEEP_PREFIX
|
|
6004
|
+
},
|
|
6005
|
+
{
|
|
6006
|
+
pattern: /\b(SK)[0-9a-fA-F]{32,}/g,
|
|
6007
|
+
replacement: KEEP_PREFIX
|
|
6008
|
+
},
|
|
6009
|
+
{
|
|
6010
|
+
pattern: /\b(dop_v1_)[a-f0-9]{64,}/g,
|
|
6011
|
+
replacement: KEEP_PREFIX
|
|
6012
|
+
},
|
|
6013
|
+
{
|
|
6014
|
+
pattern: /\b(shp(?:at|ca|pa|ss)_)[a-fA-F0-9]{32,}/g,
|
|
6015
|
+
replacement: KEEP_PREFIX
|
|
6016
|
+
},
|
|
6017
|
+
{
|
|
6018
|
+
pattern: /\b(sq0[a-z]{3}-)[0-9A-Za-z_-]{22,}/g,
|
|
6019
|
+
replacement: KEEP_PREFIX
|
|
6020
|
+
},
|
|
6021
|
+
{
|
|
6022
|
+
pattern: /\b([0-9]{8,10}:AA)[0-9A-Za-z_-]{32,}/g,
|
|
6023
|
+
replacement: KEEP_PREFIX
|
|
6024
|
+
},
|
|
6025
|
+
{
|
|
6026
|
+
pattern: /(?<=\bBearer\s)[A-Za-z0-9._~+/=-]{16,}/g,
|
|
6027
|
+
replacement: REDACTED_PLACEHOLDER
|
|
6028
|
+
},
|
|
6029
|
+
{
|
|
6030
|
+
pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
6031
|
+
replacement: REDACTED_PLACEHOLDER
|
|
6032
|
+
}
|
|
6033
|
+
];
|
|
6034
|
+
const CANDIDATE_TOKEN_PATTERN = /[A-Za-z0-9_][A-Za-z0-9_-]*/g;
|
|
6035
|
+
const GIT_OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
6036
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6037
|
+
const HAS_LETTER_PATTERN = /[A-Za-z]/;
|
|
6038
|
+
const HAS_DIGIT_PATTERN = /[0-9]/;
|
|
6039
|
+
const shannonEntropyBits = (value) => {
|
|
6040
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6041
|
+
for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
6042
|
+
let bits = 0;
|
|
6043
|
+
for (const count of counts.values()) {
|
|
6044
|
+
const probability = count / value.length;
|
|
6045
|
+
bits -= probability * Math.log2(probability);
|
|
6046
|
+
}
|
|
6047
|
+
return bits;
|
|
6048
|
+
};
|
|
6049
|
+
const looksLikeHighEntropySecret = (token) => {
|
|
6050
|
+
if (token.length < 32) return false;
|
|
6051
|
+
if (!HAS_LETTER_PATTERN.test(token) || !HAS_DIGIT_PATTERN.test(token)) return false;
|
|
6052
|
+
if (GIT_OBJECT_ID_PATTERN.test(token) || UUID_PATTERN.test(token)) return false;
|
|
6053
|
+
return shannonEntropyBits(token) >= 3;
|
|
6054
|
+
};
|
|
6055
|
+
const redactHighEntropyTokens = (text) => text.replace(CANDIDATE_TOKEN_PATTERN, (token) => looksLikeHighEntropySecret(token) ? REDACTED_PLACEHOLDER : token);
|
|
6056
|
+
/**
|
|
6057
|
+
* Masks API keys, tokens, private keys, credentialed URLs, and emails
|
|
6058
|
+
* found anywhere inside a free-text string, returning the scrubbed text.
|
|
6059
|
+
* Applied to every diagnostic's `message` / `help` at construction time
|
|
6060
|
+
* so secrets never reach the terminal, the JSON report, or the score
|
|
6061
|
+
* API — react-doctor must never echo or transmit a user's secrets.
|
|
6062
|
+
*
|
|
6063
|
+
* Provider tokens keep their non-secret, type-identifying prefix (e.g.
|
|
6064
|
+
* `sk_live_<redacted>`, `ghp_<redacted>`, `AKIA<redacted>`) so the leaked
|
|
6065
|
+
* credential's type stays visible; structural or unknown-format secrets
|
|
6066
|
+
* with no meaningful prefix are masked whole.
|
|
6067
|
+
*
|
|
6068
|
+
* Runs the high-precision known-shape detectors first, then a generic
|
|
6069
|
+
* entropy-gated sweep for unknown-format secrets. Idempotent: the inert
|
|
6070
|
+
* `<redacted>` placeholder matches none of the detectors and is too
|
|
6071
|
+
* short for the generic sweep, so re-running leaves the text unchanged.
|
|
6072
|
+
*
|
|
6073
|
+
* Accepts `unknown` on purpose: callers feed it diagnostic `message` /
|
|
6074
|
+
* `help` that originate from oxlint JSON, which is only shape-checked at
|
|
6075
|
+
* the top level (the per-field `string` types are assumed, not validated).
|
|
6076
|
+
* A malformed non-string value returns `""` instead of throwing on
|
|
6077
|
+
* `.replace`, so one bad diagnostic can't abort parsing the whole batch.
|
|
6078
|
+
*/
|
|
6079
|
+
const redactSensitiveText = (text) => {
|
|
6080
|
+
if (typeof text !== "string" || text === "") return "";
|
|
6081
|
+
let redacted = text;
|
|
6082
|
+
for (const rule of KNOWN_SECRET_RULES) redacted = redacted.replace(rule.pattern, rule.replacement);
|
|
6083
|
+
return redactHighEntropyTokens(redacted);
|
|
6084
|
+
};
|
|
5942
6085
|
const REACT_MODULE_SOURCE = "react";
|
|
5943
6086
|
const REQUIRE_IDENTIFIER = "require";
|
|
5944
6087
|
const USE_IDENTIFIER = "use";
|
|
@@ -6260,6 +6403,13 @@ const getRuleRecommendation = (ruleName, project) => {
|
|
|
6260
6403
|
};
|
|
6261
6404
|
const getRuleCategory = (ruleName) => reactDoctorPlugin.rules[ruleName]?.category;
|
|
6262
6405
|
const cleanDiagnosticMessage = (message, help, plugin, rule, project) => {
|
|
6406
|
+
const cleaned = resolveCleanedDiagnostic(typeof message === "string" ? message : "", typeof help === "string" ? help : "", plugin, rule, project);
|
|
6407
|
+
return {
|
|
6408
|
+
message: redactSensitiveText(cleaned.message),
|
|
6409
|
+
help: redactSensitiveText(cleaned.help)
|
|
6410
|
+
};
|
|
6411
|
+
};
|
|
6412
|
+
const resolveCleanedDiagnostic = (message, help, plugin, rule, project) => {
|
|
6263
6413
|
if (plugin === "react-hooks-js") return {
|
|
6264
6414
|
message: REACT_COMPILER_MESSAGE,
|
|
6265
6415
|
help: appendReanimatedSharedValueHint(message.replace(FILEPATH_WITH_LOCATION_PATTERN, "").trim() || help, rule, project)
|
|
@@ -6464,11 +6614,14 @@ const spawnLintBatches = async (input) => {
|
|
|
6464
6614
|
onFileProgress(scannedFileCount + batchFileIndex, totalFileCount);
|
|
6465
6615
|
}
|
|
6466
6616
|
}, 50) : null;
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6617
|
+
try {
|
|
6618
|
+
const batchDiagnostics = await spawnLintBatch(batch);
|
|
6619
|
+
allDiagnostics.push(...batchDiagnostics);
|
|
6620
|
+
scannedFileCount += batch.length;
|
|
6621
|
+
onFileProgress?.(scannedFileCount, totalFileCount);
|
|
6622
|
+
} finally {
|
|
6623
|
+
if (progressInterval !== null) clearInterval(progressInterval);
|
|
6624
|
+
}
|
|
6472
6625
|
}
|
|
6473
6626
|
if (droppedFiles.length > 0 && onPartialFailure) {
|
|
6474
6627
|
const previewFiles = droppedFiles.slice(0, 3).join(", ");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-doctor",
|
|
3
|
-
"version": "0.2.14-dev.
|
|
3
|
+
"version": "0.2.14-dev.ac3ca1a",
|
|
4
4
|
"description": "Diagnose and fix React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -52,20 +52,20 @@
|
|
|
52
52
|
"@effect/platform-node-shared": "4.0.0-beta.70",
|
|
53
53
|
"agent-install": "0.0.5",
|
|
54
54
|
"conf": "^15.1.0",
|
|
55
|
-
"deslop-js": "^0.0.
|
|
55
|
+
"deslop-js": "^0.0.14",
|
|
56
56
|
"effect": "4.0.0-beta.70",
|
|
57
57
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
58
58
|
"oxlint": "^1.66.0",
|
|
59
59
|
"prompts": "^2.4.2",
|
|
60
60
|
"typescript": ">=5.0.4 <7",
|
|
61
|
-
"oxlint-plugin-react-doctor": "0.2.14-dev.
|
|
61
|
+
"oxlint-plugin-react-doctor": "0.2.14-dev.ac3ca1a"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@types/prompts": "^2.4.9",
|
|
65
65
|
"commander": "^14.0.3",
|
|
66
66
|
"ora": "^9.4.0",
|
|
67
|
-
"@react-doctor/
|
|
68
|
-
"@react-doctor/
|
|
67
|
+
"@react-doctor/core": "0.2.14",
|
|
68
|
+
"@react-doctor/api": "0.2.14"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": "^20.19.0 || >=22.12.0"
|