@sarj/eslint-plugin 19.9.0 → 19.10.0

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/catalog.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  publicDocumentation,
3
3
  ruleSpec,
4
4
  src_default
5
- } from "./chunk-MFL7OATV.js";
5
+ } from "./chunk-ZWOGM4CV.js";
6
6
 
7
7
  // src/docs/directive-disable-enable-pair.ts
8
8
  var DIRECTIVE_DISABLE_ENABLE_PAIR_DOCUMENTATION = {
@@ -2519,13 +2519,47 @@ var NO_PRODUCTION_BROWSER_SOURCE_MAPS_DOCUMENTATION = {
2519
2519
  // src/docs/no-raw-env.ts
2520
2520
  var NO_RAW_ENV_DOCUMENTATION = {
2521
2521
  summary: "Disallow direct `process.env` and `import.meta.env` reads outside validated boundaries.",
2522
- rationale: "Raw environment reads are untyped and defer invalid configuration failures until use.",
2523
- remediation: "Validate environment values at startup and import the typed configuration object.",
2522
+ rationale: "Raw environment reads are untyped and defer invalid configuration failures until use. One shared validated settings object gives consumers a consistent startup configuration.",
2523
+ remediation: "Define a Zod settings schema, call SettingsSchema.parse(process.env) once at startup, and export or inject that single parsed settings object. Use Pydantic/Pydantic Settings for the equivalent Python boundary. Required values must fail before remote effects; preserve independent security policy and existing optional defaults.",
2524
2524
  category: "correctness",
2525
- limitations: ["Host markers, assignment targets, tests, scripts, build config, and recognized validation-boundary files are policy exemptions. A validation call does not prove every export is validated; assignment-target exemptions also include compound writes that read the previous value."],
2525
+ limitations: [
2526
+ "Host markers, assignment targets, tests, ordinary scripts and build config retain their policy exemptions. Boundary-named modules, including env.mjs and settings.ts, are checked. A boundary filename or schema declaration alone does not exempt raw reads: each read must flow through a parse/safeParse/createEnv argument, directly or through stable nonexported const aliases whose every use reaches validation. Only unshadowed Number/String/Boolean/BigInt calls are traversed as scalar coercions; arbitrary calls cannot consume raw settings before the parser.",
2527
+ "This is bounded same-file syntax analysis (24 flow steps, 128 reads per alias and 512 total expression visits per raw read); flows exceeding these budgets are unreported. It does not prove a parser's implementation, cross-file schemas, schema completeness, safeParse failure handling or a project-wide singleton. Handle safeParse failure before exposing result.data; prefer throwing parse at startup. Nested component settings and separate server/client runtime boundaries remain valid.",
2528
+ "Assignment-target exemptions also include compound writes that read the previous value. Whole-environment spreads outside boundary modules retain the existing forwarding exemption; a spread inside a boundary must also reach its parser. No automatic rewrite."
2529
+ ],
2526
2530
  examples: [
2527
- { id: "validated-environment", title: "Read validated configuration", outcome: "no-match", files: [{ path: "src/database.ts", source: "import { env } from './env.js'; const url = env.DATABASE_URL;" }], focusPath: "src/database.ts", expectedCount: 0, public: true },
2528
- { id: "raw-environment-read", title: "Do not read raw configuration", outcome: "match", files: [{ path: "src/database.ts", source: "const url = process.env.DATABASE_URL;" }], focusPath: "src/database.ts", expectedCount: 1, public: true }
2531
+ {
2532
+ id: "validated-environment",
2533
+ title: "Parse once and reuse one settings object",
2534
+ outcome: "no-match",
2535
+ files: [
2536
+ {
2537
+ path: "src/database.ts",
2538
+ source: "import { settings } from './env.js';\nexport const primaryUrl = settings.DATABASE_URL;\nexport const replicaUrl = settings.DATABASE_URL;"
2539
+ },
2540
+ {
2541
+ path: "src/env.ts",
2542
+ source: "import { z } from 'zod';\nconst SettingsSchema = z.object({ DATABASE_URL: z.url() });\nexport const settings = SettingsSchema.parse(process.env);"
2543
+ }
2544
+ ],
2545
+ focusPath: "src/env.ts",
2546
+ expectedCount: 0,
2547
+ public: true
2548
+ },
2549
+ {
2550
+ id: "raw-environment-read",
2551
+ title: "Declaring a schema does not validate a raw export",
2552
+ outcome: "match",
2553
+ files: [
2554
+ {
2555
+ path: "src/env.ts",
2556
+ source: "import { z } from 'zod';\nconst SettingsSchema = z.object({ DATABASE_URL: z.url() });\nexport const settings = { DATABASE_URL: process.env.DATABASE_URL };"
2557
+ }
2558
+ ],
2559
+ focusPath: "src/env.ts",
2560
+ expectedCount: 1,
2561
+ public: true
2562
+ }
2529
2563
  ]
2530
2564
  };
2531
2565
 
@@ -4444,7 +4478,7 @@ var PREFER_CODED_ERROR_RESULT_DOCUMENTATION = {
4444
4478
  var PREFER_CONFIGURED_CLOUD_SCOPE_DOCUMENTATION = {
4445
4479
  summary: "Prefer validated deployment configuration for account scope in outgoing cloud management requests.",
4446
4480
  rationale: "A source-literal cloud account scope pins operational requests to one deployment even when the credential or environment changes.",
4447
- remediation: "Read the account ID from environment variables or injected configuration, validate it at startup, and pass the validated value to the request. Preserve independent security allowlists and fail closed when required configuration is missing.",
4481
+ remediation: "Read the account ID through one shared settings object validated by a Zod schema at startup, then import or inject that parsed object into consumers. Required values must fail before remote effects. Preserve independent security allowlists; an environment read alone is not validation.",
4448
4482
  category: "correctness",
4449
4483
  defaultLevel: "warning",
4450
4484
  autofix: "none",
@@ -4465,22 +4499,30 @@ var PREFER_CONFIGURED_CLOUD_SCOPE_DOCUMENTATION = {
4465
4499
  id: "literal-account-scope",
4466
4500
  title: "Move outgoing account scope into deployment configuration",
4467
4501
  outcome: "match",
4468
- files: [{
4469
- path: ".github/scripts/publish.mjs",
4470
- source: "const accountId = '0123456789abcdef0123456789abcdef';\nconst base = `https://api.cloudflare.com/client/v4/accounts/${accountId}`;\nexport async function publish(path, request = fetch) { return request(`${base}/${path}`); }"
4471
- }],
4502
+ files: [
4503
+ {
4504
+ path: ".github/scripts/publish.mjs",
4505
+ source: "const accountId = '0123456789abcdef0123456789abcdef';\nconst base = `https://api.cloudflare.com/client/v4/accounts/${accountId}`;\nexport async function publish(path, request = fetch) { return request(`${base}/${path}`); }"
4506
+ }
4507
+ ],
4472
4508
  focusPath: ".github/scripts/publish.mjs",
4473
4509
  expectedCount: 1,
4474
4510
  public: true
4475
4511
  },
4476
4512
  {
4477
4513
  id: "validated-account-scope",
4478
- title: "Pass validated deployment configuration to the request",
4514
+ title: "Parse one shared settings object with Zod before requests",
4479
4515
  outcome: "no-match",
4480
- files: [{
4481
- path: ".github/scripts/publish.mjs",
4482
- source: "import { env } from './env.mjs';\nconst base = `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}`;\nexport async function publish(path, request = fetch) { return request(`${base}/${path}`); }"
4483
- }],
4516
+ files: [
4517
+ {
4518
+ path: ".github/scripts/publish.mjs",
4519
+ source: "import { settings } from '../../src/env.mjs';\nconst base = `https://api.cloudflare.com/client/v4/accounts/${settings.CLOUDFLARE_ACCOUNT_ID}`;\nexport async function publish(path, request = fetch) { return request(`${base}/${path}`); }\nexport async function list(request = fetch) { return request(`${base}/workers/scripts`); }"
4520
+ },
4521
+ {
4522
+ path: "src/env.mjs",
4523
+ source: "import { z } from 'zod';\nconst SettingsSchema = z.object({ CLOUDFLARE_ACCOUNT_ID: z.string().length(32).regex(/^[a-fA-F0-9]+$/u) });\nexport const settings = SettingsSchema.parse(process.env);"
4524
+ }
4525
+ ],
4484
4526
  focusPath: ".github/scripts/publish.mjs",
4485
4527
  expectedCount: 0,
4486
4528
  public: true
@@ -15557,11 +15557,25 @@ var no_production_browser_source_maps_default = defineRule({
15557
15557
 
15558
15558
  // src/rules/no-raw-env.ts
15559
15559
  var CONFIG_FILE_RE = /(^|[\\/])[\w.-]+\.config\.[cm]?[jt]sx?$/;
15560
- var ENV_BOUNDARY_FILE_RE = /(^|[\\/])(?:env|client-env|server-env|client-settings|server-settings)\.[cm]?[jt]sx?$/;
15560
+ var ENV_BOUNDARY_FILE_RE = /(^|[\\/])(?:env|settings|client-env|server-env|client-settings|server-settings)\.[cm]?[jt]sx?$/;
15561
15561
  var PROCESS_MODULES = ["node:process", "process"];
15562
- var PROCESS_IMPORT_SPECIFIERS = ["ImportDefaultSpecifier", "ImportNamespaceSpecifier"];
15563
- var BUILD_TIME_CONSTANTS = /* @__PURE__ */ new Set(["NODE_ENV", "MODE", "DEV", "PROD", "SSR"]);
15564
- var PLATFORM_MARKERS = /* @__PURE__ */ new Set(["NEXT_RUNTIME", "VERCEL", "VERCEL_ENV", "CI"]);
15562
+ var PROCESS_IMPORT_SPECIFIERS = [
15563
+ "ImportDefaultSpecifier",
15564
+ "ImportNamespaceSpecifier"
15565
+ ];
15566
+ var BUILD_TIME_CONSTANTS = /* @__PURE__ */ new Set([
15567
+ "NODE_ENV",
15568
+ "MODE",
15569
+ "DEV",
15570
+ "PROD",
15571
+ "SSR"
15572
+ ]);
15573
+ var PLATFORM_MARKERS = /* @__PURE__ */ new Set([
15574
+ "NEXT_RUNTIME",
15575
+ "VERCEL",
15576
+ "VERCEL_ENV",
15577
+ "CI"
15578
+ ]);
15565
15579
  function isProcessEnv(node, sourceCode2) {
15566
15580
  const receiver = unwrapExpression(node.object);
15567
15581
  if (receiver.type !== "Identifier" || getPropertyName(node) !== "env") return false;
@@ -15597,6 +15611,58 @@ function isWholeEnvSpread(node) {
15597
15611
  const parent = parentOf(node);
15598
15612
  return parent?.type === "SpreadElement" && parent.argument === node;
15599
15613
  }
15614
+ function reachesValidation(node, sourceCode2, seen = /* @__PURE__ */ new Set(), depth = 0, budget = { remaining: 512 }) {
15615
+ if (depth >= 24 || budget.remaining-- <= 0) return true;
15616
+ const parent = parentOf(node);
15617
+ if (parent === null) return false;
15618
+ switch (parent.type) {
15619
+ case "CallExpression":
15620
+ return parent.arguments.some((argument) => argument === node) && (isValidationCall(parent) || isScalarCoercion(parent, sourceCode2) && reachesValidation(parent, sourceCode2, seen, depth + 1, budget));
15621
+ case "VariableDeclarator":
15622
+ return parent.init === node && validatedAlias(parent, sourceCode2, seen, depth, budget);
15623
+ case "MemberExpression":
15624
+ return parent.object === node && reachesValidation(parent, sourceCode2, seen, depth + 1, budget);
15625
+ case "Property":
15626
+ return parent.value === node && parent.kind === "init" && !parent.method && reachesValidation(parent, sourceCode2, seen, depth + 1, budget);
15627
+ case "ObjectExpression":
15628
+ case "ArrayExpression":
15629
+ case "SpreadElement":
15630
+ case "LogicalExpression":
15631
+ case "BinaryExpression":
15632
+ case "ConditionalExpression":
15633
+ case "TemplateLiteral":
15634
+ case "TSAsExpression":
15635
+ case "TSNonNullExpression":
15636
+ case "TSSatisfiesExpression":
15637
+ return reachesValidation(parent, sourceCode2, seen, depth + 1, budget);
15638
+ default:
15639
+ return false;
15640
+ }
15641
+ }
15642
+ function isValidationCall(node) {
15643
+ const callee = unwrapExpression(node.callee);
15644
+ return callee.type === "Identifier" ? callee.name === "createEnv" : callee.type === "MemberExpression" && ["parse", "safeParse"].includes(getPropertyName(callee) ?? "");
15645
+ }
15646
+ function isScalarCoercion(node, sourceCode2) {
15647
+ const callee = unwrapExpression(node.callee);
15648
+ if (callee.type !== "Identifier" || !["Number", "String", "Boolean", "BigInt"].includes(callee.name) || node.arguments.length !== 1)
15649
+ return false;
15650
+ const binding3 = findVariable(sourceCode2.getScope(callee), callee.name);
15651
+ return binding3 === null || binding3.defs.length === 0;
15652
+ }
15653
+ function validatedAlias(node, sourceCode2, seen, depth, budget) {
15654
+ const declaration = parentOf(node);
15655
+ if (node.id.type !== "Identifier" || declaration?.type !== "VariableDeclaration" || declaration.kind !== "const")
15656
+ return false;
15657
+ if (parentOf(declaration)?.type === "ExportNamedDeclaration") return false;
15658
+ const variable = findVariable(sourceCode2.getScope(node.id), node.id.name);
15659
+ if (variable === null || seen.has(variable) || variable.references.some((ref) => ref.isWrite() && ref.init !== true))
15660
+ return false;
15661
+ const reads = variable.references.filter((ref) => ref.isRead());
15662
+ const next = /* @__PURE__ */ new Set([...seen, variable]);
15663
+ if (reads.length > 128) return true;
15664
+ return reads.length > 0 && reads.every((ref) => reachesValidation(ref.identifier, sourceCode2, next, depth + 1, budget));
15665
+ }
15600
15666
  var no_raw_env_default = defineRule({
15601
15667
  meta: {
15602
15668
  type: "problem",
@@ -15606,48 +15672,35 @@ var no_raw_env_default = defineRule({
15606
15672
  },
15607
15673
  schema: [],
15608
15674
  messages: {
15609
- noRawEnv: "Do not read raw environment values directly. Import the validated env module so values are typed and checked at startup."
15675
+ noRawEnv: "Read one shared settings object validated at startup with Zod (or the existing typed settings loader). Parse required environment values once at the boundary and inject or import the validated result."
15610
15676
  }
15611
15677
  },
15612
15678
  createOnce(context) {
15613
15679
  let sourceCode2;
15614
15680
  let reads = [];
15615
15681
  let boundaryFile = false;
15616
- let hasValidationCall = false;
15617
15682
  return {
15618
15683
  before() {
15619
15684
  const filename = context.filename;
15620
15685
  const file = { filename, cwd: context.cwd };
15621
- if (isTestFile(file) || isScriptFile(file) || CONFIG_FILE_RE.test(filename)) {
15686
+ boundaryFile = ENV_BOUNDARY_FILE_RE.test(filename);
15687
+ if (isTestFile(file) || isScriptFile(file) && !boundaryFile || CONFIG_FILE_RE.test(filename)) {
15622
15688
  return false;
15623
15689
  }
15624
15690
  sourceCode2 = context.sourceCode;
15625
15691
  reads = [];
15626
- boundaryFile = ENV_BOUNDARY_FILE_RE.test(filename);
15627
- hasValidationCall = false;
15628
15692
  return void 0;
15629
15693
  },
15630
- CallExpression(node) {
15631
- if (!boundaryFile) return;
15632
- const callee = node.callee;
15633
- if (callee.type === "Identifier" && callee.name === "createEnv") {
15634
- hasValidationCall = true;
15635
- } else if (callee.type === "MemberExpression") {
15636
- const calleeReceiver = unwrapExpression(callee.object);
15637
- const name = getPropertyName(callee);
15638
- if (name === "parse" || name === "safeParse" || name === "object" && calleeReceiver.type === "Identifier" && calleeReceiver.name === "z") {
15639
- hasValidationCall = true;
15640
- }
15641
- }
15642
- },
15643
15694
  MemberExpression(node) {
15644
- if ((isProcessEnv(node, sourceCode2) || isImportMetaEnv(node)) && !isExemptVariableAccess(node) && !isWriteTarget(node) && !isWholeEnvSpread(node)) {
15695
+ if ((isProcessEnv(node, sourceCode2) || isImportMetaEnv(node)) && !isExemptVariableAccess(node) && !isWriteTarget(node) && !(isWholeEnvSpread(node) && !boundaryFile)) {
15645
15696
  reads.push(node);
15646
15697
  }
15647
15698
  },
15648
15699
  "Program:exit"() {
15649
- if (boundaryFile && hasValidationCall) return;
15650
- for (const node of reads) context.report({ node, messageId: "noRawEnv" });
15700
+ for (const node of reads) {
15701
+ if (!boundaryFile || !reachesValidation(node, sourceCode2))
15702
+ context.report({ node, messageId: "noRawEnv" });
15703
+ }
15651
15704
  }
15652
15705
  };
15653
15706
  }
@@ -22035,7 +22088,7 @@ var prefer_configured_cloud_scope_default = defineRule({
22035
22088
  },
22036
22089
  schema: [],
22037
22090
  messages: {
22038
- configuredScope: "This outgoing Cloudflare management request uses a source-literal account scope. Read and validate the account ID from deployment configuration or environment variables at the boundary; keep independent security allowlists intact."
22091
+ configuredScope: "This outgoing Cloudflare management request uses a source-literal account scope. Load the account ID into one shared Zod-validated settings object at startup, then inject or import it; keep independent security allowlists intact."
22039
22092
  }
22040
22093
  },
22041
22094
  createOnce(context) {
@@ -22062,14 +22115,16 @@ var prefer_configured_cloud_scope_default = defineRule({
22062
22115
  if (callee.type !== "Identifier") return false;
22063
22116
  const binding3 = bindingOf(callee);
22064
22117
  if (binding3 === null || binding3.defs.length === 0) return callee.name === "fetch";
22065
- if (seen.has(binding3) || seen.size >= 16 || binding3.defs.length !== 1 || binding3.references.some((ref) => ref.isWrite() && ref.init !== true)) return false;
22118
+ if (seen.has(binding3) || seen.size >= 16 || binding3.defs.length !== 1 || binding3.references.some((ref) => ref.isWrite() && ref.init !== true))
22119
+ return false;
22066
22120
  const next = /* @__PURE__ */ new Set([...seen, binding3]);
22067
22121
  const [definition] = binding3.defs;
22068
22122
  if (definition?.type === "Parameter") {
22069
22123
  const parameter = parentOf(definition.name);
22070
22124
  return parameter?.type === "AssignmentPattern" && parameter.left === definition.name && outgoingFetch(parameter.right, next);
22071
22125
  }
22072
- if (definition?.type !== "Variable" || definition.parent?.type !== "VariableDeclaration" || definition.parent.kind !== "const") return false;
22126
+ if (definition?.type !== "Variable" || definition.parent?.type !== "VariableDeclaration" || definition.parent.kind !== "const")
22127
+ return false;
22073
22128
  const declarator = definition.node;
22074
22129
  return declarator.id.type === "Identifier" && declarator.init !== null && outgoingFetch(declarator.init, next);
22075
22130
  }
@@ -22090,10 +22145,15 @@ var prefer_configured_cloud_scope_default = defineRule({
22090
22145
  }
22091
22146
  return;
22092
22147
  case "ObjectPattern":
22093
- for (const property of value.properties) checkWrite(property.type === "RestElement" ? property.argument : property.value, depth + 1);
22148
+ for (const property of value.properties)
22149
+ checkWrite(
22150
+ property.type === "RestElement" ? property.argument : property.value,
22151
+ depth + 1
22152
+ );
22094
22153
  return;
22095
22154
  case "ArrayPattern":
22096
- for (const element of value.elements) if (element !== null) checkWrite(element, depth + 1);
22155
+ for (const element of value.elements)
22156
+ if (element !== null) checkWrite(element, depth + 1);
22097
22157
  return;
22098
22158
  case "AssignmentPattern":
22099
22159
  checkWrite(value.left, depth + 1);
@@ -22111,15 +22171,20 @@ var prefer_configured_cloud_scope_default = defineRule({
22111
22171
  }
22112
22172
  if (value.type === "Identifier") {
22113
22173
  const binding3 = bindingOf(value);
22114
- if (binding3 === null || seen.has(binding3) || binding3.defs.length !== 1 || binding3.references.some((ref) => ref.isWrite() && ref.init !== true)) return UNKNOWN;
22174
+ if (binding3 === null || seen.has(binding3) || binding3.defs.length !== 1 || binding3.references.some((ref) => ref.isWrite() && ref.init !== true))
22175
+ return UNKNOWN;
22115
22176
  const [definition] = binding3.defs;
22116
- if (definition?.type !== "Variable" || definition.parent?.type !== "VariableDeclaration" || definition.parent.kind !== "const") return UNKNOWN;
22177
+ if (definition?.type !== "Variable" || definition.parent?.type !== "VariableDeclaration" || definition.parent.kind !== "const")
22178
+ return UNKNOWN;
22117
22179
  const declarator = definition.node;
22118
22180
  if (declarator.id.type !== "Identifier" || declarator.init === null) return UNKNOWN;
22119
22181
  return urlParts(declarator.init, /* @__PURE__ */ new Set([...seen, binding3]), depth + 1);
22120
22182
  }
22121
22183
  if (value.type === "BinaryExpression" && value.operator === "+") {
22122
- return boundedParts([...urlParts(value.left, seen, depth + 1), ...urlParts(value.right, seen, depth + 1)]);
22184
+ return boundedParts([
22185
+ ...urlParts(value.left, seen, depth + 1),
22186
+ ...urlParts(value.right, seen, depth + 1)
22187
+ ]);
22123
22188
  }
22124
22189
  if (value.type === "TemplateLiteral") return templateParts(value, seen, depth);
22125
22190
  return UNKNOWN;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  src_default
3
- } from "./chunk-MFL7OATV.js";
3
+ } from "./chunk-ZWOGM4CV.js";
4
4
  export {
5
5
  src_default as default
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "19.9.0",
3
+ "version": "19.10.0",
4
4
  "packageManager": "npm@12.2.1",
5
5
  "description": "Custom Oxlint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",