react-doctor 0.2.14-dev.9b64d98 → 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.
@@ -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
- const batchDiagnostics = await spawnLintBatch(batch);
6438
- if (progressInterval !== null) clearInterval(progressInterval);
6439
- allDiagnostics.push(...batchDiagnostics);
6440
- scannedFileCount += batch.length;
6441
- onFileProgress?.(scannedFileCount, totalFileCount);
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 { isMonorepoRoot as A, filterDiagnosticsForSurface as C, getDiffInfo as D, formatReactDoctorError as E, restoreLegacyThrow as F, runInspect as I, toRelativePath as L, layerOtlp as M, listWorkspacePackages as N, groupBy as O, resolveScanTarget as P, discoverReactSubprojects as S, formatErrorChain as T, Score as _, DeadCode as a, buildJsonReportError as b, LintPartialFailures as c, OXLINT_NODE_REQUIREMENT as d, Progress as f, SKILL_NAME as g, SHARE_BASE_URL as h, Config as i, isReactDoctorError as j, highlighter as k, Linter as l, Reporter as m, cli_logger_exports as n, Files as o, Project as p, CANONICAL_GITHUB_URL as r, Git as s, cliLogger as t, NodeResolver as u, StagedFiles as v, filterSourceFiles as w, buildRulePromptUrl as x, buildJsonReport as y };
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-CSZagq1E.js.map
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 isMonorepoRoot, C as filterDiagnosticsForSurface, D as getDiffInfo, E as formatReactDoctorError, F as restoreLegacyThrow, I as runInspect, L as toRelativePath, M as layerOtlp, N as listWorkspacePackages, O as groupBy, P as resolveScanTarget, S as discoverReactSubprojects, T as formatErrorChain, _ as Score, a as DeadCode, b as buildJsonReportError, c as LintPartialFailures, d as OXLINT_NODE_REQUIREMENT, f as Progress, g as SKILL_NAME, h as SHARE_BASE_URL, i as Config, j as isReactDoctorError, k as highlighter, l as Linter, m as Reporter, o as Files, p as Project, r as CANONICAL_GITHUB_URL, s as Git, t as cliLogger, u as NodeResolver, v as StagedFiles, w as filterSourceFiles, x as buildRulePromptUrl, y as buildJsonReport } from "./cli-logger-CSZagq1E.js";
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.9b64d98";
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 ? "Score disabled by --no-score." : "Score unavailable (could not reach the score API).";
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-CSZagq1E.js").then((n) => n.n);
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` — output captured by the GitHub Action for the sticky
24
- * PR comment. Enabled when the CLI is run with `--pr-comment` (the
25
- * action sets this automatically when `github-token` is provided).
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
- const batchDiagnostics = await spawnLintBatch(batch);
6468
- if (progressInterval !== null) clearInterval(progressInterval);
6469
- allDiagnostics.push(...batchDiagnostics);
6470
- scannedFileCount += batch.length;
6471
- onFileProgress?.(scannedFileCount, totalFileCount);
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.9b64d98",
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,13 +52,13 @@
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.13",
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.9b64d98"
61
+ "oxlint-plugin-react-doctor": "0.2.14-dev.ac3ca1a"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/prompts": "^2.4.9",