@sarj/eslint-plugin 15.0.0 → 15.1.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/index.cjs CHANGED
@@ -2062,12 +2062,13 @@ function createSqlListener(handler) {
2062
2062
 
2063
2063
  // src/rules/no-dynamic-sql.ts
2064
2064
  var noDynamicSqlDocumentation = {
2065
- summary: "Disallow runtime interpolation or concatenation in SQL passed to statement-execution methods.",
2065
+ summary: "Disallow runtime values embedded inside quoted SQL values passed to statement-execution methods.",
2066
2066
  rationale: "Embedding runtime values in SQL bypasses driver parameterization and can introduce injection defects or unstable query plans.",
2067
2067
  remediation: "Use SQL placeholders and pass runtime values through the driver's binding API.",
2068
2068
  category: "security",
2069
2069
  limitations: [
2070
- "The rule recognizes SQL by syntax and configured method names; static fragments and parameterizing tagged templates are exempt."
2070
+ "The rule reports only visibly quoted runtime values; dynamic identifiers and unquoted fragments require provenance that syntax-only linting cannot prove.",
2071
+ "Static fragments and parameterizing tagged templates are exempt."
2071
2072
  ],
2072
2073
  examples: [
2073
2074
  {
@@ -2102,27 +2103,48 @@ function isStaticFragment(expression) {
2102
2103
  if (expression.type === import_utils9.AST_NODE_TYPES.Literal) {
2103
2104
  return typeof expression.value === "string";
2104
2105
  }
2106
+ if (expression.type === import_utils9.AST_NODE_TYPES.TemplateLiteral) {
2107
+ return expression.expressions.length === 0;
2108
+ }
2105
2109
  return false;
2106
2110
  }
2107
2111
  function runtimeInterpolations(template) {
2108
2112
  return template.expressions.filter(
2109
- (expression) => !isStaticFragment(expression)
2113
+ (expression, index) => !isStaticFragment(expression) && endsWithSqlQuote(template.quasis[index]?.value.raw ?? "") && startsWithSqlQuote(template.quasis[index + 1]?.value.raw ?? "")
2110
2114
  );
2111
2115
  }
2116
+ function endsWithSqlQuote(text) {
2117
+ return /['"]\s*$/u.test(text);
2118
+ }
2119
+ function startsWithSqlQuote(text) {
2120
+ return /^\s*['"]/u.test(text);
2121
+ }
2122
+ function staticLiteralText(node) {
2123
+ if (node.type === import_utils9.AST_NODE_TYPES.Literal && typeof node.value === "string") {
2124
+ return node.value;
2125
+ }
2126
+ if (node.type === import_utils9.AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0) {
2127
+ return node.quasis[0]?.value.raw;
2128
+ }
2129
+ return void 0;
2130
+ }
2112
2131
  function runtimeConcatOperands(node) {
2113
2132
  if (node.type !== import_utils9.AST_NODE_TYPES.BinaryExpression || node.operator !== "+") {
2114
2133
  return [];
2115
2134
  }
2116
2135
  const operands = concatOperands(node);
2117
2136
  const hasStringLiteral = operands.some(
2118
- (operand) => operand.type === import_utils9.AST_NODE_TYPES.Literal && typeof operand.value === "string"
2137
+ (operand) => operand.type === import_utils9.AST_NODE_TYPES.Literal && typeof operand.value === "string" || operand.type === import_utils9.AST_NODE_TYPES.TemplateLiteral && operand.expressions.length === 0
2119
2138
  );
2120
2139
  if (!hasStringLiteral) {
2121
2140
  return [];
2122
2141
  }
2123
- return operands.filter(
2124
- (operand) => operand.type !== import_utils9.AST_NODE_TYPES.Literal && !isStaticFragment(operand)
2125
- );
2142
+ return operands.filter((operand, index) => {
2143
+ if (isStaticFragment(operand)) return false;
2144
+ const before = operands[index - 1];
2145
+ const after = operands[index + 1];
2146
+ return before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "");
2147
+ });
2126
2148
  }
2127
2149
  function concatOperands(node) {
2128
2150
  if (node.type === import_utils9.AST_NODE_TYPES.BinaryExpression && node.operator === "+") {
@@ -2161,7 +2183,7 @@ var no_dynamic_sql_default = createRule({
2161
2183
  meta: {
2162
2184
  type: "problem",
2163
2185
  docs: {
2164
- description: "Disallow runtime interpolation or concatenation in SQL passed to statement-execution methods."
2186
+ description: noDynamicSqlDocumentation.summary
2165
2187
  },
2166
2188
  schema: [
2167
2189
  {
@@ -2177,7 +2199,7 @@ var no_dynamic_sql_default = createRule({
2177
2199
  }
2178
2200
  ],
2179
2201
  messages: {
2180
- dynamicSql: "Runtime value built into a SQL statement passed to `{{method}}()`. Use a `?` placeholder and pass the value through `.bind(...)` so the driver parameterises it."
2202
+ dynamicSql: "Runtime value embedded inside a quoted SQL value passed to `{{method}}()`. Replace the quoted interpolation with a placeholder and bind the value separately."
2181
2203
  }
2182
2204
  },
2183
2205
  defaultOptions: [{}],
@@ -3001,8 +3023,59 @@ var noInsecureRandomIdDocumentation = {
3001
3023
  { id: "predictable-token", title: "Do not derive a token from Math.random", outcome: "match", files: [{ path: "src/session.ts", source: "const sessionToken = Math.random();" }], focusPath: "src/session.ts", expectedCount: 1, public: true }
3002
3024
  ]
3003
3025
  };
3004
- var STRONG_SECURITY_PATTERN = /token|secret|csrf|password|passwd|apikey|api[-_]?key|nonce|salt|uuid|authid/i;
3005
- var NON_SECURITY_ID_PATTERN = /temp|tmp|cache|correlation|request|req|trace|execution|dev|hmr|mock|test|perf|marker|dialog|select|menu|tab|field|input|form|element|dom|aria|component/i;
3026
+ var STRONG_SECURITY_WORDS = /* @__PURE__ */ new Set([
3027
+ "apikey",
3028
+ "csrf",
3029
+ "nonce",
3030
+ "otp",
3031
+ "password",
3032
+ "passwd",
3033
+ "pin",
3034
+ "salt",
3035
+ "secret",
3036
+ "token",
3037
+ "uuid",
3038
+ "verificationcode"
3039
+ ]);
3040
+ var NON_SECURITY_ID_WORDS = /* @__PURE__ */ new Set([
3041
+ "aria",
3042
+ "cache",
3043
+ "component",
3044
+ "correlation",
3045
+ "dev",
3046
+ "dialog",
3047
+ "dom",
3048
+ "element",
3049
+ "execution",
3050
+ "field",
3051
+ "form",
3052
+ "hmr",
3053
+ "input",
3054
+ "marker",
3055
+ "menu",
3056
+ "mock",
3057
+ "perf",
3058
+ "req",
3059
+ "request",
3060
+ "select",
3061
+ "tab",
3062
+ "temp",
3063
+ "test",
3064
+ "tmp",
3065
+ "trace"
3066
+ ]);
3067
+ function nameWords(name) {
3068
+ return name.replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2").split(/[^A-Za-z0-9]+/u).filter(Boolean).map((word) => word.toLowerCase());
3069
+ }
3070
+ function isStrongSecurityName(name) {
3071
+ const words = nameWords(name);
3072
+ return words.some((word) => STRONG_SECURITY_WORDS.has(word)) || words.some(
3073
+ (word, index) => word === "api" && words[index + 1] === "key" || word === "auth" && words[index + 1] === "id" || word === "verification" && words[index + 1] === "code"
3074
+ );
3075
+ }
3076
+ function isNonSecurityName(name) {
3077
+ return nameWords(name).some((word) => NON_SECURITY_ID_WORDS.has(word));
3078
+ }
3006
3079
  var PATH_OR_DOM_MARKER = /[\\/#]|\.[A-Za-z0-9]/;
3007
3080
  function isMathRandomCall(node) {
3008
3081
  if (node.type !== "CallExpression") {
@@ -3015,70 +3088,57 @@ function isMathRandomCall(node) {
3015
3088
  const { object, property } = callee;
3016
3089
  return object.type === "Identifier" && object.name === "Math" && property.type === "Identifier" && property.name === "random";
3017
3090
  }
3018
- function isPartOfToString36Chain(node) {
3019
- let current = node;
3020
- let parent = current.parent;
3021
- while (parent) {
3022
- if (parent.type === "MemberExpression" && parent.object === current && !parent.computed && parent.property.type === "Identifier" && parent.property.name === "toString") {
3023
- const grandparent = parent.parent;
3024
- if (grandparent && grandparent.type === "CallExpression" && grandparent.callee === parent) {
3025
- const firstArg = grandparent.arguments[0];
3026
- if (firstArg && firstArg.type === "Literal" && firstArg.value === 36) {
3027
- return true;
3028
- }
3029
- }
3030
- }
3031
- if (parent.type === "MemberExpression" && parent.object === current) {
3032
- current = parent;
3033
- parent = current.parent;
3034
- continue;
3035
- }
3036
- if (parent.type === "CallExpression" && parent.callee === current) {
3037
- current = parent;
3038
- parent = current.parent;
3039
- continue;
3040
- }
3041
- break;
3042
- }
3043
- return false;
3044
- }
3045
- function findEnclosingName(node) {
3091
+ function findEnclosingNames(node) {
3092
+ const names = [];
3093
+ let directBinding = true;
3046
3094
  let current = node;
3047
3095
  let parent = current.parent;
3048
3096
  while (parent) {
3049
3097
  if (parent.type === "VariableDeclarator" && parent.init === current) {
3050
- if (parent.id.type === "Identifier") {
3051
- return parent.id.name;
3098
+ if (directBinding && parent.id.type === "Identifier") {
3099
+ names.push(parent.id.name);
3052
3100
  }
3053
- return void 0;
3054
3101
  }
3055
3102
  if (parent.type === "Property" && parent.value === current) {
3056
3103
  const key = parent.key;
3057
3104
  if (!parent.computed && key.type === "Identifier") {
3058
- return key.name;
3105
+ names.push(key.name);
3059
3106
  }
3060
3107
  if (key.type === "Literal" && typeof key.value === "string") {
3061
- return key.value;
3108
+ names.push(key.value);
3062
3109
  }
3063
- return void 0;
3110
+ directBinding = false;
3064
3111
  }
3065
3112
  if (parent.type === "PropertyDefinition" && parent.value === current) {
3066
3113
  const key = parent.key;
3067
3114
  if (!parent.computed && key.type === "Identifier") {
3068
- return key.name;
3115
+ names.push(key.name);
3069
3116
  }
3070
3117
  if (key.type === "Literal" && typeof key.value === "string") {
3071
- return key.value;
3118
+ names.push(key.value);
3072
3119
  }
3073
- return void 0;
3120
+ directBinding = false;
3074
3121
  }
3075
- if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression" || parent.type === "BlockStatement" || parent.type === "ReturnStatement" || parent.type === "ExpressionStatement") {
3076
- return void 0;
3122
+ if (parent.type === "AssignmentExpression" && parent.right === current) {
3123
+ if (directBinding && parent.left.type === "Identifier") names.push(parent.left.name);
3124
+ if (directBinding && parent.left.type === "MemberExpression" && !parent.left.computed && parent.left.property.type === "Identifier") {
3125
+ names.push(parent.left.property.name);
3126
+ }
3127
+ }
3128
+ if (parent.type === "ObjectExpression" || parent.type === "ArrayExpression") {
3129
+ directBinding = false;
3130
+ }
3131
+ if (parent.type === "FunctionDeclaration") {
3132
+ if (directBinding && parent.id !== null) names.push(parent.id.name);
3133
+ return names;
3134
+ }
3135
+ if (parent.type === "ExpressionStatement") {
3136
+ return names;
3077
3137
  }
3078
3138
  current = parent;
3079
3139
  parent = current.parent;
3080
3140
  }
3081
- return void 0;
3141
+ return names;
3082
3142
  }
3083
3143
  function isConcatenatedIntoPathOrDomId(node) {
3084
3144
  const valueNode = climbValueChain(node);
@@ -3164,20 +3224,17 @@ var no_insecure_random_id_default = createRule({
3164
3224
  if (!isMathRandomCall(node)) {
3165
3225
  return;
3166
3226
  }
3167
- const name = findEnclosingName(node);
3168
- if (name !== void 0 && STRONG_SECURITY_PATTERN.test(name)) {
3227
+ const names = findEnclosingNames(node);
3228
+ if (names.some(isStrongSecurityName)) {
3169
3229
  context.report({ node, messageId: "insecureRandomId" });
3170
3230
  return;
3171
3231
  }
3172
- if (name !== void 0 && NON_SECURITY_ID_PATTERN.test(name)) {
3232
+ if (names.some(isNonSecurityName)) {
3173
3233
  return;
3174
3234
  }
3175
3235
  if (isConcatenatedIntoPathOrDomId(node)) {
3176
3236
  return;
3177
3237
  }
3178
- if (isPartOfToString36Chain(node)) {
3179
- context.report({ node, messageId: "insecureRandomId" });
3180
- }
3181
3238
  }
3182
3239
  };
3183
3240
  }
@@ -4839,7 +4896,8 @@ var DEFAULT_ALLOW = [
4839
4896
  "[\\\\/](connectors|providers|integrations|adapters|fetchers)[\\\\/]",
4840
4897
  "[\\\\/]notifications[\\\\/]",
4841
4898
  "[Ss]ervice\\.[cm]?[jt]sx?$",
4842
- "-(service|connector|adapter|sdk|fetcher)\\.[cm]?[jt]sx?$"
4899
+ "-(service|connector|adapter|sdk|fetcher)\\.[cm]?[jt]sx?$",
4900
+ "[\\\\/][^\\\\/]*(?:Client|client)\\.[cm]?[jt]sx?$"
4843
4901
  ];
4844
4902
  var NON_PRODUCTION_TREE_RE = /[\\/](playwright|cypress|__testfixtures__)[\\/]/;
4845
4903
  var GLOBAL_RECEIVERS = /* @__PURE__ */ new Set([
@@ -4973,7 +5031,7 @@ var no_raw_fetch_outside_clients_default = createRule({
4973
5031
  defaultOptions: [{}],
4974
5032
  create(context, [options]) {
4975
5033
  const filename = context.filename;
4976
- if (isTestFile(filename) || NON_PRODUCTION_TREE_RE.test(filename)) {
5034
+ if (isTestFile(filename) || isScriptFile(filename) || NON_PRODUCTION_TREE_RE.test(filename)) {
4977
5035
  return {};
4978
5036
  }
4979
5037
  const patterns = options?.allow ?? DEFAULT_ALLOW;
@@ -5013,10 +5071,15 @@ var no_raw_fetch_outside_clients_default = createRule({
5013
5071
  function isInternalApiUrl(node) {
5014
5072
  const resolved = resolveNode2(node ?? void 0);
5015
5073
  if (resolved?.type === import_utils25.AST_NODE_TYPES.Literal) {
5016
- return typeof resolved.value === "string" && resolved.value.startsWith("/api/");
5074
+ return typeof resolved.value === "string" && /^\/(?!\/)(?:[^/]+\/)*api(?:\/|$)/.test(resolved.value);
5017
5075
  }
5018
5076
  if (resolved?.type === import_utils25.AST_NODE_TYPES.TemplateLiteral) {
5019
- return resolved.quasis[0]?.value.cooked?.startsWith("/api/") === true;
5077
+ const prefix = resolved.quasis[0]?.value.cooked;
5078
+ return typeof prefix === "string" && /^\/(?!\/)(?:[^/]+\/)*api(?:\/|$)/.test(prefix);
5079
+ }
5080
+ if (resolved?.type === import_utils25.AST_NODE_TYPES.CallExpression && resolved.callee.type === import_utils25.AST_NODE_TYPES.Identifier && resolved.callee.name === "withBase") {
5081
+ const first = resolved.arguments[0];
5082
+ return first !== void 0 && first.type !== import_utils25.AST_NODE_TYPES.SpreadElement ? isInternalApiUrl(first) : false;
5020
5083
  }
5021
5084
  return resolved?.type === import_utils25.AST_NODE_TYPES.BinaryExpression && resolved.operator === "+" && isInternalApiUrl(resolved.left);
5022
5085
  }
@@ -5860,12 +5923,17 @@ var BLOB_REDACTION_TOKENS = /* @__PURE__ */ new Set([
5860
5923
  "public"
5861
5924
  ]);
5862
5925
  function rawBlobValueName(value) {
5926
+ if (value.type === "AwaitExpression") return rawBlobValueName(value.argument);
5927
+ if (value.type === "ChainExpression") return rawBlobValueName(value.expression);
5863
5928
  if (value.type === "Identifier") {
5864
5929
  return isRawBlobName(value.name) ? value.name : null;
5865
5930
  }
5866
5931
  if (value.type === "MemberExpression" && !value.computed && value.property.type === "Identifier") {
5867
5932
  return isRawBlobName(value.property.name) ? value.property.name : null;
5868
5933
  }
5934
+ if (value.type === "CallExpression" && value.arguments.length === 0 && value.callee.type === "MemberExpression" && !value.callee.computed && value.callee.object.type === "Identifier" && /^(?:res|response|\w+Response)$/.test(value.callee.object.name) && value.callee.property.type === "Identifier" && (value.callee.property.name === "json" || value.callee.property.name === "text")) {
5935
+ return `${value.callee.object.name}.${value.callee.property.name}()`;
5936
+ }
5869
5937
  return null;
5870
5938
  }
5871
5939
  function isRawBlobName(name) {
@@ -6530,6 +6598,9 @@ var isExplanatory = (comment) => !DIRECTIVE_COMMENT_RE.test(comment.value);
6530
6598
  function isTeardownCall(node) {
6531
6599
  return node.type === import_utils33.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils33.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils33.AST_NODE_TYPES.Identifier && TEARDOWN_METHODS.has(node.callee.property.name);
6532
6600
  }
6601
+ function isCancelledWebShare(node) {
6602
+ return node.type === import_utils33.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils33.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === import_utils33.AST_NODE_TYPES.Identifier && node.callee.object.name === "navigator" && node.callee.property.type === import_utils33.AST_NODE_TYPES.Identifier && node.callee.property.name === "share";
6603
+ }
6533
6604
  function isSilentHandler(handler) {
6534
6605
  const body2 = handler.body;
6535
6606
  if (body2.type !== import_utils33.AST_NODE_TYPES.BlockStatement) {
@@ -6579,7 +6650,7 @@ var no_silent_promise_catch_default = createRule({
6579
6650
  },
6580
6651
  defaultOptions: [],
6581
6652
  create(context) {
6582
- if (isTestFile(context.filename)) {
6653
+ if (isTestFile(context.filename) || isScriptFile(context.filename)) {
6583
6654
  return {};
6584
6655
  }
6585
6656
  const hasExplanatoryComment = (call, handler) => {
@@ -6612,7 +6683,10 @@ var no_silent_promise_catch_default = createRule({
6612
6683
  if (isTeardownCall(node.callee.object)) {
6613
6684
  return;
6614
6685
  }
6615
- if (node.parent.type === import_utils33.AST_NODE_TYPES.MemberExpression && node.parent.object === node) {
6686
+ if (isCancelledWebShare(node.callee.object)) {
6687
+ return;
6688
+ }
6689
+ if (node.parent.type === import_utils33.AST_NODE_TYPES.MemberExpression && node.parent.object === node && !node.parent.computed && node.parent.property.type === import_utils33.AST_NODE_TYPES.Identifier && node.parent.property.name === "then") {
6616
6690
  return;
6617
6691
  }
6618
6692
  const expectedArguments = method === "catch" ? 1 : 2;
@@ -6924,7 +6998,20 @@ function isStringInitializedVariable(variable) {
6924
6998
  if (declarator.type !== "VariableDeclarator") {
6925
6999
  return false;
6926
7000
  }
6927
- return isStringLiteralInit(declarator.init);
7001
+ if (isStringLiteralInit(declarator.init)) return true;
7002
+ if (declarator.id.type === "Identifier" && declarator.id.typeAnnotation?.typeAnnotation.type === "TSStringKeyword") {
7003
+ return true;
7004
+ }
7005
+ return isTemplateStringsArrayElement(declarator.init, variable.scope);
7006
+ }
7007
+ function isTemplateStringsArrayElement(node, scope) {
7008
+ if (node?.type !== "MemberExpression" || !node.computed || node.object.type !== "Identifier") {
7009
+ return false;
7010
+ }
7011
+ const source = findVariable(scope, node.object.name);
7012
+ if (source?.defs.length !== 1) return false;
7013
+ const name = source.defs[0]?.name;
7014
+ return name?.type === "Identifier" && name.typeAnnotation?.typeAnnotation.type === "TSTypeReference" && name.typeAnnotation.typeAnnotation.typeName.type === "Identifier" && name.typeAnnotation.typeAnnotation.typeName.name === "TemplateStringsArray";
6928
7015
  }
6929
7016
  function isStringLiteralInit(node) {
6930
7017
  if (node === null) {
@@ -6958,12 +7045,13 @@ function isConcatOperand(node, target) {
6958
7045
  }
6959
7046
  return false;
6960
7047
  }
6961
- function isDeclaredInsideLoop(variable, loop) {
7048
+ function isDeclaredInsideLoop(variable, repetition) {
6962
7049
  const def = variable.defs[0];
6963
7050
  if (def === void 0) {
6964
7051
  return false;
6965
7052
  }
6966
- const body2 = loop.body;
7053
+ const body2 = repetition.type === "CallExpression" ? repetition.arguments[0] : repetition.body;
7054
+ if (body2 === void 0 || body2.type === "SpreadElement") return false;
6967
7055
  const [declStart, declEnd] = def.node.range;
6968
7056
  const [bodyStart, bodyEnd] = body2.range;
6969
7057
  return declStart >= bodyStart && declEnd <= bodyEnd;
@@ -6972,6 +7060,9 @@ function enclosingLoop(node) {
6972
7060
  let child = node;
6973
7061
  let parent = node.parent;
6974
7062
  while (parent !== void 0 && parent !== null) {
7063
+ if ((parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") && parent.parent.type === "CallExpression" && parent.parent.arguments[0] === parent && parent.parent.callee.type === "MemberExpression" && !parent.parent.callee.computed && parent.parent.callee.property.type === "Identifier" && parent.parent.callee.property.name === "forEach") {
7064
+ return parent.parent;
7065
+ }
6975
7066
  if (LOOP_NODE_TYPES.has(parent.type)) {
6976
7067
  const loop = parent;
6977
7068
  if (loop.body === child) {
@@ -6983,6 +7074,17 @@ function enclosingLoop(node) {
6983
7074
  }
6984
7075
  return null;
6985
7076
  }
7077
+ function isSmallStaticForLoop(node) {
7078
+ if (node.type !== "ForStatement" || node.init?.type !== "VariableDeclaration" || node.init.declarations.length !== 1 || node.test?.type !== "BinaryExpression" || node.test.operator !== "<" && node.test.operator !== "<=" || node.update?.type !== "UpdateExpression" || node.update.operator !== "++") {
7079
+ return false;
7080
+ }
7081
+ const declaration = node.init.declarations[0];
7082
+ if (declaration?.id.type !== "Identifier" || declaration.init?.type !== "Literal" || typeof declaration.init.value !== "number" || !Number.isInteger(declaration.init.value) || node.test.left.type !== "Identifier" || node.test.left.name !== declaration.id.name || node.test.right.type !== "Literal" || typeof node.test.right.value !== "number" || !Number.isInteger(node.test.right.value) || node.update.argument.type !== "Identifier" || node.update.argument.name !== declaration.id.name) {
7083
+ return false;
7084
+ }
7085
+ const iterations = node.test.right.value - declaration.init.value + (node.test.operator === "<=" ? 1 : 0);
7086
+ return iterations >= 0 && iterations <= 8;
7087
+ }
6986
7088
  var no_string_concat_in_loop_default = createRule({
6987
7089
  name: "no-string-concat-in-loop",
6988
7090
  documentation: noStringConcatInLoopDocumentation,
@@ -7015,6 +7117,9 @@ var no_string_concat_in_loop_default = createRule({
7015
7117
  if (loop === null) {
7016
7118
  return;
7017
7119
  }
7120
+ if (isSmallStaticForLoop(loop)) {
7121
+ return;
7122
+ }
7018
7123
  const scope = context.sourceCode.getScope(node);
7019
7124
  const variable = findVariable(scope, node.left.name);
7020
7125
  if (variable === void 0) {
@@ -8862,8 +8967,10 @@ function looksLikeMutuallyExclusiveState(typeLiteral) {
8862
8967
  let statusMemberCount = 0;
8863
8968
  let hasFailurePayload = false;
8864
8969
  let hasSuccessPayload = false;
8970
+ let hasUnrecognizedMember = false;
8865
8971
  for (const member of typeLiteral.members) {
8866
8972
  if (member.type !== import_utils49.AST_NODE_TYPES.TSPropertySignature) {
8973
+ hasUnrecognizedMember = true;
8867
8974
  continue;
8868
8975
  }
8869
8976
  const name = getMemberName(member);
@@ -8872,15 +8979,18 @@ function looksLikeMutuallyExclusiveState(typeLiteral) {
8872
8979
  continue;
8873
8980
  }
8874
8981
  if (!member.optional || isBooleanTyped(member) || name === null) {
8982
+ hasUnrecognizedMember = true;
8875
8983
  continue;
8876
8984
  }
8877
8985
  if (FAILURE_MEMBER_NAMES.has(name)) {
8878
8986
  hasFailurePayload = true;
8879
8987
  } else if (SUCCESS_PAYLOAD_MEMBER_NAMES.has(name)) {
8880
8988
  hasSuccessPayload = true;
8989
+ } else {
8990
+ hasUnrecognizedMember = true;
8881
8991
  }
8882
8992
  }
8883
- return statusMemberCount === REQUIRED_STATUS_MEMBER_COUNT && hasFailurePayload && hasSuccessPayload;
8993
+ return statusMemberCount === REQUIRED_STATUS_MEMBER_COUNT && hasFailurePayload && (hasSuccessPayload || !hasUnrecognizedMember);
8884
8994
  }
8885
8995
  function getMemberName(member) {
8886
8996
  if (member.type !== import_utils49.AST_NODE_TYPES.TSPropertySignature) {
@@ -10781,7 +10891,7 @@ var prefer_schema_for_api_payload_default = createRule({
10781
10891
  },
10782
10892
  defaultOptions: [],
10783
10893
  create(context) {
10784
- if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
10894
+ if (isTestFile(context.filename) || isScriptFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
10785
10895
  return {};
10786
10896
  }
10787
10897
  const unvalidatedVariables = /* @__PURE__ */ new Set();
@@ -12433,6 +12543,26 @@ var require_fetch_timeout_default = createRule({
12433
12543
  }
12434
12544
  return callee.type === import_utils63.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils63.AST_NODE_TYPES.Identifier && callee.property.name === "fetch" && callee.object.type === import_utils63.AST_NODE_TYPES.Identifier && GLOBAL_OBJECTS2.has(callee.object.name) && resolvesToGlobal(callee.object);
12435
12545
  }
12546
+ function localConstInitProvablyLacksSignal(identifier) {
12547
+ const variable = import_utils63.ASTUtils.findVariable(
12548
+ context.sourceCode.getScope(identifier),
12549
+ identifier.name
12550
+ );
12551
+ if (variable?.defs.length !== 1) return false;
12552
+ const definition = variable.defs[0];
12553
+ if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init?.type !== import_utils63.AST_NODE_TYPES.ObjectExpression || !initProvablyLacksSignal(definition.node.init)) {
12554
+ return false;
12555
+ }
12556
+ for (const reference of variable.references) {
12557
+ const ref = reference.identifier;
12558
+ if (ref === identifier || ref === definition.name) continue;
12559
+ const member = ref.parent;
12560
+ if (member.type !== import_utils63.AST_NODE_TYPES.MemberExpression || member.object !== ref || member.computed || member.property.type !== import_utils63.AST_NODE_TYPES.Identifier || member.property.name === "signal" || member.parent.type !== import_utils63.AST_NODE_TYPES.AssignmentExpression || member.parent.left !== member) {
12561
+ return false;
12562
+ }
12563
+ }
12564
+ return true;
12565
+ }
12436
12566
  return {
12437
12567
  CallExpression(node) {
12438
12568
  if (!isGlobalFetchCall2(node.callee)) {
@@ -12442,7 +12572,7 @@ var require_fetch_timeout_default = createRule({
12442
12572
  if (node.arguments.length === 1 && first !== void 0 && !isInlineUrl(first, resolvesToGlobal)) {
12443
12573
  return;
12444
12574
  }
12445
- if (init === void 0 || initProvablyLacksSignal(init)) {
12575
+ if (init === void 0 || initProvablyLacksSignal(init) || init.type === import_utils63.AST_NODE_TYPES.Identifier && localConstInitProvablyLacksSignal(init)) {
12446
12576
  context.report({ node, messageId: "missingSignal" });
12447
12577
  }
12448
12578
  }
@@ -13197,8 +13327,8 @@ var require_zod_form_validation_default = createRule({
13197
13327
  // src/rules/store-insert-requires-on-conflict.ts
13198
13328
  var import_utils67 = require("@typescript-eslint/utils");
13199
13329
  var storeInsertRequiresOnConflictDocumentation = {
13200
- summary: "Require an embedded SQL INSERT to carry ON CONFLICT; store writes replay under cron re-runs and queue redelivery and must be idempotent upserts.",
13201
- rationale: "A replayed bare insert can duplicate data or fail on a uniqueness constraint.",
13330
+ summary: "Require embedded inserts in explicitly replayable callables to carry conflict handling.",
13331
+ rationale: "A callable named as an enqueue, seed, migration, schedule, ensure, or upsert promises replay safety.",
13202
13332
  remediation: "Add an appropriate `ON CONFLICT` action or supported replay-safe insert form.",
13203
13333
  category: "correctness",
13204
13334
  examples: [
@@ -13207,7 +13337,25 @@ var storeInsertRequiresOnConflictDocumentation = {
13207
13337
  ]
13208
13338
  };
13209
13339
  var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
13210
- var CONFLICT_HANDLED = /\bON\s+CONFLICT\b|\bON\s+DUPLICATE\s+KEY\b|\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b/i;
13340
+ var CONFLICT_HANDLED = /\bON\s+CONFLICT\b|\bON\s+DUPLICATE\s+KEY\b|\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b|\bINSERT\b[\s\S]*?\bSELECT\b[\s\S]*?\bWHERE\s+NOT\s+EXISTS\b/i;
13341
+ var REPLAY_CONTRACT_NAME = /(?:enqueue|ensure|migrate|recordOnce|schedule|seed|upsert|getOrCreate|createIfAbsent|insertIfAbsent)/i;
13342
+ function owningCallableName(node) {
13343
+ for (let current = node.parent; current !== null && current !== void 0; current = current.parent) {
13344
+ if (current.type === "FunctionDeclaration") {
13345
+ return current.id?.name ?? null;
13346
+ }
13347
+ if (current.type === "MethodDefinition") {
13348
+ return current.key.type === "Identifier" ? current.key.name : null;
13349
+ }
13350
+ if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "VariableDeclarator" && current.parent.id.type === "Identifier") {
13351
+ return current.parent.id.name;
13352
+ }
13353
+ if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "Property" && current.parent.key.type === "Identifier") {
13354
+ return current.parent.key.name;
13355
+ }
13356
+ }
13357
+ return null;
13358
+ }
13211
13359
  var INSERT_GATE = /insert/i;
13212
13360
  var store_insert_requires_on_conflict_default = createRule({
13213
13361
  name: "store-insert-requires-on-conflict",
@@ -13215,7 +13363,7 @@ var store_insert_requires_on_conflict_default = createRule({
13215
13363
  meta: {
13216
13364
  type: "problem",
13217
13365
  docs: {
13218
- description: "Require an embedded SQL INSERT to carry ON CONFLICT; store writes replay under cron re-runs and queue redelivery and must be idempotent upserts."
13366
+ description: "Require embedded inserts in explicitly replayable callables to carry conflict handling."
13219
13367
  },
13220
13368
  schema: [],
13221
13369
  messages: {
@@ -13231,6 +13379,10 @@ var store_insert_requires_on_conflict_default = createRule({
13231
13379
  if (!INSERT_WRITE.test(sql) || CONFLICT_HANDLED.test(sql)) {
13232
13380
  return;
13233
13381
  }
13382
+ const owner = owningCallableName(node);
13383
+ if (owner !== null && !REPLAY_CONTRACT_NAME.test(owner)) {
13384
+ return;
13385
+ }
13234
13386
  context.report({ node, messageId: "storeInsertRequiresOnConflict" });
13235
13387
  });
13236
13388
  }
@@ -13917,7 +14069,7 @@ var rules = {
13917
14069
  };
13918
14070
  var meta = {
13919
14071
  name: "@sarj/eslint-plugin",
13920
- version: "15.0.0"
14072
+ version: "15.1.0"
13921
14073
  };
13922
14074
  var applicationOnlyRules = [
13923
14075
  "no-restricted-library-load",
package/dist/index.d.cts CHANGED
@@ -414,7 +414,7 @@ type FlatPreset = {
414
414
  declare const plugin: {
415
415
  readonly meta: {
416
416
  readonly name: "@sarj/eslint-plugin";
417
- readonly version: "15.0.0";
417
+ readonly version: "15.1.0";
418
418
  };
419
419
  readonly rules: {
420
420
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
package/dist/index.d.ts CHANGED
@@ -414,7 +414,7 @@ type FlatPreset = {
414
414
  declare const plugin: {
415
415
  readonly meta: {
416
416
  readonly name: "@sarj/eslint-plugin";
417
- readonly version: "15.0.0";
417
+ readonly version: "15.1.0";
418
418
  };
419
419
  readonly rules: {
420
420
  readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
package/dist/index.js CHANGED
@@ -2022,12 +2022,13 @@ function createSqlListener(handler) {
2022
2022
 
2023
2023
  // src/rules/no-dynamic-sql.ts
2024
2024
  var noDynamicSqlDocumentation = {
2025
- summary: "Disallow runtime interpolation or concatenation in SQL passed to statement-execution methods.",
2025
+ summary: "Disallow runtime values embedded inside quoted SQL values passed to statement-execution methods.",
2026
2026
  rationale: "Embedding runtime values in SQL bypasses driver parameterization and can introduce injection defects or unstable query plans.",
2027
2027
  remediation: "Use SQL placeholders and pass runtime values through the driver's binding API.",
2028
2028
  category: "security",
2029
2029
  limitations: [
2030
- "The rule recognizes SQL by syntax and configured method names; static fragments and parameterizing tagged templates are exempt."
2030
+ "The rule reports only visibly quoted runtime values; dynamic identifiers and unquoted fragments require provenance that syntax-only linting cannot prove.",
2031
+ "Static fragments and parameterizing tagged templates are exempt."
2031
2032
  ],
2032
2033
  examples: [
2033
2034
  {
@@ -2062,27 +2063,48 @@ function isStaticFragment(expression) {
2062
2063
  if (expression.type === AST_NODE_TYPES8.Literal) {
2063
2064
  return typeof expression.value === "string";
2064
2065
  }
2066
+ if (expression.type === AST_NODE_TYPES8.TemplateLiteral) {
2067
+ return expression.expressions.length === 0;
2068
+ }
2065
2069
  return false;
2066
2070
  }
2067
2071
  function runtimeInterpolations(template) {
2068
2072
  return template.expressions.filter(
2069
- (expression) => !isStaticFragment(expression)
2073
+ (expression, index) => !isStaticFragment(expression) && endsWithSqlQuote(template.quasis[index]?.value.raw ?? "") && startsWithSqlQuote(template.quasis[index + 1]?.value.raw ?? "")
2070
2074
  );
2071
2075
  }
2076
+ function endsWithSqlQuote(text) {
2077
+ return /['"]\s*$/u.test(text);
2078
+ }
2079
+ function startsWithSqlQuote(text) {
2080
+ return /^\s*['"]/u.test(text);
2081
+ }
2082
+ function staticLiteralText(node) {
2083
+ if (node.type === AST_NODE_TYPES8.Literal && typeof node.value === "string") {
2084
+ return node.value;
2085
+ }
2086
+ if (node.type === AST_NODE_TYPES8.TemplateLiteral && node.expressions.length === 0) {
2087
+ return node.quasis[0]?.value.raw;
2088
+ }
2089
+ return void 0;
2090
+ }
2072
2091
  function runtimeConcatOperands(node) {
2073
2092
  if (node.type !== AST_NODE_TYPES8.BinaryExpression || node.operator !== "+") {
2074
2093
  return [];
2075
2094
  }
2076
2095
  const operands = concatOperands(node);
2077
2096
  const hasStringLiteral = operands.some(
2078
- (operand) => operand.type === AST_NODE_TYPES8.Literal && typeof operand.value === "string"
2097
+ (operand) => operand.type === AST_NODE_TYPES8.Literal && typeof operand.value === "string" || operand.type === AST_NODE_TYPES8.TemplateLiteral && operand.expressions.length === 0
2079
2098
  );
2080
2099
  if (!hasStringLiteral) {
2081
2100
  return [];
2082
2101
  }
2083
- return operands.filter(
2084
- (operand) => operand.type !== AST_NODE_TYPES8.Literal && !isStaticFragment(operand)
2085
- );
2102
+ return operands.filter((operand, index) => {
2103
+ if (isStaticFragment(operand)) return false;
2104
+ const before = operands[index - 1];
2105
+ const after = operands[index + 1];
2106
+ return before !== void 0 && after !== void 0 && endsWithSqlQuote(staticLiteralText(before) ?? "") && startsWithSqlQuote(staticLiteralText(after) ?? "");
2107
+ });
2086
2108
  }
2087
2109
  function concatOperands(node) {
2088
2110
  if (node.type === AST_NODE_TYPES8.BinaryExpression && node.operator === "+") {
@@ -2121,7 +2143,7 @@ var no_dynamic_sql_default = createRule({
2121
2143
  meta: {
2122
2144
  type: "problem",
2123
2145
  docs: {
2124
- description: "Disallow runtime interpolation or concatenation in SQL passed to statement-execution methods."
2146
+ description: noDynamicSqlDocumentation.summary
2125
2147
  },
2126
2148
  schema: [
2127
2149
  {
@@ -2137,7 +2159,7 @@ var no_dynamic_sql_default = createRule({
2137
2159
  }
2138
2160
  ],
2139
2161
  messages: {
2140
- dynamicSql: "Runtime value built into a SQL statement passed to `{{method}}()`. Use a `?` placeholder and pass the value through `.bind(...)` so the driver parameterises it."
2162
+ dynamicSql: "Runtime value embedded inside a quoted SQL value passed to `{{method}}()`. Replace the quoted interpolation with a placeholder and bind the value separately."
2141
2163
  }
2142
2164
  },
2143
2165
  defaultOptions: [{}],
@@ -2961,8 +2983,59 @@ var noInsecureRandomIdDocumentation = {
2961
2983
  { id: "predictable-token", title: "Do not derive a token from Math.random", outcome: "match", files: [{ path: "src/session.ts", source: "const sessionToken = Math.random();" }], focusPath: "src/session.ts", expectedCount: 1, public: true }
2962
2984
  ]
2963
2985
  };
2964
- var STRONG_SECURITY_PATTERN = /token|secret|csrf|password|passwd|apikey|api[-_]?key|nonce|salt|uuid|authid/i;
2965
- var NON_SECURITY_ID_PATTERN = /temp|tmp|cache|correlation|request|req|trace|execution|dev|hmr|mock|test|perf|marker|dialog|select|menu|tab|field|input|form|element|dom|aria|component/i;
2986
+ var STRONG_SECURITY_WORDS = /* @__PURE__ */ new Set([
2987
+ "apikey",
2988
+ "csrf",
2989
+ "nonce",
2990
+ "otp",
2991
+ "password",
2992
+ "passwd",
2993
+ "pin",
2994
+ "salt",
2995
+ "secret",
2996
+ "token",
2997
+ "uuid",
2998
+ "verificationcode"
2999
+ ]);
3000
+ var NON_SECURITY_ID_WORDS = /* @__PURE__ */ new Set([
3001
+ "aria",
3002
+ "cache",
3003
+ "component",
3004
+ "correlation",
3005
+ "dev",
3006
+ "dialog",
3007
+ "dom",
3008
+ "element",
3009
+ "execution",
3010
+ "field",
3011
+ "form",
3012
+ "hmr",
3013
+ "input",
3014
+ "marker",
3015
+ "menu",
3016
+ "mock",
3017
+ "perf",
3018
+ "req",
3019
+ "request",
3020
+ "select",
3021
+ "tab",
3022
+ "temp",
3023
+ "test",
3024
+ "tmp",
3025
+ "trace"
3026
+ ]);
3027
+ function nameWords(name) {
3028
+ return name.replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2").split(/[^A-Za-z0-9]+/u).filter(Boolean).map((word) => word.toLowerCase());
3029
+ }
3030
+ function isStrongSecurityName(name) {
3031
+ const words = nameWords(name);
3032
+ return words.some((word) => STRONG_SECURITY_WORDS.has(word)) || words.some(
3033
+ (word, index) => word === "api" && words[index + 1] === "key" || word === "auth" && words[index + 1] === "id" || word === "verification" && words[index + 1] === "code"
3034
+ );
3035
+ }
3036
+ function isNonSecurityName(name) {
3037
+ return nameWords(name).some((word) => NON_SECURITY_ID_WORDS.has(word));
3038
+ }
2966
3039
  var PATH_OR_DOM_MARKER = /[\\/#]|\.[A-Za-z0-9]/;
2967
3040
  function isMathRandomCall(node) {
2968
3041
  if (node.type !== "CallExpression") {
@@ -2975,70 +3048,57 @@ function isMathRandomCall(node) {
2975
3048
  const { object, property } = callee;
2976
3049
  return object.type === "Identifier" && object.name === "Math" && property.type === "Identifier" && property.name === "random";
2977
3050
  }
2978
- function isPartOfToString36Chain(node) {
2979
- let current = node;
2980
- let parent = current.parent;
2981
- while (parent) {
2982
- if (parent.type === "MemberExpression" && parent.object === current && !parent.computed && parent.property.type === "Identifier" && parent.property.name === "toString") {
2983
- const grandparent = parent.parent;
2984
- if (grandparent && grandparent.type === "CallExpression" && grandparent.callee === parent) {
2985
- const firstArg = grandparent.arguments[0];
2986
- if (firstArg && firstArg.type === "Literal" && firstArg.value === 36) {
2987
- return true;
2988
- }
2989
- }
2990
- }
2991
- if (parent.type === "MemberExpression" && parent.object === current) {
2992
- current = parent;
2993
- parent = current.parent;
2994
- continue;
2995
- }
2996
- if (parent.type === "CallExpression" && parent.callee === current) {
2997
- current = parent;
2998
- parent = current.parent;
2999
- continue;
3000
- }
3001
- break;
3002
- }
3003
- return false;
3004
- }
3005
- function findEnclosingName(node) {
3051
+ function findEnclosingNames(node) {
3052
+ const names = [];
3053
+ let directBinding = true;
3006
3054
  let current = node;
3007
3055
  let parent = current.parent;
3008
3056
  while (parent) {
3009
3057
  if (parent.type === "VariableDeclarator" && parent.init === current) {
3010
- if (parent.id.type === "Identifier") {
3011
- return parent.id.name;
3058
+ if (directBinding && parent.id.type === "Identifier") {
3059
+ names.push(parent.id.name);
3012
3060
  }
3013
- return void 0;
3014
3061
  }
3015
3062
  if (parent.type === "Property" && parent.value === current) {
3016
3063
  const key = parent.key;
3017
3064
  if (!parent.computed && key.type === "Identifier") {
3018
- return key.name;
3065
+ names.push(key.name);
3019
3066
  }
3020
3067
  if (key.type === "Literal" && typeof key.value === "string") {
3021
- return key.value;
3068
+ names.push(key.value);
3022
3069
  }
3023
- return void 0;
3070
+ directBinding = false;
3024
3071
  }
3025
3072
  if (parent.type === "PropertyDefinition" && parent.value === current) {
3026
3073
  const key = parent.key;
3027
3074
  if (!parent.computed && key.type === "Identifier") {
3028
- return key.name;
3075
+ names.push(key.name);
3029
3076
  }
3030
3077
  if (key.type === "Literal" && typeof key.value === "string") {
3031
- return key.value;
3078
+ names.push(key.value);
3032
3079
  }
3033
- return void 0;
3080
+ directBinding = false;
3034
3081
  }
3035
- if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression" || parent.type === "BlockStatement" || parent.type === "ReturnStatement" || parent.type === "ExpressionStatement") {
3036
- return void 0;
3082
+ if (parent.type === "AssignmentExpression" && parent.right === current) {
3083
+ if (directBinding && parent.left.type === "Identifier") names.push(parent.left.name);
3084
+ if (directBinding && parent.left.type === "MemberExpression" && !parent.left.computed && parent.left.property.type === "Identifier") {
3085
+ names.push(parent.left.property.name);
3086
+ }
3087
+ }
3088
+ if (parent.type === "ObjectExpression" || parent.type === "ArrayExpression") {
3089
+ directBinding = false;
3090
+ }
3091
+ if (parent.type === "FunctionDeclaration") {
3092
+ if (directBinding && parent.id !== null) names.push(parent.id.name);
3093
+ return names;
3094
+ }
3095
+ if (parent.type === "ExpressionStatement") {
3096
+ return names;
3037
3097
  }
3038
3098
  current = parent;
3039
3099
  parent = current.parent;
3040
3100
  }
3041
- return void 0;
3101
+ return names;
3042
3102
  }
3043
3103
  function isConcatenatedIntoPathOrDomId(node) {
3044
3104
  const valueNode = climbValueChain(node);
@@ -3124,20 +3184,17 @@ var no_insecure_random_id_default = createRule({
3124
3184
  if (!isMathRandomCall(node)) {
3125
3185
  return;
3126
3186
  }
3127
- const name = findEnclosingName(node);
3128
- if (name !== void 0 && STRONG_SECURITY_PATTERN.test(name)) {
3187
+ const names = findEnclosingNames(node);
3188
+ if (names.some(isStrongSecurityName)) {
3129
3189
  context.report({ node, messageId: "insecureRandomId" });
3130
3190
  return;
3131
3191
  }
3132
- if (name !== void 0 && NON_SECURITY_ID_PATTERN.test(name)) {
3192
+ if (names.some(isNonSecurityName)) {
3133
3193
  return;
3134
3194
  }
3135
3195
  if (isConcatenatedIntoPathOrDomId(node)) {
3136
3196
  return;
3137
3197
  }
3138
- if (isPartOfToString36Chain(node)) {
3139
- context.report({ node, messageId: "insecureRandomId" });
3140
- }
3141
3198
  }
3142
3199
  };
3143
3200
  }
@@ -4801,7 +4858,8 @@ var DEFAULT_ALLOW = [
4801
4858
  "[\\\\/](connectors|providers|integrations|adapters|fetchers)[\\\\/]",
4802
4859
  "[\\\\/]notifications[\\\\/]",
4803
4860
  "[Ss]ervice\\.[cm]?[jt]sx?$",
4804
- "-(service|connector|adapter|sdk|fetcher)\\.[cm]?[jt]sx?$"
4861
+ "-(service|connector|adapter|sdk|fetcher)\\.[cm]?[jt]sx?$",
4862
+ "[\\\\/][^\\\\/]*(?:Client|client)\\.[cm]?[jt]sx?$"
4805
4863
  ];
4806
4864
  var NON_PRODUCTION_TREE_RE = /[\\/](playwright|cypress|__testfixtures__)[\\/]/;
4807
4865
  var GLOBAL_RECEIVERS = /* @__PURE__ */ new Set([
@@ -4935,7 +4993,7 @@ var no_raw_fetch_outside_clients_default = createRule({
4935
4993
  defaultOptions: [{}],
4936
4994
  create(context, [options]) {
4937
4995
  const filename = context.filename;
4938
- if (isTestFile(filename) || NON_PRODUCTION_TREE_RE.test(filename)) {
4996
+ if (isTestFile(filename) || isScriptFile(filename) || NON_PRODUCTION_TREE_RE.test(filename)) {
4939
4997
  return {};
4940
4998
  }
4941
4999
  const patterns = options?.allow ?? DEFAULT_ALLOW;
@@ -4975,10 +5033,15 @@ var no_raw_fetch_outside_clients_default = createRule({
4975
5033
  function isInternalApiUrl(node) {
4976
5034
  const resolved = resolveNode2(node ?? void 0);
4977
5035
  if (resolved?.type === AST_NODE_TYPES18.Literal) {
4978
- return typeof resolved.value === "string" && resolved.value.startsWith("/api/");
5036
+ return typeof resolved.value === "string" && /^\/(?!\/)(?:[^/]+\/)*api(?:\/|$)/.test(resolved.value);
4979
5037
  }
4980
5038
  if (resolved?.type === AST_NODE_TYPES18.TemplateLiteral) {
4981
- return resolved.quasis[0]?.value.cooked?.startsWith("/api/") === true;
5039
+ const prefix = resolved.quasis[0]?.value.cooked;
5040
+ return typeof prefix === "string" && /^\/(?!\/)(?:[^/]+\/)*api(?:\/|$)/.test(prefix);
5041
+ }
5042
+ if (resolved?.type === AST_NODE_TYPES18.CallExpression && resolved.callee.type === AST_NODE_TYPES18.Identifier && resolved.callee.name === "withBase") {
5043
+ const first = resolved.arguments[0];
5044
+ return first !== void 0 && first.type !== AST_NODE_TYPES18.SpreadElement ? isInternalApiUrl(first) : false;
4982
5045
  }
4983
5046
  return resolved?.type === AST_NODE_TYPES18.BinaryExpression && resolved.operator === "+" && isInternalApiUrl(resolved.left);
4984
5047
  }
@@ -5822,12 +5885,17 @@ var BLOB_REDACTION_TOKENS = /* @__PURE__ */ new Set([
5822
5885
  "public"
5823
5886
  ]);
5824
5887
  function rawBlobValueName(value) {
5888
+ if (value.type === "AwaitExpression") return rawBlobValueName(value.argument);
5889
+ if (value.type === "ChainExpression") return rawBlobValueName(value.expression);
5825
5890
  if (value.type === "Identifier") {
5826
5891
  return isRawBlobName(value.name) ? value.name : null;
5827
5892
  }
5828
5893
  if (value.type === "MemberExpression" && !value.computed && value.property.type === "Identifier") {
5829
5894
  return isRawBlobName(value.property.name) ? value.property.name : null;
5830
5895
  }
5896
+ if (value.type === "CallExpression" && value.arguments.length === 0 && value.callee.type === "MemberExpression" && !value.callee.computed && value.callee.object.type === "Identifier" && /^(?:res|response|\w+Response)$/.test(value.callee.object.name) && value.callee.property.type === "Identifier" && (value.callee.property.name === "json" || value.callee.property.name === "text")) {
5897
+ return `${value.callee.object.name}.${value.callee.property.name}()`;
5898
+ }
5831
5899
  return null;
5832
5900
  }
5833
5901
  function isRawBlobName(name) {
@@ -6492,6 +6560,9 @@ var isExplanatory = (comment) => !DIRECTIVE_COMMENT_RE.test(comment.value);
6492
6560
  function isTeardownCall(node) {
6493
6561
  return node.type === AST_NODE_TYPES24.CallExpression && node.callee.type === AST_NODE_TYPES24.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES24.Identifier && TEARDOWN_METHODS.has(node.callee.property.name);
6494
6562
  }
6563
+ function isCancelledWebShare(node) {
6564
+ return node.type === AST_NODE_TYPES24.CallExpression && node.callee.type === AST_NODE_TYPES24.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES24.Identifier && node.callee.object.name === "navigator" && node.callee.property.type === AST_NODE_TYPES24.Identifier && node.callee.property.name === "share";
6565
+ }
6495
6566
  function isSilentHandler(handler) {
6496
6567
  const body2 = handler.body;
6497
6568
  if (body2.type !== AST_NODE_TYPES24.BlockStatement) {
@@ -6541,7 +6612,7 @@ var no_silent_promise_catch_default = createRule({
6541
6612
  },
6542
6613
  defaultOptions: [],
6543
6614
  create(context) {
6544
- if (isTestFile(context.filename)) {
6615
+ if (isTestFile(context.filename) || isScriptFile(context.filename)) {
6545
6616
  return {};
6546
6617
  }
6547
6618
  const hasExplanatoryComment = (call, handler) => {
@@ -6574,7 +6645,10 @@ var no_silent_promise_catch_default = createRule({
6574
6645
  if (isTeardownCall(node.callee.object)) {
6575
6646
  return;
6576
6647
  }
6577
- if (node.parent.type === AST_NODE_TYPES24.MemberExpression && node.parent.object === node) {
6648
+ if (isCancelledWebShare(node.callee.object)) {
6649
+ return;
6650
+ }
6651
+ if (node.parent.type === AST_NODE_TYPES24.MemberExpression && node.parent.object === node && !node.parent.computed && node.parent.property.type === AST_NODE_TYPES24.Identifier && node.parent.property.name === "then") {
6578
6652
  return;
6579
6653
  }
6580
6654
  const expectedArguments = method === "catch" ? 1 : 2;
@@ -6886,7 +6960,20 @@ function isStringInitializedVariable(variable) {
6886
6960
  if (declarator.type !== "VariableDeclarator") {
6887
6961
  return false;
6888
6962
  }
6889
- return isStringLiteralInit(declarator.init);
6963
+ if (isStringLiteralInit(declarator.init)) return true;
6964
+ if (declarator.id.type === "Identifier" && declarator.id.typeAnnotation?.typeAnnotation.type === "TSStringKeyword") {
6965
+ return true;
6966
+ }
6967
+ return isTemplateStringsArrayElement(declarator.init, variable.scope);
6968
+ }
6969
+ function isTemplateStringsArrayElement(node, scope) {
6970
+ if (node?.type !== "MemberExpression" || !node.computed || node.object.type !== "Identifier") {
6971
+ return false;
6972
+ }
6973
+ const source = findVariable(scope, node.object.name);
6974
+ if (source?.defs.length !== 1) return false;
6975
+ const name = source.defs[0]?.name;
6976
+ return name?.type === "Identifier" && name.typeAnnotation?.typeAnnotation.type === "TSTypeReference" && name.typeAnnotation.typeAnnotation.typeName.type === "Identifier" && name.typeAnnotation.typeAnnotation.typeName.name === "TemplateStringsArray";
6890
6977
  }
6891
6978
  function isStringLiteralInit(node) {
6892
6979
  if (node === null) {
@@ -6920,12 +7007,13 @@ function isConcatOperand(node, target) {
6920
7007
  }
6921
7008
  return false;
6922
7009
  }
6923
- function isDeclaredInsideLoop(variable, loop) {
7010
+ function isDeclaredInsideLoop(variable, repetition) {
6924
7011
  const def = variable.defs[0];
6925
7012
  if (def === void 0) {
6926
7013
  return false;
6927
7014
  }
6928
- const body2 = loop.body;
7015
+ const body2 = repetition.type === "CallExpression" ? repetition.arguments[0] : repetition.body;
7016
+ if (body2 === void 0 || body2.type === "SpreadElement") return false;
6929
7017
  const [declStart, declEnd] = def.node.range;
6930
7018
  const [bodyStart, bodyEnd] = body2.range;
6931
7019
  return declStart >= bodyStart && declEnd <= bodyEnd;
@@ -6934,6 +7022,9 @@ function enclosingLoop(node) {
6934
7022
  let child = node;
6935
7023
  let parent = node.parent;
6936
7024
  while (parent !== void 0 && parent !== null) {
7025
+ if ((parent.type === "ArrowFunctionExpression" || parent.type === "FunctionExpression") && parent.parent.type === "CallExpression" && parent.parent.arguments[0] === parent && parent.parent.callee.type === "MemberExpression" && !parent.parent.callee.computed && parent.parent.callee.property.type === "Identifier" && parent.parent.callee.property.name === "forEach") {
7026
+ return parent.parent;
7027
+ }
6937
7028
  if (LOOP_NODE_TYPES.has(parent.type)) {
6938
7029
  const loop = parent;
6939
7030
  if (loop.body === child) {
@@ -6945,6 +7036,17 @@ function enclosingLoop(node) {
6945
7036
  }
6946
7037
  return null;
6947
7038
  }
7039
+ function isSmallStaticForLoop(node) {
7040
+ if (node.type !== "ForStatement" || node.init?.type !== "VariableDeclaration" || node.init.declarations.length !== 1 || node.test?.type !== "BinaryExpression" || node.test.operator !== "<" && node.test.operator !== "<=" || node.update?.type !== "UpdateExpression" || node.update.operator !== "++") {
7041
+ return false;
7042
+ }
7043
+ const declaration = node.init.declarations[0];
7044
+ if (declaration?.id.type !== "Identifier" || declaration.init?.type !== "Literal" || typeof declaration.init.value !== "number" || !Number.isInteger(declaration.init.value) || node.test.left.type !== "Identifier" || node.test.left.name !== declaration.id.name || node.test.right.type !== "Literal" || typeof node.test.right.value !== "number" || !Number.isInteger(node.test.right.value) || node.update.argument.type !== "Identifier" || node.update.argument.name !== declaration.id.name) {
7045
+ return false;
7046
+ }
7047
+ const iterations = node.test.right.value - declaration.init.value + (node.test.operator === "<=" ? 1 : 0);
7048
+ return iterations >= 0 && iterations <= 8;
7049
+ }
6948
7050
  var no_string_concat_in_loop_default = createRule({
6949
7051
  name: "no-string-concat-in-loop",
6950
7052
  documentation: noStringConcatInLoopDocumentation,
@@ -6977,6 +7079,9 @@ var no_string_concat_in_loop_default = createRule({
6977
7079
  if (loop === null) {
6978
7080
  return;
6979
7081
  }
7082
+ if (isSmallStaticForLoop(loop)) {
7083
+ return;
7084
+ }
6980
7085
  const scope = context.sourceCode.getScope(node);
6981
7086
  const variable = findVariable(scope, node.left.name);
6982
7087
  if (variable === void 0) {
@@ -8830,8 +8935,10 @@ function looksLikeMutuallyExclusiveState(typeLiteral) {
8830
8935
  let statusMemberCount = 0;
8831
8936
  let hasFailurePayload = false;
8832
8937
  let hasSuccessPayload = false;
8938
+ let hasUnrecognizedMember = false;
8833
8939
  for (const member of typeLiteral.members) {
8834
8940
  if (member.type !== AST_NODE_TYPES37.TSPropertySignature) {
8941
+ hasUnrecognizedMember = true;
8835
8942
  continue;
8836
8943
  }
8837
8944
  const name = getMemberName(member);
@@ -8840,15 +8947,18 @@ function looksLikeMutuallyExclusiveState(typeLiteral) {
8840
8947
  continue;
8841
8948
  }
8842
8949
  if (!member.optional || isBooleanTyped(member) || name === null) {
8950
+ hasUnrecognizedMember = true;
8843
8951
  continue;
8844
8952
  }
8845
8953
  if (FAILURE_MEMBER_NAMES.has(name)) {
8846
8954
  hasFailurePayload = true;
8847
8955
  } else if (SUCCESS_PAYLOAD_MEMBER_NAMES.has(name)) {
8848
8956
  hasSuccessPayload = true;
8957
+ } else {
8958
+ hasUnrecognizedMember = true;
8849
8959
  }
8850
8960
  }
8851
- return statusMemberCount === REQUIRED_STATUS_MEMBER_COUNT && hasFailurePayload && hasSuccessPayload;
8961
+ return statusMemberCount === REQUIRED_STATUS_MEMBER_COUNT && hasFailurePayload && (hasSuccessPayload || !hasUnrecognizedMember);
8852
8962
  }
8853
8963
  function getMemberName(member) {
8854
8964
  if (member.type !== AST_NODE_TYPES37.TSPropertySignature) {
@@ -10749,7 +10859,7 @@ var prefer_schema_for_api_payload_default = createRule({
10749
10859
  },
10750
10860
  defaultOptions: [],
10751
10861
  create(context) {
10752
- if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
10862
+ if (isTestFile(context.filename) || isScriptFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
10753
10863
  return {};
10754
10864
  }
10755
10865
  const unvalidatedVariables = /* @__PURE__ */ new Set();
@@ -12404,6 +12514,26 @@ var require_fetch_timeout_default = createRule({
12404
12514
  }
12405
12515
  return callee.type === AST_NODE_TYPES50.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES50.Identifier && callee.property.name === "fetch" && callee.object.type === AST_NODE_TYPES50.Identifier && GLOBAL_OBJECTS2.has(callee.object.name) && resolvesToGlobal(callee.object);
12406
12516
  }
12517
+ function localConstInitProvablyLacksSignal(identifier) {
12518
+ const variable = ASTUtils12.findVariable(
12519
+ context.sourceCode.getScope(identifier),
12520
+ identifier.name
12521
+ );
12522
+ if (variable?.defs.length !== 1) return false;
12523
+ const definition = variable.defs[0];
12524
+ if (definition?.type !== "Variable" || definition.parent.kind !== "const" || definition.node.init?.type !== AST_NODE_TYPES50.ObjectExpression || !initProvablyLacksSignal(definition.node.init)) {
12525
+ return false;
12526
+ }
12527
+ for (const reference of variable.references) {
12528
+ const ref = reference.identifier;
12529
+ if (ref === identifier || ref === definition.name) continue;
12530
+ const member = ref.parent;
12531
+ if (member.type !== AST_NODE_TYPES50.MemberExpression || member.object !== ref || member.computed || member.property.type !== AST_NODE_TYPES50.Identifier || member.property.name === "signal" || member.parent.type !== AST_NODE_TYPES50.AssignmentExpression || member.parent.left !== member) {
12532
+ return false;
12533
+ }
12534
+ }
12535
+ return true;
12536
+ }
12407
12537
  return {
12408
12538
  CallExpression(node) {
12409
12539
  if (!isGlobalFetchCall2(node.callee)) {
@@ -12413,7 +12543,7 @@ var require_fetch_timeout_default = createRule({
12413
12543
  if (node.arguments.length === 1 && first !== void 0 && !isInlineUrl(first, resolvesToGlobal)) {
12414
12544
  return;
12415
12545
  }
12416
- if (init === void 0 || initProvablyLacksSignal(init)) {
12546
+ if (init === void 0 || initProvablyLacksSignal(init) || init.type === AST_NODE_TYPES50.Identifier && localConstInitProvablyLacksSignal(init)) {
12417
12547
  context.report({ node, messageId: "missingSignal" });
12418
12548
  }
12419
12549
  }
@@ -13168,8 +13298,8 @@ var require_zod_form_validation_default = createRule({
13168
13298
  // src/rules/store-insert-requires-on-conflict.ts
13169
13299
  import "@typescript-eslint/utils";
13170
13300
  var storeInsertRequiresOnConflictDocumentation = {
13171
- summary: "Require an embedded SQL INSERT to carry ON CONFLICT; store writes replay under cron re-runs and queue redelivery and must be idempotent upserts.",
13172
- rationale: "A replayed bare insert can duplicate data or fail on a uniqueness constraint.",
13301
+ summary: "Require embedded inserts in explicitly replayable callables to carry conflict handling.",
13302
+ rationale: "A callable named as an enqueue, seed, migration, schedule, ensure, or upsert promises replay safety.",
13173
13303
  remediation: "Add an appropriate `ON CONFLICT` action or supported replay-safe insert form.",
13174
13304
  category: "correctness",
13175
13305
  examples: [
@@ -13178,7 +13308,25 @@ var storeInsertRequiresOnConflictDocumentation = {
13178
13308
  ]
13179
13309
  };
13180
13310
  var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
13181
- var CONFLICT_HANDLED = /\bON\s+CONFLICT\b|\bON\s+DUPLICATE\s+KEY\b|\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b/i;
13311
+ var CONFLICT_HANDLED = /\bON\s+CONFLICT\b|\bON\s+DUPLICATE\s+KEY\b|\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b|\bINSERT\b[\s\S]*?\bSELECT\b[\s\S]*?\bWHERE\s+NOT\s+EXISTS\b/i;
13312
+ var REPLAY_CONTRACT_NAME = /(?:enqueue|ensure|migrate|recordOnce|schedule|seed|upsert|getOrCreate|createIfAbsent|insertIfAbsent)/i;
13313
+ function owningCallableName(node) {
13314
+ for (let current = node.parent; current !== null && current !== void 0; current = current.parent) {
13315
+ if (current.type === "FunctionDeclaration") {
13316
+ return current.id?.name ?? null;
13317
+ }
13318
+ if (current.type === "MethodDefinition") {
13319
+ return current.key.type === "Identifier" ? current.key.name : null;
13320
+ }
13321
+ if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "VariableDeclarator" && current.parent.id.type === "Identifier") {
13322
+ return current.parent.id.name;
13323
+ }
13324
+ if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent.type === "Property" && current.parent.key.type === "Identifier") {
13325
+ return current.parent.key.name;
13326
+ }
13327
+ }
13328
+ return null;
13329
+ }
13182
13330
  var INSERT_GATE = /insert/i;
13183
13331
  var store_insert_requires_on_conflict_default = createRule({
13184
13332
  name: "store-insert-requires-on-conflict",
@@ -13186,7 +13334,7 @@ var store_insert_requires_on_conflict_default = createRule({
13186
13334
  meta: {
13187
13335
  type: "problem",
13188
13336
  docs: {
13189
- description: "Require an embedded SQL INSERT to carry ON CONFLICT; store writes replay under cron re-runs and queue redelivery and must be idempotent upserts."
13337
+ description: "Require embedded inserts in explicitly replayable callables to carry conflict handling."
13190
13338
  },
13191
13339
  schema: [],
13192
13340
  messages: {
@@ -13202,6 +13350,10 @@ var store_insert_requires_on_conflict_default = createRule({
13202
13350
  if (!INSERT_WRITE.test(sql) || CONFLICT_HANDLED.test(sql)) {
13203
13351
  return;
13204
13352
  }
13353
+ const owner = owningCallableName(node);
13354
+ if (owner !== null && !REPLAY_CONTRACT_NAME.test(owner)) {
13355
+ return;
13356
+ }
13205
13357
  context.report({ node, messageId: "storeInsertRequiresOnConflict" });
13206
13358
  });
13207
13359
  }
@@ -13891,7 +14043,7 @@ var rules = {
13891
14043
  };
13892
14044
  var meta = {
13893
14045
  name: "@sarj/eslint-plugin",
13894
- version: "15.0.0"
14046
+ version: "15.1.0"
13895
14047
  };
13896
14048
  var applicationOnlyRules = [
13897
14049
  "no-restricted-library-load",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "15.0.0",
3
+ "version": "15.1.0",
4
4
  "packageManager": "npm@11.19.0",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",