@sarj/eslint-plugin 16.12.0 → 16.13.1

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/index.cjs CHANGED
@@ -52,7 +52,7 @@ module.exports = __toCommonJS(src_exports);
52
52
  // package.json
53
53
  var package_default = {
54
54
  name: "@sarj/eslint-plugin",
55
- version: "16.12.0",
55
+ version: "16.13.1",
56
56
  packageManager: "npm@12.2.0",
57
57
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
58
58
  type: "module",
@@ -4794,254 +4794,291 @@ var no_comment_cruft_default = createRule({
4794
4794
 
4795
4795
  // src/rules/no-cors-wildcard-with-credentials.ts
4796
4796
  var import_utils28 = require("@typescript-eslint/utils");
4797
+ var ACAO_HEADER = "access-control-allow-origin";
4798
+ var ACAC_HEADER = "access-control-allow-credentials";
4799
+ var HEADER_METHODS = /* @__PURE__ */ new Set(["set", "setHeader", "append", "delete", "removeHeader"]);
4800
+ var CONTROL_STATEMENTS = /* @__PURE__ */ new Set([
4801
+ "BlockStatement",
4802
+ "IfStatement",
4803
+ "SwitchStatement",
4804
+ "TryStatement",
4805
+ "ForStatement",
4806
+ "ForInStatement",
4807
+ "ForOfStatement",
4808
+ "WhileStatement",
4809
+ "DoWhileStatement",
4810
+ "WithStatement",
4811
+ "LabeledStatement"
4812
+ ]);
4797
4813
  var NO_CORS_WILDCARD_WITH_CREDENTIALS_DOCUMENTATION = {
4798
- summary: "Disallow wildcard CORS origins when credentials are enabled.",
4799
- rationale: "Reflecting every origin while allowing credentials can let an untrusted site read authenticated cross-origin responses.",
4800
- remediation: "Enumerate the trusted origins that may receive credentialed responses.",
4801
- category: "security",
4814
+ summary: "Review wildcard CORS origins combined with an explicit credentials opt-in.",
4815
+ rationale: "Browsers do not expose credentialed cross-origin responses when Access-Control-Allow-Origin is '*'. Credentialless requests can still use a wildcard even when Access-Control-Allow-Credentials is emitted, so review the intended caller contract.",
4816
+ remediation: "Use a trusted explicit origin for credentialed callers, or omit the credentials opt-in for a credentialless wildcard endpoint.",
4817
+ category: "correctness",
4818
+ defaultLevel: "warning",
4819
+ autofix: "none",
4820
+ references: ["https://fetch.spec.whatwg.org/#cors-protocol-and-credentials"],
4802
4821
  limitations: [
4803
- "The rule detects literal CORS option and header combinations within the same syntactic scope; it does not resolve runtime configuration."
4822
+ "Options require a stable official cors default import or hono/cors named or namespace import, one literal object argument, literal origin '*', and credentials true. Custom bindings, callbacks, arrays, conditional origins and dynamic options are excluded.",
4823
+ "Literal header maps use effective case-sensitive JavaScript property writes followed by native Headers combination of differently-cased header names. Unknown values/spreads/computed keys, accessors and non-null prototypes lose proof; literals used only inside object spreads are not reported independently.",
4824
+ "Manual set/setHeader/delete/removeHeader calls use direct identifier receivers and literal arguments in straight-line program or function bodies. Control-flow bodies, member receivers, aliases and unknown operations are excluded. Append requires a proven empty native Headers constructor or a preceding known write/removal.",
4825
+ "The rule reviews a local configuration contract, not actual request credentials or a data disclosure. Runtime patches, custom header method implementations, interprocedural mutation and later consumers are not resolved. No autofix."
4804
4826
  ],
4805
4827
  examples: [
4806
4828
  {
4807
4829
  id: "trusted-origin-with-credentials",
4808
- title: "Credentials are limited to a trusted origin",
4830
+ title: "Use a trusted origin for credentialed callers",
4809
4831
  outcome: "no-match",
4810
- files: [{ path: "src/server.ts", source: "app.use(cors({ origin: 'https://app.example.com', credentials: true }));" }],
4832
+ files: [{ path: "src/server.ts", source: "import cors from 'cors'; cors({ origin: 'https://app.example.com', credentials: true });" }],
4811
4833
  focusPath: "src/server.ts",
4812
4834
  expectedCount: 0,
4813
4835
  public: true
4814
4836
  },
4815
4837
  {
4816
4838
  id: "wildcard-origin-with-credentials",
4817
- title: "Credentials are enabled for every origin",
4839
+ title: "Review a wildcard that blocks access to credentialed responses",
4818
4840
  outcome: "match",
4819
- files: [{ path: "src/server.ts", source: "app.use(cors({ origin: '*', credentials: true }));" }],
4841
+ files: [{ path: "src/server.ts", source: "import cors from 'cors'; cors({ origin: '*', credentials: true });" }],
4820
4842
  focusPath: "src/server.ts",
4821
4843
  expectedCount: 1,
4822
4844
  public: true
4823
4845
  }
4824
4846
  ]
4825
4847
  };
4826
- var ACAO_HEADER = "access-control-allow-origin";
4827
- var ACAC_HEADER = "access-control-allow-credentials";
4828
- var HEADER_SET_METHODS = /* @__PURE__ */ new Set(["setheader", "set", "append"]);
4829
- function isCredentialsTrueValue(node) {
4830
- node = unwrapExpression(node);
4831
- if (node.type === "Literal") {
4832
- if (node.value === true) {
4833
- return true;
4834
- }
4835
- if (typeof node.value === "string") {
4836
- return node.value.trim().toLowerCase() === "true";
4837
- }
4838
- }
4839
- return false;
4840
- }
4841
- function isStarLiteral(node) {
4842
- return node.type === "Literal" && node.value === "*";
4843
- }
4844
- function propertyKeyName(prop) {
4845
- return import_utils28.ASTUtils.getPropertyName(prop) ?? void 0;
4846
- }
4847
- function isCorsWildcardCredentialsCall(node) {
4848
- const name = calleeName(node);
4849
- if (name === void 0 || name.toLowerCase() !== "cors") {
4850
- return false;
4851
- }
4852
- const options = node.arguments.map(unwrapExpression).find(
4853
- (arg) => arg.type === "ObjectExpression"
4854
- );
4855
- if (options === void 0) {
4856
- return false;
4857
- }
4858
- let hasCredentials = false;
4859
- let hasWildcardOrigin = false;
4860
- for (const prop of options.properties) {
4861
- if (prop.type !== "Property") {
4862
- continue;
4863
- }
4864
- const key = propertyKeyName(prop);
4865
- if (key === "credentials" && isTrueLiteral(prop.value)) {
4866
- hasCredentials = true;
4867
- } else if (key === "origin" && subtreeContainsStarLiteral(prop.value)) {
4868
- hasWildcardOrigin = true;
4869
- }
4870
- }
4871
- return hasCredentials && hasWildcardOrigin;
4872
- }
4873
- function isTrueLiteral(node) {
4874
- node = unwrapExpression(node);
4875
- return node.type === "Literal" && node.value === true;
4876
- }
4877
- function subtreeContainsStarLiteral(node) {
4878
- if (isStarLiteral(node)) {
4879
- return true;
4880
- }
4881
- return forEachOwnAstChild(
4882
- node,
4883
- subtreeContainsStarLiteral,
4884
- (key) => key !== "loc" && key !== "range"
4885
- );
4886
- }
4887
- function calleeName(node) {
4888
- const callee = unwrapExpression(node.callee);
4889
- if (callee.type === "Identifier") {
4890
- return callee.name;
4891
- }
4892
- if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier") {
4893
- return callee.property.name;
4894
- }
4895
- return void 0;
4896
- }
4897
- function isWildcardCredentialsHeaderObject(node) {
4898
- let wildcardOrigin = false;
4899
- let credentialsTrue = false;
4900
- for (const prop of node.properties) {
4901
- if (prop.type !== "Property") {
4902
- continue;
4903
- }
4904
- const key = propertyKeyName(prop);
4905
- if (key === void 0) {
4906
- continue;
4907
- }
4908
- const header = key.toLowerCase();
4909
- if (header === ACAO_HEADER && isStarLiteral(prop.value)) {
4910
- wildcardOrigin = true;
4911
- } else if (header === ACAC_HEADER && isCredentialsTrueValue(prop.value)) {
4912
- credentialsTrue = true;
4913
- }
4914
- }
4915
- return wildcardOrigin && credentialsTrue;
4916
- }
4917
- function classifyHeaderSetCall(node) {
4918
- const callee = unwrapExpression(node.callee);
4919
- if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || !HEADER_SET_METHODS.has(callee.property.name.toLowerCase())) {
4920
- return void 0;
4921
- }
4922
- const [nameArg, valueArg] = node.arguments;
4923
- if (nameArg === void 0 || valueArg === void 0 || nameArg.type !== "Literal" || typeof nameArg.value !== "string") {
4924
- return void 0;
4925
- }
4926
- const header = nameArg.value.toLowerCase();
4927
- if (header === ACAO_HEADER && isStarLiteral(valueArg)) {
4928
- return "origin";
4929
- }
4930
- if (header === ACAC_HEADER && isCredentialsTrueValue(valueArg)) {
4931
- return "credentials";
4932
- }
4933
- return void 0;
4934
- }
4935
- function enclosingScope(node) {
4936
- let current = node.parent;
4937
- while (current) {
4938
- if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
4939
- return current;
4940
- }
4941
- current = current.parent;
4942
- }
4943
- return void 0;
4944
- }
4945
4848
  var no_cors_wildcard_with_credentials_default = createRule({
4946
4849
  name: "no-cors-wildcard-with-credentials",
4947
4850
  documentation: NO_CORS_WILDCARD_WITH_CREDENTIALS_DOCUMENTATION,
4948
4851
  meta: {
4949
4852
  type: "problem",
4950
- docs: {
4951
- description: "Disallow wildcard CORS origins when credentials are enabled."
4952
- },
4853
+ docs: { description: NO_CORS_WILDCARD_WITH_CREDENTIALS_DOCUMENTATION.summary },
4953
4854
  schema: [],
4954
4855
  messages: {
4955
- corsWildcardWithCredentials: 'CORS reflects any Origin (`"*"`) while allowing credentials \u2014 any site can read authenticated responses. Enumerate explicit trusted origins instead of using `"*"` with credentials.'
4856
+ corsWildcardWithCredentials: "A wildcard CORS origin blocks browser access to credentialed responses. Review the intended contract: use a trusted explicit origin for credentialed callers, or omit the credentials opt-in for a credentialless endpoint."
4956
4857
  }
4957
4858
  },
4958
4859
  defaultOptions: [],
4959
4860
  create(context) {
4960
- const scopeHeaderSets = /* @__PURE__ */ new Map();
4961
- const variableIds = /* @__PURE__ */ new WeakMap();
4962
- let nextVariableId = 0;
4963
- function recordHeaderSet(node, kind) {
4964
- const unwrappedNodeCallee = unwrapExpression(node.callee);
4965
- if (unwrappedNodeCallee.type !== import_utils28.AST_NODE_TYPES.MemberExpression) return;
4966
- const receiver = receiverIdentity(unwrappedNodeCallee.object);
4967
- if (receiver === null) return;
4968
- const key = enclosingScope(node) ?? "module";
4969
- let receivers = scopeHeaderSets.get(key);
4970
- if (receivers === void 0) {
4971
- receivers = /* @__PURE__ */ new Map();
4972
- scopeHeaderSets.set(key, receivers);
4973
- }
4974
- let entry = receivers.get(receiver);
4975
- if (entry === void 0) {
4976
- entry = { originNodes: [], credentialsNodes: [] };
4977
- receivers.set(receiver, entry);
4978
- }
4979
- if (kind === "origin") {
4980
- entry.originNodes.push(node);
4981
- } else {
4982
- entry.credentialsNodes.push(node);
4861
+ const sourceCode = context.sourceCode;
4862
+ const importedCors = /* @__PURE__ */ new Set();
4863
+ const importedNamespaces = /* @__PURE__ */ new Set();
4864
+ const receiverProof = /* @__PURE__ */ new Map();
4865
+ function binding(node) {
4866
+ return import_utils28.ASTUtils.findVariable(sourceCode.getScope(node), node.name);
4867
+ }
4868
+ function officialCors(node) {
4869
+ const value = unwrapExpression(node);
4870
+ if (value.type === "Identifier") {
4871
+ const variable2 = binding(value);
4872
+ return variable2 !== null && importedCors.has(variable2);
4983
4873
  }
4874
+ if (value.type !== "MemberExpression" || value.optional || value.computed || value.property.type !== "Identifier" || value.property.name !== "cors" || value.object.type !== "Identifier") return false;
4875
+ const variable = binding(value.object);
4876
+ return variable !== null && importedNamespaces.has(variable);
4984
4877
  }
4985
- function receiverIdentity(node) {
4986
- if (node.type === import_utils28.AST_NODE_TYPES.Identifier) {
4987
- const variable = import_utils28.ASTUtils.findVariable(
4988
- context.sourceCode.getScope(node),
4989
- node.name
4990
- );
4991
- return variable === null ? `global:${node.name}` : `variable:${variableId(variable)}`;
4878
+ function recordImports(node) {
4879
+ for (const statement of node.body) {
4880
+ if (statement.type === "ImportDeclaration" && statement.importKind !== "type") recordImport(statement);
4881
+ }
4882
+ }
4883
+ function recordImport(node) {
4884
+ for (const specifier of node.specifiers) {
4885
+ const role = importRole(specifier, node.source.value);
4886
+ if (role === null) continue;
4887
+ const variable = binding(specifier.local);
4888
+ if (variable === null || variable.references.some((reference) => reference.isWrite())) continue;
4889
+ (role === "namespace" ? importedNamespaces : importedCors).add(variable);
4890
+ }
4891
+ }
4892
+ function inspectSequence(node) {
4893
+ if (node.body.some((statement) => CONTROL_STATEMENTS.has(statement.type))) return;
4894
+ const states = /* @__PURE__ */ new Map();
4895
+ for (const statement of node.body) {
4896
+ if (statement.type === "ImportDeclaration" || statement.type === "FunctionDeclaration") continue;
4897
+ if (statement.type === "VariableDeclaration") {
4898
+ recordHeaderDeclaration(statement, states);
4899
+ continue;
4900
+ }
4901
+ const expression = statement.type === "ExpressionStatement" ? unwrapExpression(statement.expression) : null;
4902
+ if (expression?.type !== "CallExpression" || !applyHeaderWrite(expression, states)) states.clear();
4992
4903
  }
4993
- if (node.type === import_utils28.AST_NODE_TYPES.ThisExpression) return "this";
4994
- if (node.type === import_utils28.AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === import_utils28.AST_NODE_TYPES.Identifier) {
4995
- const owner = receiverIdentity(node.object);
4996
- return owner === null ? null : `${owner}.${node.property.name}`;
4904
+ for (const state of states.values()) {
4905
+ if (state.values[ACAO_HEADER] === "*" && state.values[ACAC_HEADER] === "true" && state.origin !== null) {
4906
+ context.report({ node: state.origin, messageId: "corsWildcardWithCredentials" });
4907
+ }
4997
4908
  }
4998
- return null;
4999
4909
  }
5000
- function variableId(variable) {
5001
- const existing = variableIds.get(variable);
5002
- if (existing !== void 0) return existing;
5003
- const value = nextVariableId++;
5004
- variableIds.set(variable, value);
5005
- return value;
4910
+ function recordHeaderDeclaration(node, states) {
4911
+ for (const declaration of node.declarations) {
4912
+ if (declaration.id.type === "Identifier" && declaration.init !== null && freshHeaders(declaration.init)) {
4913
+ const key = receiverKey(declaration.id);
4914
+ if (key !== null) states.set(key, { values: { [ACAO_HEADER]: null, [ACAC_HEADER]: null }, origin: null });
4915
+ } else if (declaration.id.type !== "Identifier" || declaration.init !== null && unwrapExpression(declaration.init).type !== "Literal") states.clear();
4916
+ }
4917
+ }
4918
+ function freshHeaders(node) {
4919
+ const value = unwrapExpression(node);
4920
+ if (value.type !== "NewExpression" || value.arguments.length !== 0) return false;
4921
+ const callee = unwrapExpression(value.callee);
4922
+ return callee.type === "Identifier" && callee.name === "Headers" && !binding(callee)?.defs.length;
4923
+ }
4924
+ function applyHeaderWrite(node, states) {
4925
+ const callee = unwrapExpression(node.callee);
4926
+ if (node.optional || callee.type !== "MemberExpression" || callee.optional || callee.computed || callee.property.type !== "Identifier" || !HEADER_METHODS.has(callee.property.name) || callee.object.type !== "Identifier") return false;
4927
+ const method = callee.property.name;
4928
+ const removal = method === "delete" || method === "removeHeader";
4929
+ if (node.arguments.length !== (removal ? 1 : 2)) return false;
4930
+ const name = literalString(node.arguments[0]);
4931
+ const value = removal ? null : headerValue(node.arguments[1]);
4932
+ if (name === null || !removal && value === null) return false;
4933
+ const key = receiverKey(callee.object);
4934
+ if (key === null) return false;
4935
+ let state = states.get(key);
4936
+ if (state === void 0) {
4937
+ state = { values: { [ACAO_HEADER]: void 0, [ACAC_HEADER]: void 0 }, origin: null };
4938
+ states.set(key, state);
4939
+ }
4940
+ const header = name.toLowerCase();
4941
+ if (header !== ACAO_HEADER && header !== ACAC_HEADER) return true;
4942
+ state.values[header] = method === "append" ? appendValue(state.values[header], value) : value;
4943
+ if (header === ACAO_HEADER) state.origin = node;
4944
+ return true;
4945
+ }
4946
+ function receiverKey(node) {
4947
+ const variable = binding(node);
4948
+ const key = variable ?? node.name;
4949
+ const cached = receiverProof.get(key);
4950
+ if (cached !== void 0) return cached ? key : null;
4951
+ const references = variable?.references ?? sourceCode.scopeManager?.globalScope?.through.filter((reference) => reference.identifier.name === node.name) ?? [];
4952
+ const unaliased = !variable?.defs.some(aliasDefinition) && references.every(headerReceiverReference);
4953
+ receiverProof.set(key, unaliased);
4954
+ return unaliased ? key : null;
5006
4955
  }
5007
4956
  return {
5008
- NewExpression(node) {
5009
- if (isCorsWildcardCredentialsCall(node)) {
5010
- context.report({ node, messageId: "corsWildcardWithCredentials" });
5011
- }
4957
+ Program(node) {
4958
+ recordImports(node);
4959
+ inspectSequence(node);
4960
+ },
4961
+ BlockStatement(node) {
4962
+ if (["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"].includes(node.parent.type)) inspectSequence(node);
5012
4963
  },
5013
4964
  CallExpression(node) {
5014
- if (isCorsWildcardCredentialsCall(node)) {
4965
+ if (node.optional || node.arguments.length !== 1 || !officialCors(node.callee)) return;
4966
+ const argument = node.arguments[0];
4967
+ const options = argument === void 0 ? void 0 : unwrapExpression(argument);
4968
+ if (options?.type !== "ObjectExpression") return;
4969
+ const properties = effectiveProperties(options);
4970
+ const origin = properties?.get("origin");
4971
+ const credentials = properties?.get("credentials");
4972
+ const value = credentials === void 0 ? void 0 : unwrapExpression(credentials.value);
4973
+ if (origin !== void 0 && literalString(origin.value) === "*" && value?.type === "Literal" && value.value === true) {
5015
4974
  context.report({ node, messageId: "corsWildcardWithCredentials" });
5016
- return;
5017
- }
5018
- const kind = classifyHeaderSetCall(node);
5019
- if (kind !== void 0) {
5020
- recordHeaderSet(node, kind);
5021
4975
  }
5022
4976
  },
5023
4977
  ObjectExpression(node) {
5024
- if (isWildcardCredentialsHeaderObject(node)) {
4978
+ if (spreadOnly(node)) return;
4979
+ const properties = effectiveProperties(node);
4980
+ if (properties === null) return;
4981
+ const headers = /* @__PURE__ */ new Map();
4982
+ for (const [key, property] of properties) {
4983
+ const value = headerValue(property.value);
4984
+ if (value === null) return;
4985
+ const name = key.toLowerCase();
4986
+ if (name !== ACAO_HEADER && name !== ACAC_HEADER) continue;
4987
+ const previous = headers.get(name);
4988
+ headers.set(name, previous === void 0 ? value : previous + ", " + value);
4989
+ }
4990
+ if (headers.get(ACAO_HEADER) === "*" && headers.get(ACAC_HEADER) === "true") {
5025
4991
  context.report({ node, messageId: "corsWildcardWithCredentials" });
5026
4992
  }
5027
- },
5028
- "Program:exit"() {
5029
- for (const receivers of scopeHeaderSets.values()) {
5030
- for (const { originNodes, credentialsNodes } of receivers.values()) {
5031
- if (originNodes.length > 0 && credentialsNodes.length > 0) {
5032
- for (const node of originNodes) {
5033
- context.report({
5034
- node,
5035
- messageId: "corsWildcardWithCredentials"
5036
- });
5037
- }
5038
- }
5039
- }
5040
- }
5041
4993
  }
5042
4994
  };
5043
4995
  }
5044
4996
  });
4997
+ function importRole(specifier, module2) {
4998
+ if (module2 === "hono/cors" && specifier.type === "ImportNamespaceSpecifier") return "namespace";
4999
+ if (module2 === "cors" && specifier.type === "ImportDefaultSpecifier") return "function";
5000
+ if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type") return null;
5001
+ const name = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
5002
+ return module2 === "cors" && name === "default" || module2 === "hono/cors" && name === "cors" ? "function" : null;
5003
+ }
5004
+ function aliasDefinition(definition) {
5005
+ if (definition.type !== "Variable") return false;
5006
+ if (definition.node.id.type !== "Identifier") return true;
5007
+ const initializer = definition.node.init;
5008
+ return initializer !== null && ["Identifier", "MemberExpression", "AssignmentExpression", "ConditionalExpression", "LogicalExpression", "SequenceExpression"].includes(unwrapExpression(initializer).type);
5009
+ }
5010
+ function headerReceiverReference(reference) {
5011
+ let typeName = reference.identifier;
5012
+ while (typeName.parent.type === "TSQualifiedName") typeName = typeName.parent;
5013
+ if (reference.isValueReference === false || typeName.parent.type === "TSTypeQuery") return true;
5014
+ if (reference.isWrite()) return reference.init === true;
5015
+ const member = reference.identifier.parent;
5016
+ if (member.type !== "MemberExpression" || member.object !== reference.identifier || member.computed || member.optional || member.property.type !== "Identifier" || !HEADER_METHODS.has(member.property.name)) return false;
5017
+ return member.parent.type === "CallExpression" && member.parent.callee === member && !member.parent.optional;
5018
+ }
5019
+ function appendValue(previous, value) {
5020
+ if (previous === void 0) return void 0;
5021
+ return previous === null ? value : previous + ", " + value;
5022
+ }
5023
+ function literalString(node) {
5024
+ const value = node === void 0 ? void 0 : unwrapExpression(node);
5025
+ return value?.type === "Literal" && typeof value.value === "string" ? value.value : null;
5026
+ }
5027
+ function headerValue(node) {
5028
+ const value = node === void 0 ? void 0 : unwrapExpression(node);
5029
+ if (value?.type !== "Literal" || !["string", "boolean", "number"].includes(typeof value.value)) return null;
5030
+ return String(value.value).replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
5031
+ }
5032
+ function effectiveProperties(node) {
5033
+ const properties = /* @__PURE__ */ new Map();
5034
+ for (const entry of node.properties) {
5035
+ const entries = propertyEntries(entry);
5036
+ if (entries === null) return null;
5037
+ for (const [key, property] of entries) properties.set(key, property);
5038
+ }
5039
+ return properties;
5040
+ }
5041
+ function propertyEntries(entry) {
5042
+ if (entry.type === "SpreadElement") {
5043
+ const value = unwrapExpression(entry.argument);
5044
+ return value.type === "ObjectExpression" ? effectiveProperties(value) : null;
5045
+ }
5046
+ if (entry.kind !== "init" || entry.method) return null;
5047
+ const key = entry.computed ? literalPropertyKey(entry.key) : import_utils28.ASTUtils.getPropertyName(entry);
5048
+ if (key === null) return null;
5049
+ if (key === "__proto__" && !entry.computed) {
5050
+ const value = unwrapExpression(entry.value);
5051
+ return value.type === "Literal" && value.value === null ? [] : null;
5052
+ }
5053
+ return [[key, entry]];
5054
+ }
5055
+ function literalPropertyKey(node) {
5056
+ const value = unwrapExpression(node);
5057
+ const literal = literalString(value);
5058
+ if (literal !== null) return literal;
5059
+ if (value.type !== "TemplateLiteral" || value.expressions.length !== 0) return null;
5060
+ return value.quasis[0]?.value.cooked ?? null;
5061
+ }
5062
+ function spreadOnly(node) {
5063
+ let current = outerExpression(node);
5064
+ while (current.parent !== void 0) {
5065
+ const parent = current.parent;
5066
+ if (parent.type === "SpreadElement") return parent.parent.type === "ObjectExpression";
5067
+ if (![
5068
+ "Property",
5069
+ "ObjectExpression",
5070
+ "ConditionalExpression",
5071
+ "LogicalExpression",
5072
+ "SequenceExpression",
5073
+ "TSAsExpression",
5074
+ "TSSatisfiesExpression",
5075
+ "TSNonNullExpression",
5076
+ "TSTypeAssertion"
5077
+ ].includes(parent.type)) return false;
5078
+ current = parent;
5079
+ }
5080
+ return false;
5081
+ }
5045
5082
 
5046
5083
  // src/rules/no-duplicate-lifecycle-refresh-listeners.ts
5047
5084
  var import_utils30 = require("@typescript-eslint/utils");
@@ -5300,7 +5337,7 @@ var NO_DANGEROUSLY_ALLOW_SVG_DOCUMENTATION = {
5300
5337
  }
5301
5338
  ]
5302
5339
  };
5303
- function literalString(node) {
5340
+ function literalString2(node) {
5304
5341
  if (node?.value.type === "Literal" && typeof node.value.value === "string") return node.value.value;
5305
5342
  if (node?.value.type === "TemplateLiteral" && node.value.expressions.length === 0) return node.value.quasis[0]?.value.cooked ?? null;
5306
5343
  return null;
@@ -5332,10 +5369,10 @@ var no_dangerously_allow_svg_default = createRule({
5332
5369
  const node = exportedNextConfigProperty(context.sourceCode, ["images", "dangerouslyAllowSVG"]);
5333
5370
  if (node !== null && node.value.type === "Literal" && node.value.value === true) {
5334
5371
  const disposition = exportedNextConfigProperty(context.sourceCode, ["images", "contentDispositionType"]);
5335
- const policy = literalString(
5372
+ const policy = literalString2(
5336
5373
  exportedNextConfigProperty(context.sourceCode, ["images", "contentSecurityPolicy"])
5337
5374
  );
5338
- const attachmentDisposition = disposition === null || literalString(disposition) === "attachment";
5375
+ const attachmentDisposition = disposition === null || literalString2(disposition) === "attachment";
5339
5376
  if (attachmentDisposition && policy !== null && hasHardenedSvgPolicy(policy)) return;
5340
5377
  context.report({ node, messageId: "noDangerouslyAllowSvg" });
5341
5378
  }
@@ -5456,31 +5493,48 @@ function sqlTextOf(node) {
5456
5493
  const right = sqlTextOf(node.right);
5457
5494
  return left !== null && right !== null ? left + right : null;
5458
5495
  }
5459
- case import_utils32.AST_NODE_TYPES.ArrayExpression: {
5460
- const parts = [];
5461
- for (const element of node.elements) {
5462
- if (element === null) {
5463
- return null;
5464
- }
5465
- const part = sqlTextOf(element);
5466
- if (part === null) {
5467
- return null;
5468
- }
5469
- parts.push(part);
5470
- }
5471
- return parts.length > 0 ? parts.join(" ") : null;
5472
- }
5496
+ case import_utils32.AST_NODE_TYPES.ArrayExpression:
5497
+ return fragmentArrayText(node);
5473
5498
  default:
5474
5499
  return null;
5475
5500
  }
5476
5501
  }
5477
- function isJoinedFragmentArray(node) {
5502
+ function fragmentArrayText(node) {
5503
+ const join4 = joinedFragmentCall(node);
5504
+ const separator = join4 === null ? " " : join4.arguments.length === 0 ? "," : join4.arguments.length === 1 ? staticString(join4.arguments[0]) : null;
5505
+ if (separator === null) return null;
5506
+ const parts = [];
5507
+ for (const element of node.elements) {
5508
+ if (element === null || join4 !== null && element.type === import_utils32.AST_NODE_TYPES.ArrayExpression) {
5509
+ return null;
5510
+ }
5511
+ const part = sqlTextOf(element);
5512
+ if (part === null) {
5513
+ return null;
5514
+ }
5515
+ parts.push(part);
5516
+ }
5517
+ return parts.length > 0 ? parts.join(separator) : null;
5518
+ }
5519
+ function joinedFragmentCall(node) {
5478
5520
  const parent = node.parent;
5479
- return parent?.type === import_utils32.AST_NODE_TYPES.MemberExpression && parent.object === node && !parent.computed && parent.property.type === import_utils32.AST_NODE_TYPES.Identifier && parent.property.name === "join" && parent.parent?.type === import_utils32.AST_NODE_TYPES.CallExpression;
5521
+ return parent?.type === import_utils32.AST_NODE_TYPES.MemberExpression && parent.object === node && !parent.computed && parent.property.type === import_utils32.AST_NODE_TYPES.Identifier && parent.property.name === "join" && parent.parent?.type === import_utils32.AST_NODE_TYPES.CallExpression && parent.parent.callee === parent ? parent.parent : null;
5480
5522
  }
5481
5523
  function markConsumed(node, consumed) {
5482
5524
  consumed.add(node);
5483
- forEachOwnAstChild(node, (child) => markConsumed(child, consumed));
5525
+ const value = unwrapExpression(node);
5526
+ if (value !== node) {
5527
+ markConsumed(value, consumed);
5528
+ return;
5529
+ }
5530
+ if (node.type === import_utils32.AST_NODE_TYPES.BinaryExpression && node.operator === "+") {
5531
+ markConsumed(node.left, consumed);
5532
+ markConsumed(node.right, consumed);
5533
+ } else if (node.type === import_utils32.AST_NODE_TYPES.ArrayExpression) {
5534
+ for (const element of node.elements) if (element !== null) markConsumed(element, consumed);
5535
+ } else if (node.type === import_utils32.AST_NODE_TYPES.TaggedTemplateExpression) {
5536
+ markConsumed(node.quasi, consumed);
5537
+ }
5484
5538
  }
5485
5539
  function createSqlListener(handler) {
5486
5540
  const consumed = /* @__PURE__ */ new WeakSet();
@@ -5500,9 +5554,12 @@ function createSqlListener(handler) {
5500
5554
  visit(node);
5501
5555
  },
5502
5556
  ArrayExpression: (node) => {
5503
- if (isJoinedFragmentArray(node)) {
5504
- visit(node);
5505
- }
5557
+ const join4 = joinedFragmentCall(node);
5558
+ if (join4 === null) return;
5559
+ visit(node);
5560
+ markConsumed(node, consumed);
5561
+ const separator = join4.arguments[0];
5562
+ if (separator !== void 0 && staticString(separator) !== null) markConsumed(separator, consumed);
5506
5563
  },
5507
5564
  TemplateLiteral: (node) => {
5508
5565
  visit(node);
@@ -5533,7 +5590,7 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
5533
5590
  remediation: "Bind data values through SQL placeholders. Select non-bindable identifiers or clauses from fixed, reviewed fragments instead of interpolating runtime text.",
5534
5591
  category: "security",
5535
5592
  limitations: [
5536
- "Template and concatenation fragments are inspected at recognized execution calls. Double-quoted identifiers, comments, and dollar strings are excluded; this is not a general SQL injection detector.",
5593
+ "Template and concatenation fragments are inspected at recognized execution calls, including runtime pieces surrounded by literal text inside a quoted value. Double-quoted identifiers, comments, and dollar strings are excluded; this is not a general SQL injection detector.",
5537
5594
  "Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.",
5538
5595
  "The bounded lexer recognizes doubled quotes, comments, and PostgreSQL dollar strings; dialect-specific escape modes and SQL generated through other APIs require separate security review."
5539
5596
  ],
@@ -5581,6 +5638,7 @@ var NO_DYNAMIC_SQL_DOCUMENTATION = {
5581
5638
  var DEFAULT_METHODS = ["prepare", "exec", "query"];
5582
5639
  var CONSTANT_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
5583
5640
  function isStaticFragment(expression) {
5641
+ expression = unwrapExpression(expression);
5584
5642
  if (expression.type === import_utils33.AST_NODE_TYPES.Identifier) {
5585
5643
  return CONSTANT_CASE_RE.test(expression.name);
5586
5644
  }
@@ -5608,28 +5666,13 @@ function runtimeInterpolations(template) {
5608
5666
  const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
5609
5667
  offset += RUNTIME_MARKER.length;
5610
5668
  if (isStaticFragment(expression)) return [];
5611
- if (inValue && endsWithSqlQuote(parts[index] ?? "") && startsWithSqlQuote(parts[index + 1] ?? "")) {
5669
+ if (inValue) {
5612
5670
  return [{ expression, messageId: "dynamicSql" }];
5613
5671
  }
5614
5672
  return unquoted ? [{ expression, messageId: "dynamicFragment" }] : [];
5615
5673
  }
5616
5674
  );
5617
5675
  }
5618
- function endsWithSqlQuote(text) {
5619
- return /'\s*$/u.test(text);
5620
- }
5621
- function startsWithSqlQuote(text) {
5622
- return /^\s*'/u.test(text);
5623
- }
5624
- function staticLiteralText(node) {
5625
- if (node.type === import_utils33.AST_NODE_TYPES.Literal && typeof node.value === "string") {
5626
- return node.value;
5627
- }
5628
- if (node.type === import_utils33.AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0) {
5629
- return node.quasis[0]?.value.raw;
5630
- }
5631
- return void 0;
5632
- }
5633
5676
  function runtimeConcatOperands(node) {
5634
5677
  if (node.type !== import_utils33.AST_NODE_TYPES.BinaryExpression || node.operator !== "+") {
5635
5678
  return [];
@@ -5641,7 +5684,7 @@ function runtimeConcatOperands(node) {
5641
5684
  if (!hasStringLiteral) {
5642
5685
  return [];
5643
5686
  }
5644
- const parts = operands.map((operand) => staticLiteralText(operand) ?? RUNTIME_MARKER);
5687
+ const parts = operands.map((operand) => staticString(operand) ?? RUNTIME_MARKER);
5645
5688
  const statement = parts.join("");
5646
5689
  const ranges = sqlSingleQuotedRanges(statement);
5647
5690
  const visible = stripSqlNoise(statement);
@@ -5651,9 +5694,7 @@ function runtimeConcatOperands(node) {
5651
5694
  const unquoted = visible.slice(offset, offset + RUNTIME_MARKER.length) === RUNTIME_MARKER;
5652
5695
  offset += parts[index]?.length ?? 0;
5653
5696
  if (isStaticFragment(operand)) return [];
5654
- const before = operands[index - 1];
5655
- const after = operands[index + 1];
5656
- if (inValue && before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "")) {
5697
+ if (inValue) {
5657
5698
  return [{ expression: operand, messageId: "dynamicSql" }];
5658
5699
  }
5659
5700
  return unquoted ? [{ expression: operand, messageId: "dynamicFragment" }] : [];
@@ -8721,7 +8762,7 @@ var LOGGING_OPTION_PROPERTIES = {
8721
8762
  loggerNames: { type: "array", items: { type: "string" } },
8722
8763
  logFunctions: { type: "array", items: { type: "string" } }
8723
8764
  };
8724
- function calleeName2(callee) {
8765
+ function calleeName(callee) {
8725
8766
  callee = unwrapExpression(callee);
8726
8767
  if (callee.type === "Identifier") {
8727
8768
  return callee.name;
@@ -8769,7 +8810,7 @@ function createLogMatcher(options = {}) {
8769
8810
  if (expr.type !== "CallExpression" || logFunctions.size === 0) {
8770
8811
  return false;
8771
8812
  }
8772
- const name = calleeName2(expr.callee);
8813
+ const name = calleeName(expr.callee);
8773
8814
  return name !== null && logFunctions.has(name);
8774
8815
  }
8775
8816
  function isLoggingCall(expr) {
@@ -11555,7 +11596,7 @@ function isRawBlobName(name) {
11555
11596
  const last = tokens.at(-1);
11556
11597
  return last !== void 0 && RAW_BLOB_WORDS.has(last);
11557
11598
  }
11558
- function propertyKeyName2(prop) {
11599
+ function propertyKeyName(prop) {
11559
11600
  if (prop.computed) {
11560
11601
  return null;
11561
11602
  }
@@ -11626,12 +11667,12 @@ var no_secret_in_log_default = createRule({
11626
11667
  continue;
11627
11668
  }
11628
11669
  for (const property of literalProperties(entry.argument)) {
11629
- const key2 = propertyKeyName2(property);
11670
+ const key2 = propertyKeyName(property);
11630
11671
  if (key2 !== null) effective.set(key2, property);
11631
11672
  }
11632
11673
  continue;
11633
11674
  }
11634
- const key = propertyKeyName2(entry);
11675
+ const key = propertyKeyName(entry);
11635
11676
  if (key === null) {
11636
11677
  effective.clear();
11637
11678
  continue;
@@ -11652,7 +11693,7 @@ var no_secret_in_log_default = createRule({
11652
11693
  return false;
11653
11694
  }
11654
11695
  function reportSecretProperty(prop) {
11655
- const keyName = propertyKeyName2(prop);
11696
+ const keyName = propertyKeyName(prop);
11656
11697
  const value = valueName(prop.value);
11657
11698
  if (value !== null && hasRedactionMarker(value)) return false;
11658
11699
  const name = value !== null && isSecretKeyword(value) ? value : keyName;
@@ -12220,7 +12261,7 @@ var no_sentinel_return_on_catch_default = createRule({
12220
12261
  if (matcher.isLoggingCall(current)) {
12221
12262
  return caughtName === null || argsIncludeBinding(current.arguments, caughtName);
12222
12263
  }
12223
- const name = calleeName2(current.callee);
12264
+ const name = calleeName(current.callee);
12224
12265
  return name !== null && REPORT_NAME_RE.test(name) && (caughtName === null || argsIncludeBinding(current.arguments, caughtName));
12225
12266
  });
12226
12267
  }
@@ -25312,6 +25353,7 @@ var LIBRARY_IMPORT_POLICY = ["error", {
25312
25353
  var ADVISORY_RULES = [
25313
25354
  "@sarj/excessive-commentary",
25314
25355
  "@sarj/no-bespoke-api-case-conversion",
25356
+ "@sarj/no-cors-wildcard-with-credentials",
25315
25357
  "@sarj/no-exception-text-in-5xx-response",
25316
25358
  "@sarj/no-restated-comment",
25317
25359
  "@sarj/no-restated-jsdoc",
@@ -25349,7 +25391,7 @@ var RECOMMENDED_RULES = {
25349
25391
  "@sarj/enforce-file-structure": "error",
25350
25392
  "@sarj/no-client-side-data-fetching": "error",
25351
25393
  "@sarj/no-comment-cruft": "error",
25352
- "@sarj/no-cors-wildcard-with-credentials": "error",
25394
+ "@sarj/no-cors-wildcard-with-credentials": "warn",
25353
25395
  "@sarj/no-dynamic-sql": "error",
25354
25396
  "@sarj/no-fat-try-blocks": ["error", { max: 5 }],
25355
25397
  "@sarj/no-hand-rolled-sleep": "error",
@@ -25468,7 +25510,7 @@ var STRICT_RULES = {
25468
25510
  "@sarj/enforce-file-structure": "error",
25469
25511
  "@sarj/no-client-side-data-fetching": "error",
25470
25512
  "@sarj/no-comment-cruft": "error",
25471
- "@sarj/no-cors-wildcard-with-credentials": "error",
25513
+ "@sarj/no-cors-wildcard-with-credentials": "warn",
25472
25514
  "@sarj/no-dynamic-sql": "error",
25473
25515
  "@sarj/no-enum": "error",
25474
25516
  "@sarj/no-fat-try-blocks": ["error", { max: 5 }],